@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
@@ -1,303 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import {
3
- Modal,
4
- Pressable,
5
- ScrollView,
6
- StyleSheet,
7
- View,
8
- } from 'react-native';
9
-
10
- import { useInteractionState } from '../hooks/useInteractionState';
11
- import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '../icons/Times';
12
- import { useTheme } from '../theme/use-theme';
13
- import { Text } from '../typography';
14
- import type { CenteredDialogProps } from './centered-dialog-types';
15
- import {
16
- CARD_RADIUS,
17
- DEFAULT_MAX_WIDTH,
18
- MAX_HEIGHT_FRACTION,
19
- resolveSpacing,
20
- VIEWPORT_GUTTER,
21
- } from './centered-dialog-tokens';
22
-
23
- const CLOSE_HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
24
-
25
- /** Stable testID for the dimmed backdrop. Overridden when a `testID` is set. */
26
- export const CENTERED_DIALOG_BACKDROP_TESTID = 'bloom-centered-dialog-backdrop';
27
-
28
- /**
29
- * Native variant of `<CenteredDialog>`.
30
- *
31
- * A controlled, centered modal: a transparent RN `Modal` (fade animation,
32
- * own native window) hosting a dimmed full-screen backdrop and a centered,
33
- * rounded card styled entirely from Bloom theme tokens.
34
- *
35
- * The backdrop is a sibling `Pressable` rendered BEHIND the card (absolute
36
- * filled), never a parent of it — so a tap on the card never bubbles to the
37
- * backdrop's close handler and the card never ends up nested inside the
38
- * backdrop's pressable on web.
39
- *
40
- * Dismissal: backdrop tap (when `dismissible`), the header close button, and
41
- * the Android hardware back button (`onRequestClose`) all call `onClose`.
42
- */
43
- export function CenteredDialog({
44
- visible,
45
- onClose,
46
- title,
47
- dismissible = true,
48
- maxWidth = DEFAULT_MAX_WIDTH,
49
- compact = true,
50
- showClose,
51
- children,
52
- footer,
53
- accessibilityLabel,
54
- closeAccessibilityLabel = 'Close dialog',
55
- backdropAccessibilityLabel = 'Dismiss dialog',
56
- cardStyle,
57
- contentStyle,
58
- testID,
59
- }: CenteredDialogProps) {
60
- const theme = useTheme();
61
- const backdropPress = useInteractionState();
62
-
63
- const spacing = useMemo(() => resolveSpacing(compact), [compact]);
64
- const resolvedShowClose = showClose ?? Boolean(title);
65
- const hasHeader = Boolean(title) || resolvedShowClose;
66
-
67
- const handleBackdropPress = dismissible ? onClose : undefined;
68
- // Android's hardware back must always be answerable; when the dialog is
69
- // non-dismissible we still need a handler (RN requires one for transparent
70
- // modals) — make it a no-op so back does nothing rather than crash.
71
- const handleRequestClose = dismissible ? onClose : noop;
72
-
73
- return (
74
- <Modal
75
- visible={visible}
76
- transparent
77
- animationType="fade"
78
- statusBarTranslucent
79
- onRequestClose={handleRequestClose}
80
- >
81
- <View style={[styles.root, { padding: VIEWPORT_GUTTER }]}>
82
- <Pressable
83
- testID={testID ? `${testID}-backdrop` : CENTERED_DIALOG_BACKDROP_TESTID}
84
- style={[
85
- styles.backdrop,
86
- { backgroundColor: theme.colors.overlay },
87
- backdropPress.state && styles.backdropPressed,
88
- ]}
89
- onPress={handleBackdropPress}
90
- onPressIn={backdropPress.onIn}
91
- onPressOut={backdropPress.onOut}
92
- disabled={!dismissible}
93
- accessibilityRole="button"
94
- accessibilityLabel={backdropAccessibilityLabel}
95
- />
96
-
97
- {/* RN's `Modal` already isolates its contents in a separate
98
- accessibility window, so an explicit `accessibilityViewIsModal`
99
- on the card is redundant here (and would mark the sibling
100
- backdrop inaccessible). The web fork uses `aria-modal` instead. */}
101
- <View
102
- testID={testID}
103
- accessibilityLabel={accessibilityLabel ?? title}
104
- style={[
105
- styles.card,
106
- {
107
- maxWidth,
108
- maxHeight: `${MAX_HEIGHT_FRACTION * 100}%`,
109
- backgroundColor: theme.colors.card,
110
- borderColor: theme.colors.border,
111
- shadowColor: theme.colors.shadow,
112
- },
113
- cardStyle,
114
- ]}
115
- >
116
- {hasHeader ? (
117
- <DialogHeader
118
- title={title}
119
- showClose={resolvedShowClose}
120
- padding={spacing.pad}
121
- gap={spacing.headerGap}
122
- closeAccessibilityLabel={closeAccessibilityLabel}
123
- onClose={onClose}
124
- />
125
- ) : null}
126
-
127
- <ScrollView
128
- style={styles.bodyScroll}
129
- contentContainerStyle={[
130
- {
131
- padding: spacing.pad,
132
- paddingTop: hasHeader ? spacing.headerGap : spacing.pad,
133
- gap: spacing.contentGap,
134
- },
135
- contentStyle,
136
- ]}
137
- showsVerticalScrollIndicator={false}
138
- keyboardShouldPersistTaps="handled"
139
- >
140
- {children}
141
- </ScrollView>
142
-
143
- {footer ? (
144
- <View
145
- style={[
146
- styles.footer,
147
- {
148
- padding: spacing.pad,
149
- paddingTop: spacing.footerGap,
150
- borderTopColor: theme.colors.borderLight,
151
- },
152
- ]}
153
- >
154
- {footer}
155
- </View>
156
- ) : null}
157
- </View>
158
- </View>
159
- </Modal>
160
- );
161
- }
162
-
163
- function DialogHeader({
164
- title,
165
- showClose,
166
- padding,
167
- gap,
168
- closeAccessibilityLabel,
169
- onClose,
170
- }: {
171
- title?: string;
172
- showClose: boolean;
173
- padding: number;
174
- gap: number;
175
- closeAccessibilityLabel: string;
176
- onClose: () => void;
177
- }) {
178
- const theme = useTheme();
179
-
180
- return (
181
- <View
182
- style={[
183
- styles.header,
184
- { paddingHorizontal: padding, paddingTop: padding, gap },
185
- ]}
186
- >
187
- {title ? (
188
- <Text
189
- accessibilityRole="header"
190
- numberOfLines={2}
191
- style={[styles.title, { color: theme.colors.text }]}
192
- >
193
- {title}
194
- </Text>
195
- ) : (
196
- <View style={styles.titleSpacer} />
197
- )}
198
- {showClose ? (
199
- <CloseButton
200
- accessibilityLabel={closeAccessibilityLabel}
201
- onPress={onClose}
202
- />
203
- ) : null}
204
- </View>
205
- );
206
- }
207
-
208
- function CloseButton({
209
- accessibilityLabel,
210
- onPress,
211
- }: {
212
- accessibilityLabel: string;
213
- onPress: () => void;
214
- }) {
215
- const theme = useTheme();
216
- const press = useInteractionState();
217
-
218
- return (
219
- <Pressable
220
- onPress={onPress}
221
- onPressIn={press.onIn}
222
- onPressOut={press.onOut}
223
- hitSlop={CLOSE_HIT_SLOP}
224
- accessibilityRole="button"
225
- accessibilityLabel={accessibilityLabel}
226
- style={[
227
- styles.closeButton,
228
- { backgroundColor: theme.colors.contrast50 },
229
- press.state && styles.closeButtonPressed,
230
- ]}
231
- >
232
- <CloseIcon size="sm" fill={theme.colors.textSecondary} />
233
- </Pressable>
234
- );
235
- }
236
-
237
- function noop(): void {
238
- /* non-dismissible: hardware back is intentionally inert */
239
- }
240
-
241
- const styles = StyleSheet.create({
242
- root: {
243
- flex: 1,
244
- alignItems: 'center',
245
- justifyContent: 'center',
246
- },
247
- backdrop: {
248
- // Spell out the absolute-fill rect (matches `Fill`'s canonical style)
249
- // rather than spreading the deprecated `StyleSheet.absoluteFillObject`,
250
- // which RN 0.83's generated types no longer surface on the `StyleSheet`
251
- // namespace and which downstream type-checks reject.
252
- position: 'absolute',
253
- top: 0,
254
- right: 0,
255
- bottom: 0,
256
- left: 0,
257
- },
258
- // A subtle deepening on press confirms the tap-to-dismiss affordance.
259
- backdropPressed: {
260
- opacity: 0.92,
261
- },
262
- card: {
263
- width: '100%',
264
- borderRadius: CARD_RADIUS,
265
- borderWidth: StyleSheet.hairlineWidth,
266
- overflow: 'hidden',
267
- shadowOpacity: 0.18,
268
- shadowRadius: 24,
269
- shadowOffset: { width: 0, height: 8 },
270
- elevation: 12,
271
- },
272
- header: {
273
- flexDirection: 'row',
274
- alignItems: 'flex-start',
275
- justifyContent: 'space-between',
276
- },
277
- title: {
278
- flex: 1,
279
- fontSize: 17,
280
- fontWeight: '700',
281
- lineHeight: 22,
282
- },
283
- titleSpacer: {
284
- flex: 1,
285
- },
286
- closeButton: {
287
- width: 28,
288
- height: 28,
289
- borderRadius: 14,
290
- alignItems: 'center',
291
- justifyContent: 'center',
292
- },
293
- closeButtonPressed: {
294
- opacity: 0.7,
295
- },
296
- bodyScroll: {
297
- flexGrow: 0,
298
- flexShrink: 1,
299
- },
300
- footer: {
301
- borderTopWidth: StyleSheet.hairlineWidth,
302
- },
303
- });
@@ -1,372 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
2
- import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from 'react-native';
3
- import { RemoveScrollBar } from 'react-remove-scroll-bar';
4
-
5
- import { useInteractionStates } from '../hooks/useInteractionState';
6
- import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '../icons/Times';
7
- import { Portal } from '../portal/index.web';
8
- import { createOverlayZIndex } from '../styles/z-index';
9
- import { useTheme } from '../theme/use-theme';
10
- import { Text } from '../typography';
11
- import type { CenteredDialogProps } from './centered-dialog-types';
12
- import {
13
- CARD_RADIUS,
14
- DEFAULT_MAX_WIDTH,
15
- MAX_HEIGHT_FRACTION,
16
- resolveSpacing,
17
- VIEWPORT_GUTTER,
18
- } from './centered-dialog-tokens';
19
-
20
- const FADE_OUT_DURATION = 150;
21
- const CLOSE_HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
22
- const dialogZIndex = createOverlayZIndex();
23
-
24
- /** Stable testID for the dimmed backdrop. Overridden when a `testID` is set. */
25
- export const CENTERED_DIALOG_BACKDROP_TESTID = 'bloom-centered-dialog-backdrop';
26
-
27
- const stopPropagation = (e: { stopPropagation: () => void }) => e.stopPropagation();
28
-
29
- /**
30
- * Web variant of `<CenteredDialog>`.
31
- *
32
- * A centered modal card rendered into the Bloom Portal at the end of
33
- * `document.body`. Identical prop surface to native — `visible`, `onClose`,
34
- * `title`, `compact`, etc. — so call sites are platform agnostic.
35
- *
36
- * Differences from the native fork (necessary, not behavioural):
37
- * - Portal + `RemoveScrollBar` lock the underlying page while open.
38
- * - `Escape` dismisses (when `dismissible`).
39
- * - A `role="dialog"` panel with `aria-label`/`aria-modal` for a11y.
40
- * - A CSS fade/zoom keyframe matches the native modal's `fade` animation;
41
- * the keyframes are injected once (idempotent) into `<head>`.
42
- *
43
- * The backdrop is a sibling element BEHIND the card, and the card stops
44
- * click/press propagation so a tap on the card never dismisses.
45
- */
46
- export function CenteredDialog({
47
- visible,
48
- onClose,
49
- title,
50
- dismissible = true,
51
- maxWidth = DEFAULT_MAX_WIDTH,
52
- compact = true,
53
- showClose,
54
- children,
55
- footer,
56
- accessibilityLabel,
57
- closeAccessibilityLabel = 'Close dialog',
58
- backdropAccessibilityLabel = 'Dismiss dialog',
59
- cardStyle,
60
- contentStyle,
61
- testID,
62
- }: CenteredDialogProps) {
63
- const theme = useTheme();
64
- // `rendered` keeps the dialog mounted through its fade-out so a controlled
65
- // `visible` flip to `false` still plays an exit animation (matching the
66
- // native `Modal animationType="fade"`, which animates both directions).
67
- const [rendered, setRendered] = useState(visible);
68
- const isClosing = rendered && !visible;
69
-
70
- useKeyframes();
71
-
72
- // Mount immediately when opened; defer unmount until the exit animation
73
- // has played when closed.
74
- useEffect(() => {
75
- if (visible) {
76
- setRendered(true);
77
- return;
78
- }
79
- if (!rendered) return;
80
- const timer = setTimeout(() => setRendered(false), FADE_OUT_DURATION);
81
- return () => clearTimeout(timer);
82
- }, [visible, rendered]);
83
-
84
- // Escape-to-close while open. Scoped to the open lifetime so stacked
85
- // dialogs don't fight for the keydown — the top-most one wins via
86
- // document-level event order.
87
- useEffect(() => {
88
- if (!visible || !dismissible || typeof document === 'undefined') return;
89
- const handler = (e: KeyboardEvent) => {
90
- if (e.key === 'Escape') {
91
- e.stopPropagation();
92
- onClose();
93
- }
94
- };
95
- document.addEventListener('keydown', handler);
96
- return () => document.removeEventListener('keydown', handler);
97
- }, [visible, dismissible, onClose]);
98
-
99
- const spacing = useMemo(() => resolveSpacing(compact), [compact]);
100
- const resolvedShowClose = showClose ?? Boolean(title);
101
- const hasHeader = Boolean(title) || resolvedShowClose;
102
-
103
- if (!rendered) return null;
104
-
105
- // While closing, the backdrop must not re-trigger onClose (it's mid-exit).
106
- const handleBackdropPress = dismissible && !isClosing ? onClose : undefined;
107
-
108
- return (
109
- <Portal>
110
- <RemoveScrollBar />
111
- {/* `pointerEvents: 'auto'` opts back in from the Portal root's
112
- `pointer-events: none` (set so the idle portal doesn't intercept
113
- clicks on the underlying app). */}
114
- <View style={[styles.root, { padding: VIEWPORT_GUTTER }]}>
115
- <Pressable
116
- testID={testID ? `${testID}-backdrop` : CENTERED_DIALOG_BACKDROP_TESTID}
117
- onPress={handleBackdropPress}
118
- disabled={!dismissible}
119
- accessibilityRole="button"
120
- accessibilityLabel={backdropAccessibilityLabel}
121
- style={[
122
- styles.backdrop,
123
- { backgroundColor: theme.colors.overlay },
124
- backdropAnimation(isClosing),
125
- ]}
126
- />
127
-
128
- <View
129
- testID={testID}
130
- role="dialog"
131
- aria-modal
132
- aria-label={accessibilityLabel ?? title}
133
- onStartShouldSetResponder={returnsTrue}
134
- onResponderRelease={stopPropagation}
135
- {...({ onClick: stopPropagation } as Record<string, unknown>)}
136
- style={[
137
- styles.card,
138
- {
139
- maxWidth,
140
- maxHeight: `${MAX_HEIGHT_FRACTION * 100}%`,
141
- backgroundColor: theme.colors.card,
142
- borderColor: theme.colors.border,
143
- shadowColor: theme.colors.shadow,
144
- },
145
- cardAnimation(isClosing),
146
- cardStyle,
147
- ]}
148
- >
149
- {hasHeader ? (
150
- <DialogHeader
151
- title={title}
152
- showClose={resolvedShowClose}
153
- padding={spacing.pad}
154
- gap={spacing.headerGap}
155
- closeAccessibilityLabel={closeAccessibilityLabel}
156
- onClose={onClose}
157
- />
158
- ) : null}
159
-
160
- <ScrollView
161
- style={styles.bodyScroll}
162
- contentContainerStyle={[
163
- {
164
- padding: spacing.pad,
165
- paddingTop: hasHeader ? spacing.headerGap : spacing.pad,
166
- gap: spacing.contentGap,
167
- },
168
- contentStyle,
169
- ]}
170
- showsVerticalScrollIndicator={false}
171
- keyboardShouldPersistTaps="handled"
172
- >
173
- {children}
174
- </ScrollView>
175
-
176
- {footer ? (
177
- <View
178
- style={[
179
- styles.footer,
180
- {
181
- padding: spacing.pad,
182
- paddingTop: spacing.footerGap,
183
- borderTopColor: theme.colors.borderLight,
184
- },
185
- ]}
186
- >
187
- {footer}
188
- </View>
189
- ) : null}
190
- </View>
191
- </View>
192
- </Portal>
193
- );
194
- }
195
-
196
- function DialogHeader({
197
- title,
198
- showClose,
199
- padding,
200
- gap,
201
- closeAccessibilityLabel,
202
- onClose,
203
- }: {
204
- title?: string;
205
- showClose: boolean;
206
- padding: number;
207
- gap: number;
208
- closeAccessibilityLabel: string;
209
- onClose: () => void;
210
- }) {
211
- const theme = useTheme();
212
-
213
- return (
214
- <View
215
- style={[
216
- styles.header,
217
- { paddingHorizontal: padding, paddingTop: padding, gap },
218
- ]}
219
- >
220
- {title ? (
221
- <Text
222
- accessibilityRole="header"
223
- numberOfLines={2}
224
- style={[styles.title, { color: theme.colors.text }]}
225
- >
226
- {title}
227
- </Text>
228
- ) : (
229
- <View style={styles.titleSpacer} />
230
- )}
231
- {showClose ? (
232
- <CloseButton
233
- accessibilityLabel={closeAccessibilityLabel}
234
- onPress={onClose}
235
- />
236
- ) : null}
237
- </View>
238
- );
239
- }
240
-
241
- function CloseButton({
242
- accessibilityLabel,
243
- onPress,
244
- }: {
245
- accessibilityLabel: string;
246
- onPress: () => void;
247
- }) {
248
- const theme = useTheme();
249
- const { hovered, pressed, hoverHandlers, pressHandlers } = useInteractionStates();
250
-
251
- return (
252
- <Pressable
253
- onPress={onPress}
254
- {...hoverHandlers}
255
- {...pressHandlers}
256
- hitSlop={CLOSE_HIT_SLOP}
257
- accessibilityRole="button"
258
- accessibilityLabel={accessibilityLabel}
259
- style={[
260
- styles.closeButton,
261
- { backgroundColor: theme.colors.contrast50 },
262
- (hovered || pressed) && styles.closeButtonActive,
263
- ]}
264
- >
265
- <CloseIcon size="sm" fill={theme.colors.textSecondary} />
266
- </Pressable>
267
- );
268
- }
269
-
270
- const returnsTrue = () => true;
271
-
272
- function backdropAnimation(isClosing: boolean): ViewStyle {
273
- return isClosing
274
- ? ({ animation: `bloomCenteredDialogFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
275
- : ({ animation: 'bloomCenteredDialogFadeIn ease-out 150ms' } as ViewStyle);
276
- }
277
-
278
- function cardAnimation(isClosing: boolean): ViewStyle {
279
- return isClosing
280
- ? ({ animation: `bloomCenteredDialogZoomOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
281
- : ({ animation: 'bloomCenteredDialogZoomIn cubic-bezier(0.16, 1, 0.3, 1) 200ms' } as ViewStyle);
282
- }
283
-
284
- const KEYFRAMES_ID = 'bloom-centered-dialog-keyframes';
285
-
286
- /**
287
- * CSS keyframes powering the web enter/exit animation. Injected once into
288
- * `<head>` on first mount (keyed by id so re-mounts and multiple dialogs
289
- * don't duplicate the rule). Mirrors the native modal's `fade` feel with a
290
- * subtle zoom on the card.
291
- */
292
- export const BLOOM_CENTERED_DIALOG_CSS = `
293
- @keyframes bloomCenteredDialogFadeIn { from { opacity: 0; } to { opacity: 1; } }
294
- @keyframes bloomCenteredDialogFadeOut { from { opacity: 1; } to { opacity: 0; } }
295
- @keyframes bloomCenteredDialogZoomIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
296
- @keyframes bloomCenteredDialogZoomOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } }
297
- `;
298
-
299
- function useKeyframes(): void {
300
- useEffect(() => {
301
- if (typeof document === 'undefined') return;
302
- if (document.getElementById(KEYFRAMES_ID)) return;
303
- const style = document.createElement('style');
304
- style.id = KEYFRAMES_ID;
305
- style.textContent = BLOOM_CENTERED_DIALOG_CSS;
306
- document.head.appendChild(style);
307
- }, []);
308
- }
309
-
310
- const styles = StyleSheet.create({
311
- root: {
312
- position: 'fixed' as 'absolute',
313
- top: 0,
314
- left: 0,
315
- right: 0,
316
- bottom: 0,
317
- zIndex: dialogZIndex.backdrop,
318
- alignItems: 'center',
319
- justifyContent: 'center',
320
- pointerEvents: 'auto',
321
- },
322
- backdrop: {
323
- position: 'fixed' as 'absolute',
324
- top: 0,
325
- left: 0,
326
- right: 0,
327
- bottom: 0,
328
- },
329
- card: {
330
- position: 'relative',
331
- width: '100%',
332
- borderRadius: CARD_RADIUS,
333
- borderWidth: 1,
334
- overflow: 'hidden',
335
- shadowOpacity: 0.18,
336
- shadowRadius: 24,
337
- shadowOffset: { width: 0, height: 8 },
338
- zIndex: dialogZIndex.surface,
339
- },
340
- header: {
341
- flexDirection: 'row',
342
- alignItems: 'flex-start',
343
- justifyContent: 'space-between',
344
- },
345
- title: {
346
- flex: 1,
347
- fontSize: 17,
348
- fontWeight: '700',
349
- lineHeight: 22,
350
- },
351
- titleSpacer: {
352
- flex: 1,
353
- },
354
- closeButton: {
355
- width: 28,
356
- height: 28,
357
- borderRadius: 14,
358
- alignItems: 'center',
359
- justifyContent: 'center',
360
- cursor: 'pointer',
361
- },
362
- closeButtonActive: {
363
- opacity: 0.7,
364
- },
365
- bodyScroll: {
366
- flexGrow: 0,
367
- flexShrink: 1,
368
- },
369
- footer: {
370
- borderTopWidth: 1,
371
- },
372
- });