@oxyhq/bloom 0.14.0 → 0.15.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/dialog/CenteredDialog.js +2 -0
- package/lib/commonjs/dialog/CenteredDialog.js.map +1 -1
- package/lib/commonjs/dialog/CenteredDialog.web.js +2 -0
- package/lib/commonjs/dialog/CenteredDialog.web.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +337 -43
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +414 -63
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/index.js +6 -0
- package/lib/commonjs/dialog/index.js.map +1 -1
- package/lib/commonjs/dialog/index.web.js +6 -0
- package/lib/commonjs/dialog/index.web.js.map +1 -1
- package/lib/commonjs/dialog/placement.js +104 -0
- package/lib/commonjs/dialog/placement.js.map +1 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +63 -266
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +1 -1
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.web.js +84 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.web.js.map +1 -0
- package/lib/commonjs/responsive-sheet/index.web.js +3 -3
- package/lib/commonjs/responsive-sheet/index.web.js.map +1 -1
- package/lib/module/dialog/CenteredDialog.js +2 -0
- package/lib/module/dialog/CenteredDialog.js.map +1 -1
- package/lib/module/dialog/CenteredDialog.web.js +2 -0
- package/lib/module/dialog/CenteredDialog.web.js.map +1 -1
- package/lib/module/dialog/Dialog.js +336 -46
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +412 -65
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/index.js +1 -1
- package/lib/module/dialog/index.js.map +1 -1
- package/lib/module/dialog/index.web.js +1 -1
- package/lib/module/dialog/index.web.js.map +1 -1
- package/lib/module/dialog/placement.js +100 -0
- package/lib/module/dialog/placement.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/responsive-sheet/ResponsiveSheet.js +63 -266
- package/lib/module/responsive-sheet/ResponsiveSheet.js.map +1 -1
- package/lib/module/responsive-sheet/ResponsiveSheet.web.js +77 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.web.js.map +1 -0
- package/lib/module/responsive-sheet/index.web.js +7 -9
- package/lib/module/responsive-sheet/index.web.js.map +1 -1
- package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts +2 -0
- package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts +2 -0
- package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts +18 -15
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +18 -5
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts +4 -0
- package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.d.ts +2 -2
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/placement.d.ts +71 -0
- package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/types.d.ts +66 -3
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +15 -19
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.web.d.ts +16 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/responsive-sheet/types.d.ts +5 -2
- package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/CenteredDialog.d.ts +2 -0
- package/lib/typescript/module/dialog/CenteredDialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/CenteredDialog.web.d.ts +2 -0
- package/lib/typescript/module/dialog/CenteredDialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts +18 -15
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts +18 -5
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/centered-dialog-types.d.ts +4 -0
- package/lib/typescript/module/dialog/centered-dialog-types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.d.ts +2 -2
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +2 -2
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/placement.d.ts +71 -0
- package/lib/typescript/module/dialog/placement.d.ts.map +1 -0
- package/lib/typescript/module/dialog/types.d.ts +66 -3
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +15 -19
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +1 -1
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.web.d.ts +16 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.web.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/index.web.d.ts +1 -1
- package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/module/responsive-sheet/types.d.ts +5 -2
- package/lib/typescript/module/responsive-sheet/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/DialogPlacement.test.tsx +144 -0
- package/src/__tests__/ResponsiveSheet.test.tsx +36 -52
- package/src/dialog/CenteredDialog.tsx +2 -0
- package/src/dialog/CenteredDialog.web.tsx +2 -0
- package/src/dialog/Dialog.tsx +407 -50
- package/src/dialog/Dialog.web.tsx +490 -75
- package/src/dialog/centered-dialog-types.ts +4 -0
- package/src/dialog/index.ts +4 -1
- package/src/dialog/index.web.ts +4 -1
- package/src/dialog/placement.ts +113 -0
- package/src/dialog/types.ts +69 -3
- package/src/index.ts +4 -0
- package/src/index.web.ts +4 -0
- package/src/responsive-sheet/ResponsiveSheet.tsx +58 -297
- package/src/responsive-sheet/ResponsiveSheet.web.tsx +76 -0
- package/src/responsive-sheet/index.web.ts +7 -9
- package/src/responsive-sheet/types.ts +5 -2
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js +0 -226
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js.map +0 -1
- package/lib/commonjs/responsive-sheet/const.js +0 -60
- package/lib/commonjs/responsive-sheet/const.js.map +0 -1
- package/lib/module/responsive-sheet/ResponsiveSheet.native.js +0 -219
- package/lib/module/responsive-sheet/ResponsiveSheet.native.js.map +0 -1
- package/lib/module/responsive-sheet/const.js +0 -56
- package/lib/module/responsive-sheet/const.js.map +0 -1
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts +0 -20
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts.map +0 -1
- package/lib/typescript/commonjs/responsive-sheet/const.d.ts +0 -43
- package/lib/typescript/commonjs/responsive-sheet/const.d.ts.map +0 -1
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts +0 -20
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts.map +0 -1
- package/lib/typescript/module/responsive-sheet/const.d.ts +0 -43
- package/lib/typescript/module/responsive-sheet/const.d.ts.map +0 -1
- package/src/responsive-sheet/ResponsiveSheet.native.tsx +0 -257
- package/src/responsive-sheet/const.ts +0 -53
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -5,63 +5,119 @@ import React, {
|
|
|
5
5
|
useImperativeHandle,
|
|
6
6
|
useMemo,
|
|
7
7
|
useRef,
|
|
8
|
+
useState,
|
|
8
9
|
} from 'react';
|
|
9
10
|
import {
|
|
11
|
+
Pressable,
|
|
12
|
+
StyleSheet,
|
|
10
13
|
Text,
|
|
11
14
|
TouchableOpacity,
|
|
15
|
+
useWindowDimensions,
|
|
12
16
|
View,
|
|
13
17
|
type GestureResponderEvent,
|
|
18
|
+
type StyleProp,
|
|
19
|
+
type ViewStyle,
|
|
14
20
|
} from 'react-native';
|
|
21
|
+
import Animated, {
|
|
22
|
+
cancelAnimation,
|
|
23
|
+
Easing,
|
|
24
|
+
runOnJS,
|
|
25
|
+
useAnimatedStyle,
|
|
26
|
+
useSharedValue,
|
|
27
|
+
withTiming,
|
|
28
|
+
} from 'react-native-reanimated';
|
|
15
29
|
|
|
16
30
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
31
|
+
import { Z_INDEX } from '../styles/z-index';
|
|
17
32
|
import type { ThemeColors } from '../theme/types';
|
|
18
33
|
import { useTheme } from '../theme/use-theme';
|
|
19
34
|
import { Context, useDialogContext, useDialogControl } from './context';
|
|
35
|
+
import {
|
|
36
|
+
ANIMATION_DURATION,
|
|
37
|
+
DEFAULT_MAX_HEIGHT_RATIO,
|
|
38
|
+
DEFAULT_SIDE_WIDTH,
|
|
39
|
+
DIALOG_SHEET_BACKDROP_TESTID,
|
|
40
|
+
PANEL_RADIUS,
|
|
41
|
+
SHEET_BACKDROP_OPACITY,
|
|
42
|
+
SIDE_SHEET_MIN_GUTTER,
|
|
43
|
+
useResolvedPlacement,
|
|
44
|
+
} from './placement';
|
|
20
45
|
import type {
|
|
21
46
|
DialogAction,
|
|
22
47
|
DialogActionColor,
|
|
23
48
|
DialogControlProps,
|
|
49
|
+
DialogInset,
|
|
24
50
|
DialogProps,
|
|
25
51
|
} from './types';
|
|
26
52
|
|
|
53
|
+
export { DIALOG_SHEET_BACKDROP_TESTID };
|
|
54
|
+
|
|
27
55
|
/**
|
|
28
56
|
* Native variant of `<Dialog>`.
|
|
29
57
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* gracefully degrades from sheet (phone) to centered card (tablet, via the
|
|
33
|
-
* 500px max-width cap).
|
|
58
|
+
* Open/close is driven by EITHER the imperative `control` (legacy) OR the
|
|
59
|
+
* controlled `open` prop. When `open` is provided it wins.
|
|
34
60
|
*
|
|
35
|
-
*
|
|
61
|
+
* `placement` selects the surface:
|
|
36
62
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
63
|
+
* - `'center'` (default) / `'bottom'` — bloom's `BottomSheet` (`detached`
|
|
64
|
+
* for center: a floating, content-hugging card that degrades from sheet to
|
|
65
|
+
* centered card via the 500px cap; flush for bottom: a full-width sheet
|
|
66
|
+
* with a drag handle). Default behavior is unchanged.
|
|
67
|
+
* - `'left'` / `'right'` — an anchored side-sheet/drawer driven by
|
|
68
|
+
* reanimated (the `responsive-sheet` native technique). Exit animations
|
|
69
|
+
* are safe on native.
|
|
43
70
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
71
|
+
* The component accepts three rendering modes simultaneously:
|
|
72
|
+
*
|
|
73
|
+
* 1. Declarative — `title`, `description`, `actions`.
|
|
74
|
+
* 2. Custom children — caller passes JSX, bloom renders the chrome.
|
|
75
|
+
* 3. Pure children — no `title`/`description`/`actions`.
|
|
47
76
|
*/
|
|
48
77
|
export function Dialog({
|
|
49
78
|
control,
|
|
79
|
+
open: controlledOpen,
|
|
50
80
|
onClose,
|
|
51
81
|
testID,
|
|
52
82
|
title,
|
|
53
83
|
description,
|
|
54
84
|
actions,
|
|
85
|
+
placement,
|
|
86
|
+
width = DEFAULT_SIDE_WIDTH,
|
|
87
|
+
maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
|
|
88
|
+
inset,
|
|
89
|
+
showHandle = true,
|
|
90
|
+
dismissOnBackdrop = true,
|
|
55
91
|
style,
|
|
92
|
+
panelStyle,
|
|
93
|
+
panelClassName,
|
|
94
|
+
containerStyle,
|
|
95
|
+
containerClassName,
|
|
56
96
|
label,
|
|
57
97
|
children,
|
|
58
98
|
}: DialogProps) {
|
|
99
|
+
const isControlled = controlledOpen !== undefined;
|
|
100
|
+
const resolvedPlacement = useResolvedPlacement(placement);
|
|
101
|
+
const isSide = resolvedPlacement === 'left' || resolvedPlacement === 'right';
|
|
102
|
+
|
|
103
|
+
// Imperative open state for the side placement (the BottomSheet path owns its
|
|
104
|
+
// own visibility via its ref instead).
|
|
105
|
+
const [sideOpen, setSideOpen] = useState(false);
|
|
106
|
+
|
|
59
107
|
const theme = useTheme();
|
|
60
108
|
const ref = useRef<BottomSheetRef>(null);
|
|
61
109
|
const closeCallbacks = useRef<(() => void)[]>([]);
|
|
62
110
|
const titleId = useId();
|
|
63
111
|
const descriptionId = useId();
|
|
64
112
|
|
|
113
|
+
// Read the latest controlled flag / `onClose` inside stable callbacks
|
|
114
|
+
// without re-binding them (the context + imperative handle depend on `close`
|
|
115
|
+
// staying referentially stable).
|
|
116
|
+
const isControlledRef = useRef(isControlled);
|
|
117
|
+
isControlledRef.current = isControlled;
|
|
118
|
+
const onCloseRef = useRef(onClose);
|
|
119
|
+
onCloseRef.current = onClose;
|
|
120
|
+
|
|
65
121
|
// Drain queued close callbacks atomically — capturing the list and
|
|
66
122
|
// resetting it before invocation ensures a callback that synchronously
|
|
67
123
|
// re-opens the dialog (and queues fresh callbacks) does not see the old
|
|
@@ -81,23 +137,51 @@ export function Dialog({
|
|
|
81
137
|
}, []);
|
|
82
138
|
|
|
83
139
|
const open = useCallback(() => {
|
|
140
|
+
setSideOpen(true);
|
|
84
141
|
ref.current?.present();
|
|
85
142
|
}, []);
|
|
86
143
|
|
|
144
|
+
// A dismissal request (backdrop, pan-to-close, action button). In controlled
|
|
145
|
+
// mode it asks the host to close via `onClose` (the host flips `open`); in
|
|
146
|
+
// imperative mode it dismisses the surface directly. The optional callback
|
|
147
|
+
// runs once the dialog has finished closing.
|
|
87
148
|
const close = useCallback<DialogControlProps['close']>((cb) => {
|
|
88
149
|
if (typeof cb === 'function') {
|
|
89
150
|
closeCallbacks.current.push(cb);
|
|
90
151
|
}
|
|
152
|
+
if (isControlledRef.current) {
|
|
153
|
+
onCloseRef.current?.();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
setSideOpen(false);
|
|
91
157
|
ref.current?.dismiss();
|
|
92
158
|
}, []);
|
|
93
159
|
|
|
160
|
+
// Fired when the underlying surface has finished closing. Drains queued
|
|
161
|
+
// callbacks, then fires `onClose` ONLY in imperative mode — controlled mode
|
|
162
|
+
// already requested the close through `onClose` (the host then flipped
|
|
163
|
+
// `open`), so firing it again here would double-call it.
|
|
94
164
|
const handleDismiss = useCallback(() => {
|
|
95
165
|
callQueuedCallbacks();
|
|
96
|
-
|
|
97
|
-
}, [callQueuedCallbacks
|
|
166
|
+
if (!isControlledRef.current) onCloseRef.current?.();
|
|
167
|
+
}, [callQueuedCallbacks]);
|
|
168
|
+
|
|
169
|
+
// In controlled mode, mirror the `open` prop onto the underlying surface so
|
|
170
|
+
// both modes share one lifecycle. The BottomSheet path presents/dismisses by
|
|
171
|
+
// ref; the side path flips `sideOpen`.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!isControlled) return;
|
|
174
|
+
if (controlledOpen) {
|
|
175
|
+
setSideOpen(true);
|
|
176
|
+
ref.current?.present();
|
|
177
|
+
} else {
|
|
178
|
+
setSideOpen(false);
|
|
179
|
+
ref.current?.dismiss();
|
|
180
|
+
}
|
|
181
|
+
}, [isControlled, controlledOpen]);
|
|
98
182
|
|
|
99
183
|
useImperativeHandle(
|
|
100
|
-
control
|
|
184
|
+
control?.ref,
|
|
101
185
|
() => ({ open, close }),
|
|
102
186
|
[open, close],
|
|
103
187
|
);
|
|
@@ -119,16 +203,59 @@ export function Dialog({
|
|
|
119
203
|
[theme.colors.background],
|
|
120
204
|
);
|
|
121
205
|
|
|
206
|
+
const bodyNode = (
|
|
207
|
+
<DialogBody
|
|
208
|
+
titleId={titleId}
|
|
209
|
+
descriptionId={descriptionId}
|
|
210
|
+
title={title}
|
|
211
|
+
description={description}
|
|
212
|
+
actions={actions}
|
|
213
|
+
>
|
|
214
|
+
{children}
|
|
215
|
+
</DialogBody>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (isSide) {
|
|
219
|
+
return (
|
|
220
|
+
<Context.Provider value={context}>
|
|
221
|
+
<SideSheet
|
|
222
|
+
open={sideOpen}
|
|
223
|
+
onDismiss={handleDismiss}
|
|
224
|
+
side={resolvedPlacement}
|
|
225
|
+
width={width}
|
|
226
|
+
inset={inset}
|
|
227
|
+
dismissOnBackdrop={dismissOnBackdrop}
|
|
228
|
+
testID={testID}
|
|
229
|
+
label={label}
|
|
230
|
+
title={title}
|
|
231
|
+
description={description}
|
|
232
|
+
titleId={titleId}
|
|
233
|
+
descriptionId={descriptionId}
|
|
234
|
+
panelStyle={panelStyle}
|
|
235
|
+
panelClassName={panelClassName}
|
|
236
|
+
containerStyle={containerStyle}
|
|
237
|
+
containerClassName={containerClassName}
|
|
238
|
+
style={style}
|
|
239
|
+
>
|
|
240
|
+
{bodyNode}
|
|
241
|
+
</SideSheet>
|
|
242
|
+
</Context.Provider>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const isBottom = resolvedPlacement === 'bottom';
|
|
247
|
+
|
|
122
248
|
return (
|
|
123
249
|
<BottomSheet
|
|
124
250
|
ref={ref}
|
|
125
251
|
onDismiss={handleDismiss}
|
|
126
252
|
enablePanDownToClose
|
|
127
|
-
detached
|
|
253
|
+
detached={!isBottom}
|
|
254
|
+
showHandle={isBottom ? showHandle : true}
|
|
128
255
|
// Stronger dim when a Dialog is stacked over another sheet so the
|
|
129
256
|
// underlying sheet's handle/content doesn't bleed through.
|
|
130
257
|
backdropOpacity={0.7}
|
|
131
|
-
style={sheetStyle}
|
|
258
|
+
style={isBottom ? bottomSheetStyle(theme.colors.background, maxHeightRatio) : sheetStyle}
|
|
132
259
|
>
|
|
133
260
|
<Context.Provider value={context}>
|
|
134
261
|
<View
|
|
@@ -136,6 +263,7 @@ export function Dialog({
|
|
|
136
263
|
accessibilityLabel={label}
|
|
137
264
|
aria-labelledby={title ? titleId : undefined}
|
|
138
265
|
aria-describedby={description ? descriptionId : undefined}
|
|
266
|
+
{...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
|
|
139
267
|
style={[
|
|
140
268
|
// Detached BottomSheet already adds `marginBottom: insets.bottom + 16`
|
|
141
269
|
// to the sheet container — the floating card sits ABOVE the
|
|
@@ -143,45 +271,252 @@ export function Dialog({
|
|
|
143
271
|
{ paddingTop: 20, paddingHorizontal: 20, paddingBottom: 20 },
|
|
144
272
|
{ backgroundColor: theme.colors.background },
|
|
145
273
|
style,
|
|
274
|
+
panelStyle,
|
|
146
275
|
]}
|
|
147
276
|
>
|
|
148
|
-
{
|
|
149
|
-
<Text
|
|
150
|
-
nativeID={titleId}
|
|
151
|
-
style={{
|
|
152
|
-
fontSize: 22,
|
|
153
|
-
fontWeight: '600',
|
|
154
|
-
color: theme.colors.text,
|
|
155
|
-
paddingBottom: description ? 4 : 16,
|
|
156
|
-
lineHeight: 30,
|
|
157
|
-
}}
|
|
158
|
-
>
|
|
159
|
-
{title}
|
|
160
|
-
</Text>
|
|
161
|
-
) : null}
|
|
162
|
-
{description ? (
|
|
163
|
-
<Text
|
|
164
|
-
nativeID={descriptionId}
|
|
165
|
-
style={{
|
|
166
|
-
fontSize: 16,
|
|
167
|
-
color: theme.colors.textSecondary,
|
|
168
|
-
paddingBottom: 16,
|
|
169
|
-
lineHeight: 22,
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
172
|
-
{description}
|
|
173
|
-
</Text>
|
|
174
|
-
) : null}
|
|
175
|
-
{children}
|
|
176
|
-
{actions && actions.length > 0 ? (
|
|
177
|
-
<ActionRow actions={actions} />
|
|
178
|
-
) : null}
|
|
277
|
+
{bodyNode}
|
|
179
278
|
</View>
|
|
180
279
|
</Context.Provider>
|
|
181
280
|
</BottomSheet>
|
|
182
281
|
);
|
|
183
282
|
}
|
|
184
283
|
|
|
284
|
+
function bottomSheetStyle(background: string, maxHeightRatio: number): ViewStyle {
|
|
285
|
+
return {
|
|
286
|
+
backgroundColor: background,
|
|
287
|
+
borderTopLeftRadius: PANEL_RADIUS + 4,
|
|
288
|
+
borderTopRightRadius: PANEL_RADIUS + 4,
|
|
289
|
+
maxHeight: `${Math.round(maxHeightRatio * 100)}%`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Renders the dialog's body: optional declarative title + description, any
|
|
295
|
+
* `children`, then the action row. Shared by all placements. The
|
|
296
|
+
* `titleId`/`descriptionId` are wired by the caller for accessibility.
|
|
297
|
+
*/
|
|
298
|
+
function DialogBody({
|
|
299
|
+
titleId,
|
|
300
|
+
descriptionId,
|
|
301
|
+
title,
|
|
302
|
+
description,
|
|
303
|
+
actions,
|
|
304
|
+
children,
|
|
305
|
+
}: {
|
|
306
|
+
titleId: string;
|
|
307
|
+
descriptionId: string;
|
|
308
|
+
title?: string;
|
|
309
|
+
description?: string;
|
|
310
|
+
actions?: DialogAction[];
|
|
311
|
+
children?: React.ReactNode;
|
|
312
|
+
}) {
|
|
313
|
+
const theme = useTheme();
|
|
314
|
+
return (
|
|
315
|
+
<>
|
|
316
|
+
{title ? (
|
|
317
|
+
<Text
|
|
318
|
+
nativeID={titleId}
|
|
319
|
+
style={{
|
|
320
|
+
fontSize: 22,
|
|
321
|
+
fontWeight: '600',
|
|
322
|
+
color: theme.colors.text,
|
|
323
|
+
paddingBottom: description ? 4 : 16,
|
|
324
|
+
lineHeight: 30,
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{title}
|
|
328
|
+
</Text>
|
|
329
|
+
) : null}
|
|
330
|
+
{description ? (
|
|
331
|
+
<Text
|
|
332
|
+
nativeID={descriptionId}
|
|
333
|
+
style={{
|
|
334
|
+
fontSize: 16,
|
|
335
|
+
color: theme.colors.textSecondary,
|
|
336
|
+
paddingBottom: 16,
|
|
337
|
+
lineHeight: 22,
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
{description}
|
|
341
|
+
</Text>
|
|
342
|
+
) : null}
|
|
343
|
+
{children}
|
|
344
|
+
{actions && actions.length > 0 ? <ActionRow actions={actions} /> : null}
|
|
345
|
+
</>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Reanimated side-sheet/drawer for the `left`/`right` placements (native).
|
|
351
|
+
*
|
|
352
|
+
* A shared `progress` value times 0 -> 1 on open and 1 -> 0 on close, mapped to
|
|
353
|
+
* the panel translateX and the backdrop opacity. The node stays mounted through
|
|
354
|
+
* the close timing and unmounts from the animation completion callback. Native
|
|
355
|
+
* exit animations are safe (the web `removeChild` crash does not apply).
|
|
356
|
+
*/
|
|
357
|
+
function SideSheet({
|
|
358
|
+
open,
|
|
359
|
+
onDismiss,
|
|
360
|
+
side,
|
|
361
|
+
width,
|
|
362
|
+
inset,
|
|
363
|
+
dismissOnBackdrop,
|
|
364
|
+
testID,
|
|
365
|
+
label,
|
|
366
|
+
title,
|
|
367
|
+
description,
|
|
368
|
+
titleId,
|
|
369
|
+
descriptionId,
|
|
370
|
+
panelStyle,
|
|
371
|
+
panelClassName,
|
|
372
|
+
containerStyle,
|
|
373
|
+
containerClassName,
|
|
374
|
+
style,
|
|
375
|
+
children,
|
|
376
|
+
}: {
|
|
377
|
+
open: boolean;
|
|
378
|
+
onDismiss: () => void;
|
|
379
|
+
side: 'left' | 'right';
|
|
380
|
+
width: number;
|
|
381
|
+
inset?: DialogInset;
|
|
382
|
+
dismissOnBackdrop: boolean;
|
|
383
|
+
testID?: string;
|
|
384
|
+
label?: string;
|
|
385
|
+
title?: string;
|
|
386
|
+
description?: string;
|
|
387
|
+
titleId: string;
|
|
388
|
+
descriptionId: string;
|
|
389
|
+
panelStyle?: StyleProp<ViewStyle>;
|
|
390
|
+
panelClassName?: string;
|
|
391
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
392
|
+
containerClassName?: string;
|
|
393
|
+
style?: StyleProp<ViewStyle>;
|
|
394
|
+
children: React.ReactNode;
|
|
395
|
+
}) {
|
|
396
|
+
const theme = useTheme();
|
|
397
|
+
const { width: viewportWidth } = useWindowDimensions();
|
|
398
|
+
|
|
399
|
+
const [mounted, setMounted] = useState(open);
|
|
400
|
+
const progress = useSharedValue(open ? 1 : 0);
|
|
401
|
+
|
|
402
|
+
const insetTop = inset?.top ?? 0;
|
|
403
|
+
const insetBottom = inset?.bottom ?? 0;
|
|
404
|
+
const insetLeft = inset?.left ?? 0;
|
|
405
|
+
const insetRight = inset?.right ?? 0;
|
|
406
|
+
const anchorInset = side === 'left' ? insetLeft : insetRight;
|
|
407
|
+
const oppositeInset = side === 'left' ? insetRight : insetLeft;
|
|
408
|
+
|
|
409
|
+
const sideWidth = useMemo(() => {
|
|
410
|
+
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
411
|
+
return Math.max(0, Math.min(width, available));
|
|
412
|
+
}, [viewportWidth, anchorInset, oppositeInset, width]);
|
|
413
|
+
|
|
414
|
+
// Travel distance for the hidden state: the panel's measured width, with the
|
|
415
|
+
// geometry target as the pre-measurement fallback.
|
|
416
|
+
const travel = useSharedValue(sideWidth);
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
travel.value = sideWidth;
|
|
419
|
+
}, [sideWidth, travel]);
|
|
420
|
+
|
|
421
|
+
const finishClose = useCallback(() => {
|
|
422
|
+
setMounted(false);
|
|
423
|
+
onDismiss();
|
|
424
|
+
}, [onDismiss]);
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
cancelAnimation(progress);
|
|
428
|
+
const timing = { duration: ANIMATION_DURATION, easing: Easing.out(Easing.cubic) };
|
|
429
|
+
if (open) {
|
|
430
|
+
setMounted(true);
|
|
431
|
+
progress.value = withTiming(1, timing);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!mounted) return;
|
|
435
|
+
progress.value = withTiming(0, timing, (finished) => {
|
|
436
|
+
if (finished) runOnJS(finishClose)();
|
|
437
|
+
});
|
|
438
|
+
}, [open, mounted, progress, finishClose]);
|
|
439
|
+
|
|
440
|
+
const handleBackdropPress = useCallback(() => {
|
|
441
|
+
if (dismissOnBackdrop) onDismiss();
|
|
442
|
+
}, [dismissOnBackdrop, onDismiss]);
|
|
443
|
+
|
|
444
|
+
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
|
445
|
+
opacity: progress.value * SHEET_BACKDROP_OPACITY,
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
const hiddenSign = side === 'left' ? -1 : 1;
|
|
449
|
+
const panelAnimatedStyle = useAnimatedStyle(() => {
|
|
450
|
+
const hidden = 1 - progress.value;
|
|
451
|
+
return {
|
|
452
|
+
opacity: progress.value,
|
|
453
|
+
transform: [{ translateX: hidden * travel.value * hiddenSign }],
|
|
454
|
+
};
|
|
455
|
+
}, [hiddenSign]);
|
|
456
|
+
|
|
457
|
+
const measurePanel = useCallback(
|
|
458
|
+
(e: { nativeEvent: { layout: { width: number } } }) => {
|
|
459
|
+
const measured = e.nativeEvent.layout.width;
|
|
460
|
+
if (measured > 0) travel.value = measured;
|
|
461
|
+
},
|
|
462
|
+
[travel],
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const panelGeometry = useMemo<ViewStyle>(
|
|
466
|
+
() =>
|
|
467
|
+
({
|
|
468
|
+
top: insetTop,
|
|
469
|
+
bottom: insetBottom,
|
|
470
|
+
[side]: anchorInset,
|
|
471
|
+
width: sideWidth,
|
|
472
|
+
borderRadius: PANEL_RADIUS,
|
|
473
|
+
}) as ViewStyle,
|
|
474
|
+
[insetTop, insetBottom, side, anchorInset, sideWidth],
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
if (!mounted) return null;
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<View
|
|
481
|
+
style={[sideStyles.root, containerStyle]}
|
|
482
|
+
{...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
|
|
483
|
+
pointerEvents="box-none"
|
|
484
|
+
>
|
|
485
|
+
<Animated.View style={[sideStyles.backdrop, backdropAnimatedStyle]} pointerEvents="auto">
|
|
486
|
+
<Pressable
|
|
487
|
+
testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
|
|
488
|
+
accessibilityRole="button"
|
|
489
|
+
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
490
|
+
onPress={handleBackdropPress}
|
|
491
|
+
disabled={!dismissOnBackdrop}
|
|
492
|
+
style={StyleSheet.absoluteFill}
|
|
493
|
+
/>
|
|
494
|
+
</Animated.View>
|
|
495
|
+
|
|
496
|
+
<Animated.View
|
|
497
|
+
accessibilityViewIsModal
|
|
498
|
+
accessibilityLabel={label}
|
|
499
|
+
aria-labelledby={title ? titleId : undefined}
|
|
500
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
501
|
+
testID={testID}
|
|
502
|
+
onLayout={measurePanel}
|
|
503
|
+
{...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
|
|
504
|
+
style={[
|
|
505
|
+
sideStyles.panel,
|
|
506
|
+
{ backgroundColor: theme.colors.background, shadowColor: theme.colors.shadow },
|
|
507
|
+
panelGeometry,
|
|
508
|
+
panelAnimatedStyle,
|
|
509
|
+
panelStyle,
|
|
510
|
+
style,
|
|
511
|
+
]}
|
|
512
|
+
pointerEvents="auto"
|
|
513
|
+
>
|
|
514
|
+
<View style={sideStyles.body}>{children}</View>
|
|
515
|
+
</Animated.View>
|
|
516
|
+
</View>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
185
520
|
function ActionRow({ actions }: { actions: DialogAction[] }) {
|
|
186
521
|
return (
|
|
187
522
|
<View style={{ width: '100%', gap: 8, justifyContent: 'flex-end' }}>
|
|
@@ -265,6 +600,28 @@ function getActionPalette(
|
|
|
265
600
|
}
|
|
266
601
|
}
|
|
267
602
|
|
|
603
|
+
const sideStyles = StyleSheet.create({
|
|
604
|
+
root: {
|
|
605
|
+
...StyleSheet.absoluteFill,
|
|
606
|
+
zIndex: Z_INDEX.fullscreen,
|
|
607
|
+
},
|
|
608
|
+
backdrop: {
|
|
609
|
+
...StyleSheet.absoluteFill,
|
|
610
|
+
backgroundColor: '#000',
|
|
611
|
+
},
|
|
612
|
+
panel: {
|
|
613
|
+
position: 'absolute',
|
|
614
|
+
overflow: 'hidden',
|
|
615
|
+
shadowOpacity: 0.18,
|
|
616
|
+
shadowRadius: 24,
|
|
617
|
+
shadowOffset: { width: 0, height: 8 },
|
|
618
|
+
elevation: 12,
|
|
619
|
+
},
|
|
620
|
+
body: {
|
|
621
|
+
padding: 20,
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
|
268
625
|
/**
|
|
269
626
|
* Helper used by the imperative `alert()` API. Mounts a `<Dialog>` against
|
|
270
627
|
* a fresh control and presents it immediately. `onResolve` is invoked
|