@oxyhq/bloom 0.46.0 → 0.49.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 +36 -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 +103 -8
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +189 -12
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +111 -31
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +371 -0
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -0
- package/lib/commonjs/dialog/DialogMorph.js +458 -0
- package/lib/commonjs/dialog/DialogMorph.js.map +1 -0
- package/lib/commonjs/dialog/index.js +14 -0
- package/lib/commonjs/dialog/index.js.map +1 -1
- package/lib/commonjs/dialog/index.web.js +14 -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 +6 -2
- package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +23 -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 +36 -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 +104 -9
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +192 -16
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +113 -33
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +362 -0
- package/lib/module/dialog/DialogHeader.js.map +1 -0
- package/lib/module/dialog/DialogMorph.js +451 -0
- package/lib/module/dialog/DialogMorph.js.map +1 -0
- package/lib/module/dialog/index.js +2 -0
- package/lib/module/dialog/index.js.map +1 -1
- package/lib/module/dialog/index.web.js +2 -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 +6 -2
- package/lib/module/settings-list/SettingsList.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +23 -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 +31 -1
- 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/DialogMorph.d.ts +150 -0
- package/lib/typescript/commonjs/dialog/DialogMorph.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/index.d.ts +4 -1
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +4 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +105 -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 +26 -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 +31 -1
- 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/DialogMorph.d.ts +150 -0
- package/lib/typescript/module/dialog/DialogMorph.d.ts.map +1 -0
- package/lib/typescript/module/dialog/index.d.ts +4 -1
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +4 -1
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +105 -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 +26 -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/__tests__/DialogMorph.test.tsx +230 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +72 -11
- package/src/combobox/Combobox.tsx +3 -3
- package/src/command/Command.tsx +3 -3
- package/src/dialog/Dialog.tsx +111 -14
- package/src/dialog/Dialog.web.tsx +186 -25
- package/src/dialog/DialogBottomSheet.tsx +122 -30
- package/src/dialog/DialogHeader.tsx +429 -0
- package/src/dialog/DialogMorph.tsx +576 -0
- package/src/dialog/index.ts +4 -0
- package/src/dialog/index.web.ts +4 -0
- package/src/dialog/types.ts +106 -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 +6 -2
- package/src/surfaces/SurfaceHost.tsx +17 -1
- package/src/surfaces/types.ts +26 -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
|
@@ -12,7 +12,7 @@ import { useTheme } from '../theme/use-theme';
|
|
|
12
12
|
import { useControllableState } from '../hooks/useControllableState';
|
|
13
13
|
import { Text } from '../typography';
|
|
14
14
|
import { Item } from '../item';
|
|
15
|
-
import {
|
|
15
|
+
import { Search } from '../search';
|
|
16
16
|
import { borderRadius, fontSize, space } from '../styles/tokens';
|
|
17
17
|
import {
|
|
18
18
|
ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon,
|
|
@@ -55,7 +55,7 @@ function defaultFilter<T>(option: ComboboxOption<T>, query: string): boolean {
|
|
|
55
55
|
* `.web` resolution. The combobox logic is single-source.
|
|
56
56
|
*
|
|
57
57
|
* `Combobox` is a filterable autocomplete select that delegates its overlay to
|
|
58
|
-
* Bloom's `Popover` and composes `
|
|
58
|
+
* Bloom's `Popover` and composes `Search` + `Item`. The trigger + panel
|
|
59
59
|
* read the overlay control from `Popover` context (via an inner component) so
|
|
60
60
|
* selecting an option reliably closes the overlay on every platform.
|
|
61
61
|
*/
|
|
@@ -153,7 +153,7 @@ export function createCombobox(PopoverImpl: PopoverModule) {
|
|
|
153
153
|
|
|
154
154
|
<PopoverImpl.PopoverContent label={accessibleLabel} placement="bottom-start" minWidth={240}>
|
|
155
155
|
<View style={styles.searchWrap}>
|
|
156
|
-
<
|
|
156
|
+
<Search
|
|
157
157
|
ref={searchRef}
|
|
158
158
|
label={label ?? 'Search'}
|
|
159
159
|
value={query}
|
package/src/command/Command.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { useControllableState } from '../hooks/useControllableState';
|
|
|
12
12
|
import { Text } from '../typography';
|
|
13
13
|
import { Item } from '../item';
|
|
14
14
|
import { Kbd } from '../kbd';
|
|
15
|
-
import {
|
|
15
|
+
import { Search } from '../search';
|
|
16
16
|
import { fontSize, space } from '../styles/tokens';
|
|
17
17
|
import type { DialogProps } from '../dialog';
|
|
18
18
|
import { useDialogControl } from '../dialog/context';
|
|
@@ -46,7 +46,7 @@ interface FlatEntry {
|
|
|
46
46
|
* relies on implicit `.web` resolution. The body is single-source.
|
|
47
47
|
*
|
|
48
48
|
* `Command` is a ⌘K command palette built on `<Dialog placement="center">`,
|
|
49
|
-
* `
|
|
49
|
+
* `Search`, an `Item` results list and `Kbd` shortcut hints. Items group
|
|
50
50
|
* by their `group` field (ungrouped first). On web the list is
|
|
51
51
|
* keyboard-navigable (Up/Down/Enter); on native, tap to select.
|
|
52
52
|
*
|
|
@@ -231,7 +231,7 @@ export function createCommand(Dialog: DialogComponent) {
|
|
|
231
231
|
testID={testID}>
|
|
232
232
|
<View {...webKeyHandler}>
|
|
233
233
|
<View style={[styles.searchWrap, { borderBottomColor: theme.colors.borderLight }]}>
|
|
234
|
-
<
|
|
234
|
+
<Search
|
|
235
235
|
ref={searchRef}
|
|
236
236
|
label={placeholder}
|
|
237
237
|
placeholder={placeholder}
|
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -30,6 +30,15 @@ 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';
|
|
41
|
+
import { DialogMorphContent, useDialogMorph } from './DialogMorph';
|
|
33
42
|
import {
|
|
34
43
|
ANIMATION_DURATION,
|
|
35
44
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
@@ -103,6 +112,9 @@ function CenteredOrSideDialog({
|
|
|
103
112
|
title,
|
|
104
113
|
description,
|
|
105
114
|
actions,
|
|
115
|
+
header,
|
|
116
|
+
scrollable,
|
|
117
|
+
morph,
|
|
106
118
|
placement,
|
|
107
119
|
layer,
|
|
108
120
|
width = DEFAULT_SIDE_WIDTH,
|
|
@@ -119,6 +131,21 @@ function CenteredOrSideDialog({
|
|
|
119
131
|
}: Omit<DialogProps, 'placement'> & { placement: 'center' | 'left' | 'right' }) {
|
|
120
132
|
const isControlled = controlledOpen !== undefined;
|
|
121
133
|
const isSide = placement === 'left' || placement === 'right';
|
|
134
|
+
const headerController = useDialogHeaderController();
|
|
135
|
+
const scrollableResolved = scrollable ?? true;
|
|
136
|
+
const { height: viewportHeight } = useWindowDimensions();
|
|
137
|
+
|
|
138
|
+
// Size morphing across an in-place content swap. The detached centered card
|
|
139
|
+
// is content-hugging, so animating its height reshapes it about its own
|
|
140
|
+
// centre; the side placements are anchored top-to-bottom at a fixed width and
|
|
141
|
+
// have no size to morph, so they never pin (`measurable: false`). The card's
|
|
142
|
+
// own `maxHeight` (inside `BottomSheet`) does the exact capping — the viewport
|
|
143
|
+
// is just the bound the reshape may not exceed.
|
|
144
|
+
const morphState = useDialogMorph({
|
|
145
|
+
enabled: morph !== false && !isSide,
|
|
146
|
+
measurable: scrollableResolved && !isSide,
|
|
147
|
+
maxHeight: viewportHeight,
|
|
148
|
+
});
|
|
122
149
|
|
|
123
150
|
// Imperative open state for the side placement (the BottomSheet path owns its
|
|
124
151
|
// own visibility via its ref instead).
|
|
@@ -211,16 +238,21 @@ function CenteredOrSideDialog({
|
|
|
211
238
|
[close],
|
|
212
239
|
);
|
|
213
240
|
|
|
214
|
-
const sheetStyle = useMemo(
|
|
215
|
-
() =>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
241
|
+
const sheetStyle = useMemo<StyleProp<ViewStyle>>(
|
|
242
|
+
() => [
|
|
243
|
+
{
|
|
244
|
+
maxWidth: 500,
|
|
245
|
+
backgroundColor: theme.colors.background,
|
|
246
|
+
// All four corners rounded — bloom's BottomSheet defaults to top-only
|
|
247
|
+
// radius in flush mode, but we use `detached` so the whole card is
|
|
248
|
+
// floating and rounded uniformly.
|
|
249
|
+
borderRadius: 20,
|
|
250
|
+
},
|
|
251
|
+
// Drives the card's `height` only while a morph is in flight; at rest it
|
|
252
|
+
// resolves to `height: 'auto'` — the card's normal content sizing.
|
|
253
|
+
morphState.panelStyle,
|
|
254
|
+
],
|
|
255
|
+
[theme.colors.background, morphState.panelStyle],
|
|
224
256
|
);
|
|
225
257
|
|
|
226
258
|
const bodyNode = (
|
|
@@ -235,6 +267,24 @@ function CenteredOrSideDialog({
|
|
|
235
267
|
</DialogBody>
|
|
236
268
|
);
|
|
237
269
|
|
|
270
|
+
// Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
|
|
271
|
+
// collapsing title over its own scroll content. The large title + screen body
|
|
272
|
+
// scroll under the bar, feeding the shared collapse controller. `contentPadding`
|
|
273
|
+
// is dropped (the large title + screens own their padding).
|
|
274
|
+
const headerBody = header ? (
|
|
275
|
+
scrollableResolved ? (
|
|
276
|
+
<>
|
|
277
|
+
<DialogLargeTitle controller={headerController} header={header} />
|
|
278
|
+
<DialogHeaderProvider controller={headerController}>{bodyNode}</DialogHeaderProvider>
|
|
279
|
+
</>
|
|
280
|
+
) : (
|
|
281
|
+
<>
|
|
282
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
283
|
+
<DialogHeaderProvider controller={headerController}>{bodyNode}</DialogHeaderProvider>
|
|
284
|
+
</>
|
|
285
|
+
)
|
|
286
|
+
) : null;
|
|
287
|
+
|
|
238
288
|
if (isSide) {
|
|
239
289
|
return (
|
|
240
290
|
<Context.Provider value={context}>
|
|
@@ -253,6 +303,9 @@ function CenteredOrSideDialog({
|
|
|
253
303
|
description={description}
|
|
254
304
|
titleId={titleId}
|
|
255
305
|
descriptionId={descriptionId}
|
|
306
|
+
header={header}
|
|
307
|
+
headerController={headerController}
|
|
308
|
+
onHeaderDismiss={close}
|
|
256
309
|
panelStyle={panelStyle}
|
|
257
310
|
panelClassName={panelClassName}
|
|
258
311
|
containerStyle={containerStyle}
|
|
@@ -271,10 +324,26 @@ function CenteredOrSideDialog({
|
|
|
271
324
|
onDismiss={handleDismiss}
|
|
272
325
|
enablePanDownToClose
|
|
273
326
|
detached
|
|
274
|
-
|
|
327
|
+
// Nav-header mode hides the drag handle (the nav bar sits at the very top)
|
|
328
|
+
// and coordinates the body pan with the scroll so content scrolls.
|
|
329
|
+
showHandle={!header}
|
|
330
|
+
manualActivation={!!header}
|
|
331
|
+
scrollY={header ? headerController.scrollY : undefined}
|
|
332
|
+
headerOverlay={
|
|
333
|
+
header ? (
|
|
334
|
+
<DialogNavHeader
|
|
335
|
+
controller={headerController}
|
|
336
|
+
header={header}
|
|
337
|
+
onDismiss={close}
|
|
338
|
+
collapse={scrollableResolved}
|
|
339
|
+
/>
|
|
340
|
+
) : undefined
|
|
341
|
+
}
|
|
275
342
|
// Stronger dim when a Dialog is stacked over another sheet so the
|
|
276
343
|
// underlying sheet's handle/content doesn't bleed through.
|
|
277
344
|
backdropOpacity={0.7}
|
|
345
|
+
// Measures the floating card — the size a morph starts from.
|
|
346
|
+
onLayout={morphState.onPanelLayout}
|
|
278
347
|
style={sheetStyle}
|
|
279
348
|
>
|
|
280
349
|
<Context.Provider value={context}>
|
|
@@ -288,13 +357,19 @@ function CenteredOrSideDialog({
|
|
|
288
357
|
// Detached BottomSheet already adds `marginBottom: insets.bottom + 16`
|
|
289
358
|
// to the sheet container — the floating card sits ABOVE the
|
|
290
359
|
// system gesture bar, so we don't add `insets.bottom` here.
|
|
291
|
-
|
|
360
|
+
// Nav-header mode: the large title + screens own their padding.
|
|
361
|
+
header ? null : { padding: contentPadding },
|
|
292
362
|
{ backgroundColor: theme.colors.background },
|
|
293
363
|
style,
|
|
294
364
|
panelStyle,
|
|
295
365
|
]}
|
|
296
366
|
>
|
|
297
|
-
{
|
|
367
|
+
{/* The morph layer is its own node INSIDE the class-name'd container:
|
|
368
|
+
a className'd component's `onLayout` never fires on web, and this
|
|
369
|
+
one has to measure the incoming frame. */}
|
|
370
|
+
<DialogMorphContent morph={morphState}>
|
|
371
|
+
{header ? headerBody : bodyNode}
|
|
372
|
+
</DialogMorphContent>
|
|
298
373
|
</View>
|
|
299
374
|
</Context.Provider>
|
|
300
375
|
</BottomSheet>
|
|
@@ -324,6 +399,9 @@ function SideSheet({
|
|
|
324
399
|
description,
|
|
325
400
|
titleId,
|
|
326
401
|
descriptionId,
|
|
402
|
+
header,
|
|
403
|
+
headerController,
|
|
404
|
+
onHeaderDismiss,
|
|
327
405
|
panelStyle,
|
|
328
406
|
panelClassName,
|
|
329
407
|
containerStyle,
|
|
@@ -345,6 +423,9 @@ function SideSheet({
|
|
|
345
423
|
description?: string;
|
|
346
424
|
titleId: string;
|
|
347
425
|
descriptionId: string;
|
|
426
|
+
header?: DialogProps['header'];
|
|
427
|
+
headerController: DialogHeaderController;
|
|
428
|
+
onHeaderDismiss: () => void;
|
|
348
429
|
panelStyle?: StyleProp<ViewStyle>;
|
|
349
430
|
panelClassName?: string;
|
|
350
431
|
containerStyle?: StyleProp<ViewStyle>;
|
|
@@ -476,7 +557,23 @@ function SideSheet({
|
|
|
476
557
|
]}
|
|
477
558
|
pointerEvents="auto"
|
|
478
559
|
>
|
|
479
|
-
|
|
560
|
+
{header ? (
|
|
561
|
+
// Nav-header mode on a side drawer: a static titled bar (the drawer body
|
|
562
|
+
// has no Dialog scroll offset to drive a collapse) over content inset
|
|
563
|
+
// below the bar.
|
|
564
|
+
<View style={{ flex: 1, minHeight: 0 }}>
|
|
565
|
+
<DialogNavHeader
|
|
566
|
+
controller={headerController}
|
|
567
|
+
header={header}
|
|
568
|
+
onDismiss={onHeaderDismiss}
|
|
569
|
+
collapse={false}
|
|
570
|
+
/>
|
|
571
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
572
|
+
<DialogHeaderProvider controller={headerController}>{children}</DialogHeaderProvider>
|
|
573
|
+
</View>
|
|
574
|
+
) : (
|
|
575
|
+
<View style={{ padding: contentPadding }}>{children}</View>
|
|
576
|
+
)}
|
|
480
577
|
</Animated.View>
|
|
481
578
|
</View>
|
|
482
579
|
);
|
|
@@ -10,24 +10,35 @@ import React, {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
12
|
Pressable,
|
|
13
|
+
ScrollView,
|
|
13
14
|
useWindowDimensions,
|
|
14
15
|
View,
|
|
15
16
|
type StyleProp,
|
|
16
17
|
type ViewStyle,
|
|
17
18
|
} from 'react-native';
|
|
19
|
+
import Animated from 'react-native-reanimated';
|
|
18
20
|
import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
19
21
|
|
|
20
22
|
import { Portal } from '../portal/index.web';
|
|
21
23
|
import { createOverlayZIndex } from '../styles/z-index';
|
|
22
24
|
import { useTheme } from '../theme/use-theme';
|
|
23
|
-
import { Context, useDialogControl } from './context';
|
|
25
|
+
import { Context, useDialogContext, useDialogControl } from './context';
|
|
24
26
|
import { DialogBody } from './DialogContent';
|
|
25
27
|
import { DialogBottomSheet } from './DialogBottomSheet';
|
|
28
|
+
import {
|
|
29
|
+
DIALOG_NAV_BAR_HEIGHT,
|
|
30
|
+
DialogHeaderProvider,
|
|
31
|
+
DialogLargeTitle,
|
|
32
|
+
DialogNavHeader,
|
|
33
|
+
useDialogHeaderController,
|
|
34
|
+
} from './DialogHeader';
|
|
35
|
+
import { DialogMorphContent, useDialogMorph } from './DialogMorph';
|
|
26
36
|
import {
|
|
27
37
|
ANIMATION_DURATION,
|
|
28
38
|
CENTER_FADE_OUT_DURATION,
|
|
29
39
|
DEFAULT_CENTER_MAX_WIDTH,
|
|
30
40
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
41
|
+
DEFAULT_MAX_HEIGHT_RATIO,
|
|
31
42
|
DEFAULT_SIDE_WIDTH,
|
|
32
43
|
DIALOG_SHEET_BACKDROP_TESTID,
|
|
33
44
|
EASE_OUT,
|
|
@@ -125,11 +136,13 @@ export function Dialog({ placement, ...rest }: DialogProps) {
|
|
|
125
136
|
function CenterOrSideDialog({
|
|
126
137
|
control,
|
|
127
138
|
open: controlledOpen,
|
|
139
|
+
startOpen,
|
|
128
140
|
onClose,
|
|
129
141
|
testID,
|
|
130
142
|
title,
|
|
131
143
|
description,
|
|
132
144
|
actions,
|
|
145
|
+
header,
|
|
133
146
|
placement,
|
|
134
147
|
layer,
|
|
135
148
|
width = DEFAULT_SIDE_WIDTH,
|
|
@@ -137,6 +150,9 @@ function CenterOrSideDialog({
|
|
|
137
150
|
inset,
|
|
138
151
|
dismissOnBackdrop = true,
|
|
139
152
|
contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
|
|
153
|
+
maxHeightRatio,
|
|
154
|
+
scrollable,
|
|
155
|
+
morph,
|
|
140
156
|
style,
|
|
141
157
|
panelStyle,
|
|
142
158
|
panelClassName,
|
|
@@ -160,7 +176,13 @@ function CenterOrSideDialog({
|
|
|
160
176
|
const isControlled = controlledOpen !== undefined;
|
|
161
177
|
const resolvedPlacement = placement;
|
|
162
178
|
|
|
163
|
-
|
|
179
|
+
// Seed the initial visible state so a branch that mounts while the surface is
|
|
180
|
+
// already open (the imperative `startOpen` intent) or under a truthy
|
|
181
|
+
// controlled `open` renders open on its FIRST commit — this is what lets a
|
|
182
|
+
// responsive Dialog survive a placement swap (centered card ↔ bottom sheet on
|
|
183
|
+
// resize) without going blank. `startOpen` never flips the close semantics;
|
|
184
|
+
// the imperative `control.close()` still drives the exit + post-exit onClose.
|
|
185
|
+
const [isOpen, setIsOpen] = useState(() => controlledOpen ?? startOpen ?? false);
|
|
164
186
|
const [isClosing, setIsClosing] = useState(false);
|
|
165
187
|
const closeCallbacksRef = useRef<(() => void)[]>([]);
|
|
166
188
|
// Read the latest controlled flag inside stable callbacks without re-binding.
|
|
@@ -299,9 +321,13 @@ function CenterOrSideDialog({
|
|
|
299
321
|
title={title}
|
|
300
322
|
description={description}
|
|
301
323
|
actions={actions}
|
|
324
|
+
header={header}
|
|
302
325
|
style={style}
|
|
303
326
|
maxWidth={maxWidth}
|
|
304
327
|
contentPadding={contentPadding}
|
|
328
|
+
maxHeightRatio={maxHeightRatio}
|
|
329
|
+
scrollable={scrollable}
|
|
330
|
+
morph={morph}
|
|
305
331
|
surfaceZIndex={dialogZIndex.surface}
|
|
306
332
|
isClosing={isClosing}
|
|
307
333
|
>
|
|
@@ -325,6 +351,8 @@ function CenterOrSideDialog({
|
|
|
325
351
|
title={title}
|
|
326
352
|
description={description}
|
|
327
353
|
actions={actions}
|
|
354
|
+
header={header}
|
|
355
|
+
scrollable={scrollable}
|
|
328
356
|
placement={resolvedPlacement}
|
|
329
357
|
shown={!isClosing}
|
|
330
358
|
width={width}
|
|
@@ -354,9 +382,13 @@ function DialogPanel({
|
|
|
354
382
|
title,
|
|
355
383
|
description,
|
|
356
384
|
actions,
|
|
385
|
+
header,
|
|
357
386
|
style,
|
|
358
387
|
maxWidth,
|
|
359
388
|
contentPadding,
|
|
389
|
+
maxHeightRatio,
|
|
390
|
+
scrollable,
|
|
391
|
+
morph,
|
|
360
392
|
surfaceZIndex,
|
|
361
393
|
isClosing,
|
|
362
394
|
children,
|
|
@@ -366,24 +398,56 @@ function DialogPanel({
|
|
|
366
398
|
title?: string;
|
|
367
399
|
description?: string;
|
|
368
400
|
actions?: DialogAction[];
|
|
401
|
+
header?: DialogProps['header'];
|
|
369
402
|
style?: DialogProps['style'];
|
|
370
403
|
maxWidth: number;
|
|
371
404
|
contentPadding: number;
|
|
405
|
+
maxHeightRatio?: number;
|
|
406
|
+
scrollable?: boolean;
|
|
407
|
+
morph?: boolean;
|
|
372
408
|
surfaceZIndex: number;
|
|
373
409
|
isClosing: boolean;
|
|
374
410
|
children?: React.ReactNode;
|
|
375
411
|
}) {
|
|
376
412
|
const theme = useTheme();
|
|
413
|
+
const { close } = useDialogContext();
|
|
377
414
|
const titleId = useId();
|
|
378
415
|
const descriptionId = useId();
|
|
416
|
+
const headerController = useDialogHeaderController();
|
|
417
|
+
const { height: viewportHeight } = useWindowDimensions();
|
|
418
|
+
const heightRatio = maxHeightRatio ?? DEFAULT_MAX_HEIGHT_RATIO;
|
|
419
|
+
|
|
420
|
+
// Size morphing across an in-place content swap. The centered card is the one
|
|
421
|
+
// placement whose width can vary too, so `maxWidth` is handed over as well.
|
|
422
|
+
// The card's own `maxHeight` (the ratio percentage below) does the exact
|
|
423
|
+
// capping — the viewport is just the bound the reshape may not exceed.
|
|
424
|
+
const morphState = useDialogMorph({
|
|
425
|
+
enabled: morph !== false,
|
|
426
|
+
measurable: scrollable !== false,
|
|
427
|
+
maxHeight: viewportHeight,
|
|
428
|
+
maxWidth,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const body = (
|
|
432
|
+
<DialogBody
|
|
433
|
+
titleId={titleId}
|
|
434
|
+
descriptionId={descriptionId}
|
|
435
|
+
title={title}
|
|
436
|
+
description={description}
|
|
437
|
+
actions={actions}
|
|
438
|
+
>
|
|
439
|
+
{children}
|
|
440
|
+
</DialogBody>
|
|
441
|
+
);
|
|
379
442
|
|
|
380
443
|
return (
|
|
381
|
-
<View
|
|
444
|
+
<Animated.View
|
|
382
445
|
role="dialog"
|
|
383
446
|
aria-label={label}
|
|
384
447
|
aria-labelledby={title ? titleId : undefined}
|
|
385
448
|
aria-describedby={description ? descriptionId : undefined}
|
|
386
449
|
testID={testID}
|
|
450
|
+
onLayout={morphState.onPanelLayout}
|
|
387
451
|
onStartShouldSetResponder={() => true}
|
|
388
452
|
onResponderRelease={stopPropagation}
|
|
389
453
|
{...({ onClick: stopPropagation } as Record<string, unknown>)}
|
|
@@ -393,6 +457,13 @@ function DialogPanel({
|
|
|
393
457
|
borderRadius: 20,
|
|
394
458
|
width: '100%',
|
|
395
459
|
maxWidth,
|
|
460
|
+
// The Dialog OWNS the size cap + scroll boundary (its content renders
|
|
461
|
+
// bare, edge-to-edge). Short content stays natural; tall content is
|
|
462
|
+
// capped at `maxHeightRatio` of the viewport and scrolls inside the
|
|
463
|
+
// rounded card (via the ScrollView below) — consumers never add their
|
|
464
|
+
// own height cap / ScrollView. `overflow: hidden` clips to the radius.
|
|
465
|
+
maxHeight: `${Math.round(heightRatio * 100)}%`,
|
|
466
|
+
overflow: 'hidden',
|
|
396
467
|
backgroundColor: theme.colors.background,
|
|
397
468
|
borderWidth: 1,
|
|
398
469
|
borderColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
|
@@ -400,25 +471,74 @@ function DialogPanel({
|
|
|
400
471
|
shadowOpacity: theme.isDark ? 0.4 : 0.1,
|
|
401
472
|
shadowRadius: 30,
|
|
402
473
|
shadowOffset: { width: 0, height: 4 },
|
|
403
|
-
padding: contentPadding,
|
|
404
474
|
zIndex: surfaceZIndex,
|
|
405
475
|
},
|
|
406
476
|
isClosing
|
|
407
477
|
? ({ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
|
|
408
478
|
: ({ animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s' } as ViewStyle),
|
|
479
|
+
// Drives `height` (and `maxWidth`) only while a morph is in flight; at
|
|
480
|
+
// rest it resolves to `height: 'auto'` — the natural sizing above. Placed
|
|
481
|
+
// before `style` so a consumer's explicit size still wins.
|
|
482
|
+
morphState.panelStyle,
|
|
409
483
|
style,
|
|
410
484
|
]}
|
|
411
485
|
>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
title
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
486
|
+
{header ? (
|
|
487
|
+
// Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
|
|
488
|
+
// collapsing title over its own scroll content (see `DialogHeader`). The
|
|
489
|
+
// bar overlays; the large title + screen body scroll under it, feeding
|
|
490
|
+
// the shared scroll offset that drives the small-title cross-fade.
|
|
491
|
+
<>
|
|
492
|
+
<DialogNavHeader
|
|
493
|
+
controller={headerController}
|
|
494
|
+
header={header}
|
|
495
|
+
onDismiss={close}
|
|
496
|
+
collapse={scrollable !== false}
|
|
497
|
+
/>
|
|
498
|
+
{scrollable === false ? (
|
|
499
|
+
<DialogMorphContent morph={morphState} style={{ flex: 1, minHeight: 0 }}>
|
|
500
|
+
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
501
|
+
<DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
|
|
502
|
+
</DialogMorphContent>
|
|
503
|
+
) : (
|
|
504
|
+
<ScrollView
|
|
505
|
+
style={{ flexShrink: 1 }}
|
|
506
|
+
contentContainerStyle={{ paddingBottom: contentPadding }}
|
|
507
|
+
showsVerticalScrollIndicator={false}
|
|
508
|
+
scrollEventThrottle={16}
|
|
509
|
+
onScroll={(e) => {
|
|
510
|
+
headerController.scrollY.value = e.nativeEvent.contentOffset.y;
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
513
|
+
<DialogMorphContent morph={morphState}>
|
|
514
|
+
<DialogLargeTitle controller={headerController} header={header} />
|
|
515
|
+
<DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
|
|
516
|
+
</DialogMorphContent>
|
|
517
|
+
</ScrollView>
|
|
518
|
+
)}
|
|
519
|
+
</>
|
|
520
|
+
) : scrollable === false ? (
|
|
521
|
+
// The content owns its own scrolling (a FlatList / VirtualizedList or a
|
|
522
|
+
// nested ScrollView). Render it bare in a bounded flex box — the card's
|
|
523
|
+
// `maxHeight` + `overflow: hidden` cap it and `flex: 1` + `minHeight: 0`
|
|
524
|
+
// hand the child a bounded height to scroll into — with NO wrapping
|
|
525
|
+
// ScrollView, so a VirtualizedList never nests inside one.
|
|
526
|
+
<DialogMorphContent
|
|
527
|
+
morph={morphState}
|
|
528
|
+
style={{ flex: 1, minHeight: 0, padding: contentPadding }}
|
|
529
|
+
>
|
|
530
|
+
{body}
|
|
531
|
+
</DialogMorphContent>
|
|
532
|
+
) : (
|
|
533
|
+
<ScrollView
|
|
534
|
+
style={{ flexShrink: 1 }}
|
|
535
|
+
contentContainerStyle={{ padding: contentPadding }}
|
|
536
|
+
showsVerticalScrollIndicator={false}
|
|
537
|
+
>
|
|
538
|
+
<DialogMorphContent morph={morphState}>{body}</DialogMorphContent>
|
|
539
|
+
</ScrollView>
|
|
540
|
+
)}
|
|
541
|
+
</Animated.View>
|
|
422
542
|
);
|
|
423
543
|
}
|
|
424
544
|
|
|
@@ -439,6 +559,8 @@ function SheetSurface({
|
|
|
439
559
|
title,
|
|
440
560
|
description,
|
|
441
561
|
actions,
|
|
562
|
+
header,
|
|
563
|
+
scrollable,
|
|
442
564
|
placement,
|
|
443
565
|
shown,
|
|
444
566
|
width,
|
|
@@ -460,6 +582,8 @@ function SheetSurface({
|
|
|
460
582
|
title?: string;
|
|
461
583
|
description?: string;
|
|
462
584
|
actions?: DialogAction[];
|
|
585
|
+
header?: DialogProps['header'];
|
|
586
|
+
scrollable?: boolean;
|
|
463
587
|
placement: 'left' | 'right';
|
|
464
588
|
shown: boolean;
|
|
465
589
|
width: number;
|
|
@@ -479,6 +603,7 @@ function SheetSurface({
|
|
|
479
603
|
const theme = useTheme();
|
|
480
604
|
const titleId = useId();
|
|
481
605
|
const descriptionId = useId();
|
|
606
|
+
const headerController = useDialogHeaderController();
|
|
482
607
|
const { width: viewportWidth } = useWindowDimensions();
|
|
483
608
|
|
|
484
609
|
// Defer the entry transition by a frame so the start state paints before the
|
|
@@ -588,17 +713,53 @@ function SheetSurface({
|
|
|
588
713
|
]}
|
|
589
714
|
pointerEvents="auto"
|
|
590
715
|
>
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
716
|
+
{header ? (
|
|
717
|
+
// Nav-header mode on a side drawer: a static titled bar (the drawer
|
|
718
|
+
// body does not own a Dialog scroll offset to drive a collapse) over
|
|
719
|
+
// the content, which is inset below the bar.
|
|
720
|
+
<View style={{ flex: 1, minHeight: 0 }}>
|
|
721
|
+
<DialogNavHeader
|
|
722
|
+
controller={headerController}
|
|
723
|
+
header={header}
|
|
724
|
+
onDismiss={onDismiss}
|
|
725
|
+
collapse={false}
|
|
726
|
+
/>
|
|
727
|
+
<ScrollView
|
|
728
|
+
style={{ flexShrink: 1 }}
|
|
729
|
+
contentContainerStyle={{ paddingTop: DIALOG_NAV_BAR_HEIGHT, paddingBottom: contentPadding }}
|
|
730
|
+
showsVerticalScrollIndicator={false}
|
|
731
|
+
scrollEventThrottle={16}
|
|
732
|
+
scrollEnabled={scrollable !== false}
|
|
733
|
+
onScroll={(e) => {
|
|
734
|
+
headerController.scrollY.value = e.nativeEvent.contentOffset.y;
|
|
735
|
+
}}
|
|
736
|
+
>
|
|
737
|
+
<DialogHeaderProvider controller={headerController}>
|
|
738
|
+
<DialogBody
|
|
739
|
+
titleId={titleId}
|
|
740
|
+
descriptionId={descriptionId}
|
|
741
|
+
title={title}
|
|
742
|
+
description={description}
|
|
743
|
+
actions={actions}
|
|
744
|
+
>
|
|
745
|
+
{children}
|
|
746
|
+
</DialogBody>
|
|
747
|
+
</DialogHeaderProvider>
|
|
748
|
+
</ScrollView>
|
|
749
|
+
</View>
|
|
750
|
+
) : (
|
|
751
|
+
<View style={{ padding: contentPadding }}>
|
|
752
|
+
<DialogBody
|
|
753
|
+
titleId={titleId}
|
|
754
|
+
descriptionId={descriptionId}
|
|
755
|
+
title={title}
|
|
756
|
+
description={description}
|
|
757
|
+
actions={actions}
|
|
758
|
+
>
|
|
759
|
+
{children}
|
|
760
|
+
</DialogBody>
|
|
761
|
+
</View>
|
|
762
|
+
)}
|
|
602
763
|
</View>
|
|
603
764
|
</View>
|
|
604
765
|
);
|