@oxyhq/bloom 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +34 -12
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/combobox/Combobox.js +3 -3
- package/lib/commonjs/combobox/Combobox.js.map +1 -1
- package/lib/commonjs/command/Command.js +3 -3
- package/lib/commonjs/command/Command.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +74 -5
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +153 -10
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +84 -29
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +365 -0
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -0
- package/lib/commonjs/dialog/index.js +7 -0
- package/lib/commonjs/dialog/index.js.map +1 -1
- package/lib/commonjs/dialog/index.web.js +7 -0
- package/lib/commonjs/dialog/index.web.js.map +1 -1
- package/lib/commonjs/index.js +11 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +11 -4
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/react-native-className.d.js +6 -0
- package/lib/commonjs/react-native-className.d.js.map +1 -0
- package/lib/commonjs/{search-input → search}/index.js +5 -3
- package/lib/commonjs/search/index.js.map +1 -0
- package/lib/commonjs/settings-list/SettingsList.js +5 -1
- package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +22 -9
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
- package/lib/commonjs/text-field/index.js +22 -6
- package/lib/commonjs/text-field/index.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +34 -12
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/combobox/Combobox.js +3 -3
- package/lib/module/combobox/Combobox.js.map +1 -1
- package/lib/module/command/Command.js +3 -3
- package/lib/module/command/Command.js.map +1 -1
- package/lib/module/dialog/Dialog.js +75 -6
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +156 -13
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +85 -30
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +356 -0
- package/lib/module/dialog/DialogHeader.js.map +1 -0
- package/lib/module/dialog/index.js +1 -0
- package/lib/module/dialog/index.js.map +1 -1
- package/lib/module/dialog/index.web.js +1 -0
- package/lib/module/dialog/index.web.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -2
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/react-native-className.d.js +20 -0
- package/lib/module/react-native-className.d.js.map +1 -0
- package/lib/module/{search-input → search}/index.js +6 -2
- package/lib/module/search/index.js.map +1 -0
- package/lib/module/settings-list/SettingsList.js +5 -1
- package/lib/module/settings-list/SettingsList.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +22 -9
- package/lib/module/surfaces/SurfaceHost.js.map +1 -1
- package/lib/module/text-field/index.js +22 -6
- package/lib/module/text-field/index.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +23 -0
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/combobox/Combobox.d.ts +1 -1
- package/lib/typescript/commonjs/command/Command.d.ts +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +2 -2
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +86 -0
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/index.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +75 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/common.d.ts +1 -0
- package/lib/typescript/commonjs/icons/common.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +3 -3
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-group/types.d.ts +1 -1
- package/lib/typescript/commonjs/{search-input → search}/index.d.ts +1 -1
- package/lib/typescript/commonjs/search/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/types.d.ts +17 -1
- package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/index.d.ts +2 -1
- package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +23 -0
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/combobox/Combobox.d.ts +1 -1
- package/lib/typescript/module/command/Command.d.ts +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +2 -2
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogHeader.d.ts +86 -0
- package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -0
- package/lib/typescript/module/dialog/index.d.ts +2 -1
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +2 -1
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +75 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/icons/common.d.ts +1 -0
- package/lib/typescript/module/icons/common.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +3 -3
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/input-group/types.d.ts +1 -1
- package/lib/typescript/module/{search-input → search}/index.d.ts +1 -1
- package/lib/typescript/module/search/index.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/types.d.ts +17 -1
- package/lib/typescript/module/surfaces/types.d.ts.map +1 -1
- package/lib/typescript/module/text-field/index.d.ts +2 -1
- package/lib/typescript/module/text-field/index.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/BottomSheet.test.tsx +47 -0
- package/src/__tests__/DialogBottomSheet.test.tsx +30 -5
- package/src/bottom-sheet/BottomSheetBase.tsx +59 -10
- package/src/combobox/Combobox.tsx +3 -3
- package/src/command/Command.tsx +3 -3
- package/src/dialog/Dialog.tsx +74 -4
- package/src/dialog/Dialog.web.tsx +154 -23
- package/src/dialog/DialogBottomSheet.tsx +90 -28
- package/src/dialog/DialogHeader.tsx +420 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/index.web.ts +2 -0
- package/src/dialog/types.ts +76 -0
- package/src/index.ts +3 -1
- package/src/index.web.ts +3 -1
- package/src/input-group/types.ts +1 -1
- package/src/react-native-className.d.ts +38 -0
- package/src/{search-input → search}/index.tsx +8 -5
- package/src/settings-list/SettingsList.tsx +5 -1
- package/src/surfaces/SurfaceHost.tsx +16 -1
- package/src/surfaces/types.ts +17 -1
- package/src/text-field/index.tsx +24 -5
- package/lib/commonjs/search-input/index.js.map +0 -1
- package/lib/module/search-input/index.js.map +0 -1
- package/lib/typescript/commonjs/search-input/index.d.ts.map +0 -1
- package/lib/typescript/module/search-input/index.d.ts.map +0 -1
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -30,6 +30,14 @@ import { useTheme } from '../theme/use-theme';
|
|
|
30
30
|
import { Context, useDialogControl } from './context';
|
|
31
31
|
import { DialogBody } from './DialogContent';
|
|
32
32
|
import { DialogBottomSheet } from './DialogBottomSheet';
|
|
33
|
+
import {
|
|
34
|
+
DIALOG_NAV_BAR_HEIGHT,
|
|
35
|
+
type DialogHeaderController,
|
|
36
|
+
DialogHeaderProvider,
|
|
37
|
+
DialogLargeTitle,
|
|
38
|
+
DialogNavHeader,
|
|
39
|
+
useDialogHeaderController,
|
|
40
|
+
} from './DialogHeader';
|
|
33
41
|
import {
|
|
34
42
|
ANIMATION_DURATION,
|
|
35
43
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
@@ -103,6 +111,8 @@ function CenteredOrSideDialog({
|
|
|
103
111
|
title,
|
|
104
112
|
description,
|
|
105
113
|
actions,
|
|
114
|
+
header,
|
|
115
|
+
scrollable,
|
|
106
116
|
placement,
|
|
107
117
|
layer,
|
|
108
118
|
width = DEFAULT_SIDE_WIDTH,
|
|
@@ -119,6 +129,8 @@ function CenteredOrSideDialog({
|
|
|
119
129
|
}: Omit<DialogProps, 'placement'> & { placement: 'center' | 'left' | 'right' }) {
|
|
120
130
|
const isControlled = controlledOpen !== undefined;
|
|
121
131
|
const isSide = placement === 'left' || placement === 'right';
|
|
132
|
+
const headerController = useDialogHeaderController();
|
|
133
|
+
const scrollableResolved = scrollable ?? true;
|
|
122
134
|
|
|
123
135
|
// Imperative open state for the side placement (the BottomSheet path owns its
|
|
124
136
|
// own visibility via its ref instead).
|
|
@@ -235,6 +247,24 @@ function CenteredOrSideDialog({
|
|
|
235
247
|
</DialogBody>
|
|
236
248
|
);
|
|
237
249
|
|
|
250
|
+
// Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
|
|
251
|
+
// collapsing title over its own scroll content. The large title + screen body
|
|
252
|
+
// scroll under the bar, feeding the shared collapse controller. `contentPadding`
|
|
253
|
+
// is dropped (the large title + screens own their padding).
|
|
254
|
+
const headerBody = header ? (
|
|
255
|
+
scrollableResolved ? (
|
|
256
|
+
<>
|
|
257
|
+
<DialogLargeTitle controller={headerController} header={header} />
|
|
258
|
+
<DialogHeaderProvider controller={headerController}>{bodyNode}</DialogHeaderProvider>
|
|
259
|
+
</>
|
|
260
|
+
) : (
|
|
261
|
+
<>
|
|
262
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
263
|
+
<DialogHeaderProvider controller={headerController}>{bodyNode}</DialogHeaderProvider>
|
|
264
|
+
</>
|
|
265
|
+
)
|
|
266
|
+
) : null;
|
|
267
|
+
|
|
238
268
|
if (isSide) {
|
|
239
269
|
return (
|
|
240
270
|
<Context.Provider value={context}>
|
|
@@ -253,6 +283,9 @@ function CenteredOrSideDialog({
|
|
|
253
283
|
description={description}
|
|
254
284
|
titleId={titleId}
|
|
255
285
|
descriptionId={descriptionId}
|
|
286
|
+
header={header}
|
|
287
|
+
headerController={headerController}
|
|
288
|
+
onHeaderDismiss={close}
|
|
256
289
|
panelStyle={panelStyle}
|
|
257
290
|
panelClassName={panelClassName}
|
|
258
291
|
containerStyle={containerStyle}
|
|
@@ -271,7 +304,21 @@ function CenteredOrSideDialog({
|
|
|
271
304
|
onDismiss={handleDismiss}
|
|
272
305
|
enablePanDownToClose
|
|
273
306
|
detached
|
|
274
|
-
|
|
307
|
+
// Nav-header mode hides the drag handle (the nav bar sits at the very top)
|
|
308
|
+
// and coordinates the body pan with the scroll so content scrolls.
|
|
309
|
+
showHandle={!header}
|
|
310
|
+
manualActivation={!!header}
|
|
311
|
+
scrollY={header ? headerController.scrollY : undefined}
|
|
312
|
+
headerOverlay={
|
|
313
|
+
header ? (
|
|
314
|
+
<DialogNavHeader
|
|
315
|
+
controller={headerController}
|
|
316
|
+
header={header}
|
|
317
|
+
onDismiss={close}
|
|
318
|
+
collapse={scrollableResolved}
|
|
319
|
+
/>
|
|
320
|
+
) : undefined
|
|
321
|
+
}
|
|
275
322
|
// Stronger dim when a Dialog is stacked over another sheet so the
|
|
276
323
|
// underlying sheet's handle/content doesn't bleed through.
|
|
277
324
|
backdropOpacity={0.7}
|
|
@@ -288,13 +335,14 @@ function CenteredOrSideDialog({
|
|
|
288
335
|
// Detached BottomSheet already adds `marginBottom: insets.bottom + 16`
|
|
289
336
|
// to the sheet container — the floating card sits ABOVE the
|
|
290
337
|
// system gesture bar, so we don't add `insets.bottom` here.
|
|
291
|
-
|
|
338
|
+
// Nav-header mode: the large title + screens own their padding.
|
|
339
|
+
header ? null : { padding: contentPadding },
|
|
292
340
|
{ backgroundColor: theme.colors.background },
|
|
293
341
|
style,
|
|
294
342
|
panelStyle,
|
|
295
343
|
]}
|
|
296
344
|
>
|
|
297
|
-
{bodyNode}
|
|
345
|
+
{header ? headerBody : bodyNode}
|
|
298
346
|
</View>
|
|
299
347
|
</Context.Provider>
|
|
300
348
|
</BottomSheet>
|
|
@@ -324,6 +372,9 @@ function SideSheet({
|
|
|
324
372
|
description,
|
|
325
373
|
titleId,
|
|
326
374
|
descriptionId,
|
|
375
|
+
header,
|
|
376
|
+
headerController,
|
|
377
|
+
onHeaderDismiss,
|
|
327
378
|
panelStyle,
|
|
328
379
|
panelClassName,
|
|
329
380
|
containerStyle,
|
|
@@ -345,6 +396,9 @@ function SideSheet({
|
|
|
345
396
|
description?: string;
|
|
346
397
|
titleId: string;
|
|
347
398
|
descriptionId: string;
|
|
399
|
+
header?: DialogProps['header'];
|
|
400
|
+
headerController: DialogHeaderController;
|
|
401
|
+
onHeaderDismiss: () => void;
|
|
348
402
|
panelStyle?: StyleProp<ViewStyle>;
|
|
349
403
|
panelClassName?: string;
|
|
350
404
|
containerStyle?: StyleProp<ViewStyle>;
|
|
@@ -476,7 +530,23 @@ function SideSheet({
|
|
|
476
530
|
]}
|
|
477
531
|
pointerEvents="auto"
|
|
478
532
|
>
|
|
479
|
-
|
|
533
|
+
{header ? (
|
|
534
|
+
// Nav-header mode on a side drawer: a static titled bar (the drawer body
|
|
535
|
+
// has no Dialog scroll offset to drive a collapse) over content inset
|
|
536
|
+
// below the bar.
|
|
537
|
+
<View style={{ flex: 1, minHeight: 0 }}>
|
|
538
|
+
<DialogNavHeader
|
|
539
|
+
controller={headerController}
|
|
540
|
+
header={header}
|
|
541
|
+
onDismiss={onHeaderDismiss}
|
|
542
|
+
collapse={false}
|
|
543
|
+
/>
|
|
544
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
545
|
+
<DialogHeaderProvider controller={headerController}>{children}</DialogHeaderProvider>
|
|
546
|
+
</View>
|
|
547
|
+
) : (
|
|
548
|
+
<View style={{ padding: contentPadding }}>{children}</View>
|
|
549
|
+
)}
|
|
480
550
|
</Animated.View>
|
|
481
551
|
</View>
|
|
482
552
|
);
|
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
12
|
Pressable,
|
|
13
|
+
ScrollView,
|
|
13
14
|
useWindowDimensions,
|
|
14
15
|
View,
|
|
15
16
|
type StyleProp,
|
|
@@ -20,9 +21,16 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
|
20
21
|
import { Portal } from '../portal/index.web';
|
|
21
22
|
import { createOverlayZIndex } from '../styles/z-index';
|
|
22
23
|
import { useTheme } from '../theme/use-theme';
|
|
23
|
-
import { Context, useDialogControl } from './context';
|
|
24
|
+
import { Context, useDialogContext, useDialogControl } from './context';
|
|
24
25
|
import { DialogBody } from './DialogContent';
|
|
25
26
|
import { DialogBottomSheet } from './DialogBottomSheet';
|
|
27
|
+
import {
|
|
28
|
+
DIALOG_NAV_BAR_HEIGHT,
|
|
29
|
+
DialogHeaderProvider,
|
|
30
|
+
DialogLargeTitle,
|
|
31
|
+
DialogNavHeader,
|
|
32
|
+
useDialogHeaderController,
|
|
33
|
+
} from './DialogHeader';
|
|
26
34
|
import {
|
|
27
35
|
ANIMATION_DURATION,
|
|
28
36
|
CENTER_FADE_OUT_DURATION,
|
|
@@ -125,11 +133,13 @@ export function Dialog({ placement, ...rest }: DialogProps) {
|
|
|
125
133
|
function CenterOrSideDialog({
|
|
126
134
|
control,
|
|
127
135
|
open: controlledOpen,
|
|
136
|
+
startOpen,
|
|
128
137
|
onClose,
|
|
129
138
|
testID,
|
|
130
139
|
title,
|
|
131
140
|
description,
|
|
132
141
|
actions,
|
|
142
|
+
header,
|
|
133
143
|
placement,
|
|
134
144
|
layer,
|
|
135
145
|
width = DEFAULT_SIDE_WIDTH,
|
|
@@ -137,6 +147,8 @@ function CenterOrSideDialog({
|
|
|
137
147
|
inset,
|
|
138
148
|
dismissOnBackdrop = true,
|
|
139
149
|
contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
|
|
150
|
+
maxHeightRatio,
|
|
151
|
+
scrollable,
|
|
140
152
|
style,
|
|
141
153
|
panelStyle,
|
|
142
154
|
panelClassName,
|
|
@@ -160,7 +172,13 @@ function CenterOrSideDialog({
|
|
|
160
172
|
const isControlled = controlledOpen !== undefined;
|
|
161
173
|
const resolvedPlacement = placement;
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
// Seed the initial visible state so a branch that mounts while the surface is
|
|
176
|
+
// already open (the imperative `startOpen` intent) or under a truthy
|
|
177
|
+
// controlled `open` renders open on its FIRST commit — this is what lets a
|
|
178
|
+
// responsive Dialog survive a placement swap (centered card ↔ bottom sheet on
|
|
179
|
+
// resize) without going blank. `startOpen` never flips the close semantics;
|
|
180
|
+
// the imperative `control.close()` still drives the exit + post-exit onClose.
|
|
181
|
+
const [isOpen, setIsOpen] = useState(() => controlledOpen ?? startOpen ?? false);
|
|
164
182
|
const [isClosing, setIsClosing] = useState(false);
|
|
165
183
|
const closeCallbacksRef = useRef<(() => void)[]>([]);
|
|
166
184
|
// Read the latest controlled flag inside stable callbacks without re-binding.
|
|
@@ -299,9 +317,12 @@ function CenterOrSideDialog({
|
|
|
299
317
|
title={title}
|
|
300
318
|
description={description}
|
|
301
319
|
actions={actions}
|
|
320
|
+
header={header}
|
|
302
321
|
style={style}
|
|
303
322
|
maxWidth={maxWidth}
|
|
304
323
|
contentPadding={contentPadding}
|
|
324
|
+
maxHeightRatio={maxHeightRatio}
|
|
325
|
+
scrollable={scrollable}
|
|
305
326
|
surfaceZIndex={dialogZIndex.surface}
|
|
306
327
|
isClosing={isClosing}
|
|
307
328
|
>
|
|
@@ -325,6 +346,8 @@ function CenterOrSideDialog({
|
|
|
325
346
|
title={title}
|
|
326
347
|
description={description}
|
|
327
348
|
actions={actions}
|
|
349
|
+
header={header}
|
|
350
|
+
scrollable={scrollable}
|
|
328
351
|
placement={resolvedPlacement}
|
|
329
352
|
shown={!isClosing}
|
|
330
353
|
width={width}
|
|
@@ -354,9 +377,12 @@ function DialogPanel({
|
|
|
354
377
|
title,
|
|
355
378
|
description,
|
|
356
379
|
actions,
|
|
380
|
+
header,
|
|
357
381
|
style,
|
|
358
382
|
maxWidth,
|
|
359
383
|
contentPadding,
|
|
384
|
+
maxHeightRatio,
|
|
385
|
+
scrollable,
|
|
360
386
|
surfaceZIndex,
|
|
361
387
|
isClosing,
|
|
362
388
|
children,
|
|
@@ -366,16 +392,33 @@ function DialogPanel({
|
|
|
366
392
|
title?: string;
|
|
367
393
|
description?: string;
|
|
368
394
|
actions?: DialogAction[];
|
|
395
|
+
header?: DialogProps['header'];
|
|
369
396
|
style?: DialogProps['style'];
|
|
370
397
|
maxWidth: number;
|
|
371
398
|
contentPadding: number;
|
|
399
|
+
maxHeightRatio?: number;
|
|
400
|
+
scrollable?: boolean;
|
|
372
401
|
surfaceZIndex: number;
|
|
373
402
|
isClosing: boolean;
|
|
374
403
|
children?: React.ReactNode;
|
|
375
404
|
}) {
|
|
376
405
|
const theme = useTheme();
|
|
406
|
+
const { close } = useDialogContext();
|
|
377
407
|
const titleId = useId();
|
|
378
408
|
const descriptionId = useId();
|
|
409
|
+
const headerController = useDialogHeaderController();
|
|
410
|
+
|
|
411
|
+
const body = (
|
|
412
|
+
<DialogBody
|
|
413
|
+
titleId={titleId}
|
|
414
|
+
descriptionId={descriptionId}
|
|
415
|
+
title={title}
|
|
416
|
+
description={description}
|
|
417
|
+
actions={actions}
|
|
418
|
+
>
|
|
419
|
+
{children}
|
|
420
|
+
</DialogBody>
|
|
421
|
+
);
|
|
379
422
|
|
|
380
423
|
return (
|
|
381
424
|
<View
|
|
@@ -393,6 +436,13 @@ function DialogPanel({
|
|
|
393
436
|
borderRadius: 20,
|
|
394
437
|
width: '100%',
|
|
395
438
|
maxWidth,
|
|
439
|
+
// The Dialog OWNS the size cap + scroll boundary (its content renders
|
|
440
|
+
// bare, edge-to-edge). Short content stays natural; tall content is
|
|
441
|
+
// capped at `maxHeightRatio` of the viewport and scrolls inside the
|
|
442
|
+
// rounded card (via the ScrollView below) — consumers never add their
|
|
443
|
+
// own height cap / ScrollView. `overflow: hidden` clips to the radius.
|
|
444
|
+
maxHeight: `${Math.round((maxHeightRatio ?? 0.9) * 100)}%`,
|
|
445
|
+
overflow: 'hidden',
|
|
396
446
|
backgroundColor: theme.colors.background,
|
|
397
447
|
borderWidth: 1,
|
|
398
448
|
borderColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
|
@@ -400,7 +450,6 @@ function DialogPanel({
|
|
|
400
450
|
shadowOpacity: theme.isDark ? 0.4 : 0.1,
|
|
401
451
|
shadowRadius: 30,
|
|
402
452
|
shadowOffset: { width: 0, height: 4 },
|
|
403
|
-
padding: contentPadding,
|
|
404
453
|
zIndex: surfaceZIndex,
|
|
405
454
|
},
|
|
406
455
|
isClosing
|
|
@@ -409,15 +458,56 @@ function DialogPanel({
|
|
|
409
458
|
style,
|
|
410
459
|
]}
|
|
411
460
|
>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
title
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
461
|
+
{header ? (
|
|
462
|
+
// Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
|
|
463
|
+
// collapsing title over its own scroll content (see `DialogHeader`). The
|
|
464
|
+
// bar overlays; the large title + screen body scroll under it, feeding
|
|
465
|
+
// the shared scroll offset that drives the small-title cross-fade.
|
|
466
|
+
<>
|
|
467
|
+
<DialogNavHeader
|
|
468
|
+
controller={headerController}
|
|
469
|
+
header={header}
|
|
470
|
+
onDismiss={close}
|
|
471
|
+
collapse={scrollable !== false}
|
|
472
|
+
/>
|
|
473
|
+
{scrollable === false ? (
|
|
474
|
+
<View style={{ flex: 1, minHeight: 0 }}>
|
|
475
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
476
|
+
<DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
|
|
477
|
+
</View>
|
|
478
|
+
) : (
|
|
479
|
+
<ScrollView
|
|
480
|
+
style={{ flexShrink: 1 }}
|
|
481
|
+
contentContainerStyle={{ paddingBottom: contentPadding }}
|
|
482
|
+
showsVerticalScrollIndicator={false}
|
|
483
|
+
scrollEventThrottle={16}
|
|
484
|
+
onScroll={(e) => {
|
|
485
|
+
headerController.scrollY.value = e.nativeEvent.contentOffset.y;
|
|
486
|
+
}}
|
|
487
|
+
>
|
|
488
|
+
<DialogLargeTitle controller={headerController} header={header} />
|
|
489
|
+
<DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
|
|
490
|
+
</ScrollView>
|
|
491
|
+
)}
|
|
492
|
+
</>
|
|
493
|
+
) : scrollable === false ? (
|
|
494
|
+
// The content owns its own scrolling (a FlatList / VirtualizedList or a
|
|
495
|
+
// nested ScrollView). Render it bare in a bounded flex box — the card's
|
|
496
|
+
// `maxHeight` + `overflow: hidden` cap it and `flex: 1` + `minHeight: 0`
|
|
497
|
+
// hand the child a bounded height to scroll into — with NO wrapping
|
|
498
|
+
// ScrollView, so a VirtualizedList never nests inside one.
|
|
499
|
+
<View style={{ flex: 1, minHeight: 0, padding: contentPadding }}>
|
|
500
|
+
{body}
|
|
501
|
+
</View>
|
|
502
|
+
) : (
|
|
503
|
+
<ScrollView
|
|
504
|
+
style={{ flexShrink: 1 }}
|
|
505
|
+
contentContainerStyle={{ padding: contentPadding }}
|
|
506
|
+
showsVerticalScrollIndicator={false}
|
|
507
|
+
>
|
|
508
|
+
{body}
|
|
509
|
+
</ScrollView>
|
|
510
|
+
)}
|
|
421
511
|
</View>
|
|
422
512
|
);
|
|
423
513
|
}
|
|
@@ -439,6 +529,8 @@ function SheetSurface({
|
|
|
439
529
|
title,
|
|
440
530
|
description,
|
|
441
531
|
actions,
|
|
532
|
+
header,
|
|
533
|
+
scrollable,
|
|
442
534
|
placement,
|
|
443
535
|
shown,
|
|
444
536
|
width,
|
|
@@ -460,6 +552,8 @@ function SheetSurface({
|
|
|
460
552
|
title?: string;
|
|
461
553
|
description?: string;
|
|
462
554
|
actions?: DialogAction[];
|
|
555
|
+
header?: DialogProps['header'];
|
|
556
|
+
scrollable?: boolean;
|
|
463
557
|
placement: 'left' | 'right';
|
|
464
558
|
shown: boolean;
|
|
465
559
|
width: number;
|
|
@@ -479,6 +573,7 @@ function SheetSurface({
|
|
|
479
573
|
const theme = useTheme();
|
|
480
574
|
const titleId = useId();
|
|
481
575
|
const descriptionId = useId();
|
|
576
|
+
const headerController = useDialogHeaderController();
|
|
482
577
|
const { width: viewportWidth } = useWindowDimensions();
|
|
483
578
|
|
|
484
579
|
// Defer the entry transition by a frame so the start state paints before the
|
|
@@ -588,17 +683,53 @@ function SheetSurface({
|
|
|
588
683
|
]}
|
|
589
684
|
pointerEvents="auto"
|
|
590
685
|
>
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
686
|
+
{header ? (
|
|
687
|
+
// Nav-header mode on a side drawer: a static titled bar (the drawer
|
|
688
|
+
// body does not own a Dialog scroll offset to drive a collapse) over
|
|
689
|
+
// the content, which is inset below the bar.
|
|
690
|
+
<View style={{ flex: 1, minHeight: 0 }}>
|
|
691
|
+
<DialogNavHeader
|
|
692
|
+
controller={headerController}
|
|
693
|
+
header={header}
|
|
694
|
+
onDismiss={onDismiss}
|
|
695
|
+
collapse={false}
|
|
696
|
+
/>
|
|
697
|
+
<ScrollView
|
|
698
|
+
style={{ flexShrink: 1 }}
|
|
699
|
+
contentContainerStyle={{ paddingTop: DIALOG_NAV_BAR_HEIGHT, paddingBottom: contentPadding }}
|
|
700
|
+
showsVerticalScrollIndicator={false}
|
|
701
|
+
scrollEventThrottle={16}
|
|
702
|
+
scrollEnabled={scrollable !== false}
|
|
703
|
+
onScroll={(e) => {
|
|
704
|
+
headerController.scrollY.value = e.nativeEvent.contentOffset.y;
|
|
705
|
+
}}
|
|
706
|
+
>
|
|
707
|
+
<DialogHeaderProvider controller={headerController}>
|
|
708
|
+
<DialogBody
|
|
709
|
+
titleId={titleId}
|
|
710
|
+
descriptionId={descriptionId}
|
|
711
|
+
title={title}
|
|
712
|
+
description={description}
|
|
713
|
+
actions={actions}
|
|
714
|
+
>
|
|
715
|
+
{children}
|
|
716
|
+
</DialogBody>
|
|
717
|
+
</DialogHeaderProvider>
|
|
718
|
+
</ScrollView>
|
|
719
|
+
</View>
|
|
720
|
+
) : (
|
|
721
|
+
<View style={{ padding: contentPadding }}>
|
|
722
|
+
<DialogBody
|
|
723
|
+
titleId={titleId}
|
|
724
|
+
descriptionId={descriptionId}
|
|
725
|
+
title={title}
|
|
726
|
+
description={description}
|
|
727
|
+
actions={actions}
|
|
728
|
+
>
|
|
729
|
+
{children}
|
|
730
|
+
</DialogBody>
|
|
731
|
+
</View>
|
|
732
|
+
)}
|
|
602
733
|
</View>
|
|
603
734
|
</View>
|
|
604
735
|
);
|
|
@@ -12,6 +12,13 @@ import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
|
12
12
|
import { useTheme } from '../theme/use-theme';
|
|
13
13
|
import { Context } from './context';
|
|
14
14
|
import { DialogBody } from './DialogContent';
|
|
15
|
+
import {
|
|
16
|
+
DIALOG_NAV_BAR_HEIGHT,
|
|
17
|
+
DialogHeaderProvider,
|
|
18
|
+
DialogLargeTitle,
|
|
19
|
+
DialogNavHeader,
|
|
20
|
+
useDialogHeaderController,
|
|
21
|
+
} from './DialogHeader';
|
|
15
22
|
import {
|
|
16
23
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
17
24
|
DEFAULT_MAX_HEIGHT_RATIO,
|
|
@@ -43,15 +50,18 @@ import type { DialogControlProps, DialogProps } from './types';
|
|
|
43
50
|
export function DialogBottomSheet({
|
|
44
51
|
control,
|
|
45
52
|
open: controlledOpen,
|
|
53
|
+
startOpen,
|
|
46
54
|
onClose,
|
|
47
55
|
testID,
|
|
48
56
|
title,
|
|
49
57
|
description,
|
|
50
58
|
actions,
|
|
59
|
+
header,
|
|
51
60
|
maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
|
|
52
61
|
showHandle = true,
|
|
53
62
|
dismissOnBackdrop = true,
|
|
54
63
|
contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
|
|
64
|
+
scrollable,
|
|
55
65
|
style,
|
|
56
66
|
panelStyle,
|
|
57
67
|
panelClassName,
|
|
@@ -66,6 +76,7 @@ export function DialogBottomSheet({
|
|
|
66
76
|
const closeCallbacks = useRef<(() => void)[]>([]);
|
|
67
77
|
const titleId = useId();
|
|
68
78
|
const descriptionId = useId();
|
|
79
|
+
const headerController = useDialogHeaderController();
|
|
69
80
|
|
|
70
81
|
// Read the latest controlled flag / `onClose` inside stable callbacks without
|
|
71
82
|
// re-binding them (the context + imperative handle depend on `close` staying
|
|
@@ -161,41 +172,95 @@ export function DialogBottomSheet({
|
|
|
161
172
|
}, [theme.colors.background, maxHeightRatio, panelStyle]);
|
|
162
173
|
|
|
163
174
|
// Does this dialog render bloom's declarative chrome (title / description /
|
|
164
|
-
// actions)
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// viewports. This only changes what `DialogBottomSheet` passes to
|
|
171
|
-
// `BottomSheet`; the standalone `BottomSheet` API and its `scrollable` default
|
|
172
|
-
// are unchanged for direct consumers.
|
|
175
|
+
// actions)? This no longer gates the SCROLL decision (that is now the explicit
|
|
176
|
+
// `scrollable` prop below) — it only opts declarative dialogs into the
|
|
177
|
+
// gorhom-style pan coordination for their internal ScrollView. A declarative
|
|
178
|
+
// dialog overflowing on a small viewport needs the coordinated pan so the body
|
|
179
|
+
// scrolls instead of the drag stealing every vertical gesture; pure-custom
|
|
180
|
+
// children keep the legacy always-active pan.
|
|
173
181
|
const hasDeclarativeChrome =
|
|
174
182
|
title !== undefined ||
|
|
175
183
|
description !== undefined ||
|
|
176
184
|
(actions !== undefined && actions.length > 0);
|
|
177
185
|
|
|
186
|
+
const scrollableResolved = scrollable ?? true;
|
|
187
|
+
|
|
188
|
+
const dialogBody = (
|
|
189
|
+
<DialogBody
|
|
190
|
+
titleId={titleId}
|
|
191
|
+
descriptionId={descriptionId}
|
|
192
|
+
title={title}
|
|
193
|
+
description={description}
|
|
194
|
+
actions={actions}
|
|
195
|
+
>
|
|
196
|
+
{children}
|
|
197
|
+
</DialogBody>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
|
|
201
|
+
// collapsing title over the sheet's OWN scroll content. The bar floats via the
|
|
202
|
+
// BottomSheet `headerOverlay` slot; the sheet's scroll offset feeds the shared
|
|
203
|
+
// controller (`scrollY`) that drives the small-title cross-fade. `contentPadding`
|
|
204
|
+
// is dropped (the large title + screens own their padding).
|
|
205
|
+
const headerBody = header ? (
|
|
206
|
+
scrollableResolved ? (
|
|
207
|
+
<>
|
|
208
|
+
<DialogLargeTitle controller={headerController} header={header} />
|
|
209
|
+
<DialogHeaderProvider controller={headerController}>{dialogBody}</DialogHeaderProvider>
|
|
210
|
+
</>
|
|
211
|
+
) : (
|
|
212
|
+
<>
|
|
213
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
214
|
+
<DialogHeaderProvider controller={headerController}>{dialogBody}</DialogHeaderProvider>
|
|
215
|
+
</>
|
|
216
|
+
)
|
|
217
|
+
) : null;
|
|
218
|
+
|
|
178
219
|
return (
|
|
179
220
|
<BottomSheet
|
|
180
221
|
ref={ref}
|
|
222
|
+
// Seed the sheet's open state so a fresh mount that should be open renders
|
|
223
|
+
// visible on its FIRST commit instead of relying on a present()-in-effect
|
|
224
|
+
// that races the mount and flashes blank. In controlled mode that seed is
|
|
225
|
+
// `controlledOpen`; in imperative mode it is the surface's `startOpen`
|
|
226
|
+
// intent — which is what lets a responsive Dialog survive a resize
|
|
227
|
+
// placement swap (centered card → bottom sheet) STILL OPEN.
|
|
228
|
+
open={isControlled ? controlledOpen : startOpen}
|
|
181
229
|
onDismiss={handleDismiss}
|
|
182
230
|
onDismissAttempt={onDismissAttempt}
|
|
183
231
|
enablePanDownToClose
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
//
|
|
232
|
+
// Nav-header mode hides the drag handle (the nav bar sits at the very top);
|
|
233
|
+
// drag-to-dismiss still works via the coordinated body pan.
|
|
234
|
+
showHandle={header ? false : showHandle}
|
|
235
|
+
// Content scrolls inside the sheet's internal ScrollView by default; the
|
|
236
|
+
// Dialog owns the scroll boundary. A surface whose content owns its own
|
|
237
|
+
// scroller (a FlatList / VirtualizedList) passes `scrollable={false}` to
|
|
238
|
+
// opt OUT so there is exactly ONE scroll container (no scroll-in-scroll,
|
|
239
|
+
// no "VirtualizedList inside a plain ScrollView" warning).
|
|
240
|
+
scrollable={scrollableResolved}
|
|
241
|
+
// Declarative dialogs (title/description/actions) that overflow — AND every
|
|
242
|
+
// nav-header page (its scroll content overflows by design) — need
|
|
190
243
|
// gorhom-style pan coordination: the drag-to-dismiss pan only activates
|
|
191
244
|
// when the ScrollView is at the top AND the finger moves down >8dp —
|
|
192
245
|
// otherwise the ScrollView owns the touch. Without this the always-active
|
|
193
246
|
// legacy body-pan steals vertical drags and a tall body cannot scroll
|
|
194
247
|
// (dragging up dismisses the sheet / bleeds into the parent instead). The
|
|
195
248
|
// standalone `BottomSheet` API keeps its legacy `manualActivation={false}`
|
|
196
|
-
// default, so direct consumers are unaffected.
|
|
197
|
-
|
|
198
|
-
|
|
249
|
+
// default, so direct consumers are unaffected.
|
|
250
|
+
manualActivation={header ? scrollableResolved : hasDeclarativeChrome}
|
|
251
|
+
// Feed the sheet's scroll offset into the nav header's collapse controller.
|
|
252
|
+
scrollY={header ? headerController.scrollY : undefined}
|
|
253
|
+
// Float the sticky nav bar over the scroll body (clipped to the sheet top).
|
|
254
|
+
headerOverlay={
|
|
255
|
+
header ? (
|
|
256
|
+
<DialogNavHeader
|
|
257
|
+
controller={headerController}
|
|
258
|
+
header={header}
|
|
259
|
+
onDismiss={close}
|
|
260
|
+
collapse={scrollableResolved}
|
|
261
|
+
/>
|
|
262
|
+
) : undefined
|
|
263
|
+
}
|
|
199
264
|
// Stronger dim than a lone sheet so an underlying sheet's handle/content
|
|
200
265
|
// doesn't bleed through when a Dialog is stacked over one.
|
|
201
266
|
backdropOpacity={SHEET_BACKDROP_OPACITY + 0.3}
|
|
@@ -210,7 +275,9 @@ export function DialogBottomSheet({
|
|
|
210
275
|
{...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
|
|
211
276
|
{...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
|
|
212
277
|
style={[
|
|
213
|
-
|
|
278
|
+
// Nav-header mode: the large title + screens own their padding, so the
|
|
279
|
+
// wrapper adds none. Otherwise the Dialog's uniform content padding.
|
|
280
|
+
header ? null : { padding: contentPadding },
|
|
214
281
|
// `containerStyle` carries the host's CSS-var theme scope; it wraps
|
|
215
282
|
// the content subtree so descendants read the scoped palette.
|
|
216
283
|
containerStyle,
|
|
@@ -218,15 +285,7 @@ export function DialogBottomSheet({
|
|
|
218
285
|
panelStyle,
|
|
219
286
|
]}
|
|
220
287
|
>
|
|
221
|
-
|
|
222
|
-
titleId={titleId}
|
|
223
|
-
descriptionId={descriptionId}
|
|
224
|
-
title={title}
|
|
225
|
-
description={description}
|
|
226
|
-
actions={actions}
|
|
227
|
-
>
|
|
228
|
-
{children}
|
|
229
|
-
</DialogBody>
|
|
288
|
+
{header ? headerBody : dialogBody}
|
|
230
289
|
</View>
|
|
231
290
|
</Context.Provider>
|
|
232
291
|
</BottomSheet>
|
|
@@ -242,15 +301,18 @@ export type DialogBottomSheetProps = Pick<
|
|
|
242
301
|
DialogProps,
|
|
243
302
|
| 'control'
|
|
244
303
|
| 'open'
|
|
304
|
+
| 'startOpen'
|
|
245
305
|
| 'onClose'
|
|
246
306
|
| 'testID'
|
|
247
307
|
| 'title'
|
|
248
308
|
| 'description'
|
|
249
309
|
| 'actions'
|
|
310
|
+
| 'header'
|
|
250
311
|
| 'maxHeightRatio'
|
|
251
312
|
| 'showHandle'
|
|
252
313
|
| 'dismissOnBackdrop'
|
|
253
314
|
| 'contentPadding'
|
|
315
|
+
| 'scrollable'
|
|
254
316
|
| 'style'
|
|
255
317
|
| 'panelStyle'
|
|
256
318
|
| 'panelClassName'
|