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