@astryxdesign/core 0.4.3 → 0.4.4-canary.0e8af82

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 (254) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +79 -45
  3. package/dist/Banner/Banner.d.ts +3 -0
  4. package/dist/Banner/Banner.d.ts.map +1 -1
  5. package/dist/Banner/Banner.js +13 -3
  6. package/dist/BottomSheet/BottomSheet.d.ts +75 -0
  7. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  8. package/dist/BottomSheet/BottomSheet.js +396 -0
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts +63 -0
  10. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
  11. package/dist/BottomSheet/BottomSheetPanel.js +441 -0
  12. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
  13. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
  14. package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
  15. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
  16. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
  17. package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
  18. package/dist/BottomSheet/index.d.ts +11 -0
  19. package/dist/BottomSheet/index.d.ts.map +1 -0
  20. package/dist/BottomSheet/index.js +12 -0
  21. package/dist/BottomSheet/snapOffsets.d.ts +109 -0
  22. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
  23. package/dist/BottomSheet/snapOffsets.js +206 -0
  24. package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
  25. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
  26. package/dist/BottomSheet/useMobileKeyboard.js +373 -0
  27. package/dist/BottomSheet/useSheetGestures.d.ts +185 -0
  28. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  29. package/dist/BottomSheet/useSheetGestures.js +1038 -0
  30. package/dist/Calendar/Calendar.d.ts +17 -0
  31. package/dist/Calendar/Calendar.d.ts.map +1 -1
  32. package/dist/Calendar/Calendar.js +25 -1
  33. package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
  34. package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
  35. package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
  36. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  37. package/dist/Chat/ChatComposerInput.js +3 -4
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +9 -0
  40. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  41. package/dist/ComplexSelector/ComplexSelector.js +18 -7
  42. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  43. package/dist/ContextMenu/ContextMenu.js +4 -2
  44. package/dist/DateInput/DateInput.d.ts.map +1 -1
  45. package/dist/DateInput/DateInput.js +15 -2
  46. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  47. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  48. package/dist/DateRangeInput/DateRangeInput.js +38 -3
  49. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  50. package/dist/DateTimeInput/DateTimeInput.js +38 -3
  51. package/dist/Dialog/Dialog.d.ts.map +1 -1
  52. package/dist/Dialog/Dialog.js +2 -1
  53. package/dist/Field/FieldLabel.d.ts +2 -1
  54. package/dist/Field/FieldLabel.d.ts.map +1 -1
  55. package/dist/Field/FieldLabel.js +21 -3
  56. package/dist/Field/InputClearButton.d.ts +2 -1
  57. package/dist/Field/InputClearButton.d.ts.map +1 -1
  58. package/dist/Field/InputClearButton.js +6 -1
  59. package/dist/FormLayout/FormLayout.d.ts +24 -2
  60. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  61. package/dist/FormLayout/FormLayout.js +7 -2
  62. package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
  63. package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
  64. package/dist/FormLayout/FormLayoutContext.js +19 -4
  65. package/dist/FormLayout/index.d.ts +1 -1
  66. package/dist/FormLayout/index.d.ts.map +1 -1
  67. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  68. package/dist/Lightbox/Lightbox.js +9 -3
  69. package/dist/MobileNav/MobileNav.d.ts +12 -0
  70. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  71. package/dist/MobileNav/MobileNav.js +102 -30
  72. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  73. package/dist/MultiSelector/MultiSelector.js +27 -8
  74. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  75. package/dist/NumberInput/NumberInput.js +15 -2
  76. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  77. package/dist/RadioList/RadioList.d.ts.map +1 -1
  78. package/dist/RadioList/RadioList.js +12 -1
  79. package/dist/Selector/Selector.d.ts.map +1 -1
  80. package/dist/Selector/Selector.js +20 -4
  81. package/dist/StatusDot/StatusDot.d.ts +43 -3
  82. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  83. package/dist/StatusDot/StatusDot.js +42 -5
  84. package/dist/Switch/Switch.d.ts.map +1 -1
  85. package/dist/Switch/Switch.js +14 -1
  86. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  87. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  88. package/dist/TextArea/TextArea.d.ts.map +1 -1
  89. package/dist/TextArea/TextArea.js +6 -1
  90. package/dist/TextInput/TextInput.d.ts.map +1 -1
  91. package/dist/TextInput/TextInput.js +6 -1
  92. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  93. package/dist/TimeInput/TimeInput.js +14 -2
  94. package/dist/Tokenizer/Tokenizer.js +11 -5
  95. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  96. package/dist/Tooltip/useTooltip.js +4 -1
  97. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  98. package/dist/Typeahead/BaseTypeahead.js +5 -3
  99. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  100. package/dist/Typeahead/Typeahead.js +10 -1
  101. package/dist/astryx.css +21 -0
  102. package/dist/hooks/index.d.ts +8 -1
  103. package/dist/hooks/index.d.ts.map +1 -1
  104. package/dist/hooks/index.js +8 -1
  105. package/dist/hooks/useFocusTrap.d.ts +0 -11
  106. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  107. package/dist/hooks/useFocusTrap.js +1 -11
  108. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  109. package/dist/hooks/useInputStatusIcon.js +1 -0
  110. package/dist/hooks/useResolvedRequired.d.ts +19 -0
  111. package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
  112. package/dist/hooks/useResolvedRequired.js +40 -0
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1 -0
  116. package/dist/theme/defineTheme.d.ts +22 -4
  117. package/dist/theme/defineTheme.d.ts.map +1 -1
  118. package/dist/theme/expandColorScale.d.ts +17 -2
  119. package/dist/theme/expandColorScale.d.ts.map +1 -1
  120. package/dist/theme/expandColorScale.js +63 -33
  121. package/dist/utils/ime.d.ts +5 -0
  122. package/dist/utils/ime.d.ts.map +1 -0
  123. package/dist/utils/ime.js +63 -0
  124. package/dist/utils/index.d.ts +1 -0
  125. package/dist/utils/index.d.ts.map +1 -1
  126. package/dist/utils/index.js +2 -1
  127. package/dist/utils/plainDate.d.ts +6 -0
  128. package/dist/utils/plainDate.d.ts.map +1 -1
  129. package/dist/utils/plainDate.js +12 -0
  130. package/locales/af-ZA.json +958 -0
  131. package/locales/ar-SA.json +998 -0
  132. package/locales/ca-ES.json +974 -0
  133. package/locales/cs-CZ.json +990 -0
  134. package/locales/da-DK.json +966 -0
  135. package/locales/de-DE.json +966 -0
  136. package/locales/el-GR.json +990 -0
  137. package/locales/en.json +52 -0
  138. package/locales/es-ES.json +978 -0
  139. package/locales/fi-FI.json +990 -0
  140. package/locales/fr-FR.json +928 -0
  141. package/locales/he-IL.json +990 -0
  142. package/locales/hu-HU.json +990 -0
  143. package/locales/it-IT.json +978 -0
  144. package/locales/ja-JP.json +998 -0
  145. package/locales/ko-KR.json +990 -0
  146. package/locales/nl-NL.json +954 -0
  147. package/locales/no-NO.json +978 -0
  148. package/locales/pl-PL.json +982 -0
  149. package/locales/pseudo.json +39 -0
  150. package/locales/pt-BR.json +982 -0
  151. package/locales/pt-PT.json +982 -0
  152. package/locales/ro-RO.json +982 -0
  153. package/locales/ru-RU.json +990 -0
  154. package/locales/sr-SP.json +990 -0
  155. package/locales/sv-SE.json +986 -0
  156. package/locales/tr-TR.json +990 -0
  157. package/locales/uk-UA.json +990 -0
  158. package/locales/vi-VN.json +986 -0
  159. package/locales/zh-CN.json +998 -0
  160. package/locales/zh-TW.json +998 -0
  161. package/package.json +9 -8
  162. package/src/Banner/Banner.doc.mjs +3 -3
  163. package/src/Banner/Banner.test.tsx +40 -0
  164. package/src/Banner/Banner.tsx +27 -2
  165. package/src/BottomSheet/BottomSheet.doc.mjs +299 -0
  166. package/src/BottomSheet/BottomSheet.test.tsx +2265 -0
  167. package/src/BottomSheet/BottomSheet.tsx +559 -0
  168. package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
  169. package/src/BottomSheet/BottomSheetPanel.tsx +601 -0
  170. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
  171. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
  172. package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
  173. package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
  174. package/src/BottomSheet/index.ts +19 -0
  175. package/src/BottomSheet/snapOffsets.test.ts +254 -0
  176. package/src/BottomSheet/snapOffsets.ts +245 -0
  177. package/src/BottomSheet/useMobileKeyboard.ts +499 -0
  178. package/src/BottomSheet/useSheetGestures.test.ts +1112 -0
  179. package/src/BottomSheet/useSheetGestures.ts +1503 -0
  180. package/src/Calendar/Calendar.doc.mjs +16 -0
  181. package/src/Calendar/Calendar.test.tsx +96 -0
  182. package/src/Calendar/Calendar.tsx +55 -0
  183. package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
  184. package/src/Chat/ChatComposerInput.tsx +3 -4
  185. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  186. package/src/ComplexSelector/ComplexSelector.tsx +15 -6
  187. package/src/ContextMenu/ContextMenu.tsx +4 -2
  188. package/src/DateInput/DateInput.test.tsx +21 -0
  189. package/src/DateInput/DateInput.tsx +12 -2
  190. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  191. package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
  192. package/src/DateRangeInput/DateRangeInput.tsx +72 -1
  193. package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
  194. package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
  195. package/src/DateTimeInput/DateTimeInput.tsx +43 -4
  196. package/src/Dialog/Dialog.tsx +2 -1
  197. package/src/Field/Field.doc.mjs +1 -0
  198. package/src/Field/FieldLabel.tsx +20 -4
  199. package/src/Field/InputClearButton.test.tsx +25 -0
  200. package/src/Field/InputClearButton.tsx +4 -1
  201. package/src/FileInput/FileInput.test.tsx +53 -9
  202. package/src/FormLayout/FormLayout.doc.mjs +13 -0
  203. package/src/FormLayout/FormLayout.test.tsx +181 -4
  204. package/src/FormLayout/FormLayout.tsx +33 -2
  205. package/src/FormLayout/FormLayoutContext.ts +22 -7
  206. package/src/FormLayout/index.ts +1 -1
  207. package/src/Lightbox/Lightbox.test.tsx +49 -28
  208. package/src/Lightbox/Lightbox.tsx +7 -3
  209. package/src/MobileNav/MobileNav.tsx +128 -37
  210. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
  211. package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
  212. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
  213. package/src/MultiSelector/MultiSelector.test.tsx +195 -51
  214. package/src/MultiSelector/MultiSelector.tsx +25 -9
  215. package/src/NumberInput/NumberInput.test.tsx +18 -0
  216. package/src/NumberInput/NumberInput.tsx +12 -2
  217. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  218. package/src/RadioList/RadioList.tsx +9 -1
  219. package/src/Selector/Selector.test.tsx +85 -26
  220. package/src/Selector/Selector.tsx +16 -5
  221. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  222. package/src/StatusDot/StatusDot.test.tsx +115 -8
  223. package/src/StatusDot/StatusDot.tsx +74 -9
  224. package/src/Switch/Switch.test.tsx +16 -0
  225. package/src/Switch/Switch.tsx +13 -0
  226. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  227. package/src/TextArea/TextArea.tsx +3 -1
  228. package/src/TextInput/TextInput.tsx +3 -1
  229. package/src/TimeInput/TimeInput.test.tsx +23 -0
  230. package/src/TimeInput/TimeInput.tsx +11 -1
  231. package/src/Tokenizer/Tokenizer.test.tsx +50 -32
  232. package/src/Tokenizer/Tokenizer.tsx +5 -5
  233. package/src/Tooltip/useTooltip.tsx +4 -1
  234. package/src/Typeahead/BaseTypeahead.tsx +10 -3
  235. package/src/Typeahead/Typeahead.test.tsx +116 -7
  236. package/src/Typeahead/Typeahead.tsx +10 -1
  237. package/src/__tests__/stubMatchMedia.test.ts +79 -0
  238. package/src/__tests__/stubMatchMedia.ts +95 -0
  239. package/src/hooks/index.ts +9 -5
  240. package/src/hooks/useFocusTrap.ts +1 -14
  241. package/src/hooks/useInputStatusIcon.test.tsx +19 -0
  242. package/src/hooks/useInputStatusIcon.tsx +7 -0
  243. package/src/hooks/useResolvedRequired.ts +42 -0
  244. package/src/index.ts +1 -0
  245. package/src/theme/defineTheme.ts +22 -4
  246. package/src/theme/expandColorScale.test.ts +138 -0
  247. package/src/theme/expandColorScale.ts +89 -44
  248. package/src/utils/ime.test.ts +32 -0
  249. package/src/utils/ime.ts +69 -0
  250. package/src/utils/index.ts +2 -0
  251. package/src/utils/plainDate.test.ts +50 -0
  252. package/src/utils/plainDate.ts +12 -0
  253. package/dist/astryx.umd.js +0 -147
  254. package/dist/astryx.umd.js.map +0 -7
@@ -0,0 +1,559 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file BottomSheet.tsx
7
+ * @input Uses React, StyleX, core hooks/utils, BottomSheetPanel, BottomSheetSwitcherContext
8
+ * @output Exports BottomSheet component and BottomSheetProps
9
+ * @position Public BottomSheet router plus private standalone/switcher hosts
10
+ *
11
+ * BottomSheet selects one of two focused hosts. A standalone host owns its
12
+ * native dialog lifecycle; a switcher item participates in the parent's shared
13
+ * dialog and transition state machine. Both render the same BottomSheetPanel,
14
+ * which owns sheet presentation, gestures, mobile-keyboard accommodation, and
15
+ * motion completion.
16
+ *
17
+ * SYNC: When modified, update these files to stay in sync:
18
+ * - /packages/core/src/BottomSheet/BottomSheetPanel.tsx
19
+ * - /packages/core/src/BottomSheet/BottomSheet.doc.mjs
20
+ * - /packages/core/src/BottomSheet/BottomSheet.test.tsx
21
+ * - /packages/core/src/BottomSheet/BottomSheetSwitcher.tsx
22
+ * - /packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx
23
+ * - /apps/storybook/stories/BottomSheet.stories.tsx
24
+ * - /packages/cli/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.tsx
25
+ */
26
+
27
+ import {
28
+ use,
29
+ useCallback,
30
+ useEffect,
31
+ useLayoutEffect,
32
+ useRef,
33
+ useState,
34
+ type ReactNode,
35
+ } from 'react';
36
+ import * as stylex from '@stylexjs/stylex';
37
+ import type {BaseProps} from '../BaseProps';
38
+ import type {DialogPurpose} from '../Dialog';
39
+ import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
40
+ import {useDevWarning, useScrollLock} from '../hooks';
41
+ import {
42
+ BottomSheetPanel,
43
+ type BottomSheetPanelMotion,
44
+ type BottomSheetPanelState,
45
+ } from './BottomSheetPanel';
46
+ import {
47
+ BottomSheetSwitcherContext,
48
+ type BottomSheetSwitcherContextValue,
49
+ type BottomSheetSwitcherPhase,
50
+ } from './BottomSheetSwitcherContext';
51
+
52
+ export type {BottomSheetHeight, BottomSheetSnapPoint} from './BottomSheetPanel';
53
+ import type {BottomSheetHeight, BottomSheetSnapPoint} from './BottomSheetPanel';
54
+
55
+ const styles = stylex.create({
56
+ dialog: {
57
+ position: 'fixed',
58
+ inset: 0,
59
+ width: '100dvw',
60
+ height: '100dvh',
61
+ maxWidth: 'none',
62
+ maxHeight: 'none',
63
+ margin: 0,
64
+ padding: 0,
65
+ border: 'none',
66
+ backgroundColor: 'transparent',
67
+ overflow: 'visible',
68
+ display: 'none',
69
+ outline: 'none',
70
+ },
71
+ dialogOpen: {
72
+ display: 'block',
73
+ },
74
+ dialogNonModal: {
75
+ pointerEvents: 'none',
76
+ zIndex: 1000,
77
+ width: '100%',
78
+ height: '100%',
79
+ },
80
+ scrim: {
81
+ '::backdrop': {
82
+ backgroundColor: colorVars['--color-overlay'],
83
+ opacity: {
84
+ default: 'var(--_sheet-scrim-opacity, 1)',
85
+ '@starting-style': 0,
86
+ },
87
+ transitionProperty: 'opacity, display',
88
+ transitionDuration: durationVars['--duration-medium'],
89
+ transitionTimingFunction: easeVars['--ease-standard'],
90
+ transitionBehavior: 'allow-discrete',
91
+ '@media (prefers-reduced-motion: reduce)': {
92
+ transitionDuration: '0.01s',
93
+ },
94
+ },
95
+ },
96
+ positioner: {
97
+ position: 'absolute',
98
+ insetInline: 0,
99
+ insetBlockEnd: 0,
100
+ display: 'flex',
101
+ justifyContent: 'center',
102
+ pointerEvents: 'none',
103
+ },
104
+ positionerHidden: {
105
+ display: 'none',
106
+ },
107
+ positionerTop: {
108
+ zIndex: 1,
109
+ },
110
+ });
111
+
112
+ interface BottomSheetSharedProps extends BaseProps<HTMLDivElement> {
113
+ /** Ref forwarded to the visual sheet panel <div>. */
114
+ ref?: React.Ref<HTMLDivElement>;
115
+
116
+ /** Accessible label for the sheet. */
117
+ label: string;
118
+
119
+ /** Sheet content, rendered below the grab handle in a scrollable area. */
120
+ children: ReactNode;
121
+
122
+ /** Height budget or custom CSS length. Only fully expanded Tall is keyboard-aware. @default 'capped' */
123
+ height?: BottomSheetHeight | number | string;
124
+
125
+ /**
126
+ * Extra heights the sheet can rest at when dragged; its own height is always
127
+ * the tallest stop, and omitting this gives a sheet that only opens and
128
+ * closes. Each stop is the sheet's visible height: a number is a viewport
129
+ * fraction (`0.5` is half the screen), `'50%'` the same in CSS, `'320px'` an
130
+ * absolute length. A stop of a quarter of the sheet or less is a peek — it
131
+ * slides away instead of reflowing, and thins the scrim.
132
+ */
133
+ snapPoints?: ReadonlyArray<BottomSheetSnapPoint>;
134
+
135
+ /**
136
+ * Configures implicit dismissal behavior, matching Dialog.
137
+ * - required: Blocks swipe, scrim click, and Escape
138
+ * - form: Blocks swipe and scrim click, allows Escape
139
+ * - info: Allows swipe, scrim click, and Escape
140
+ * @default 'info'
141
+ */
142
+ purpose?: DialogPurpose;
143
+ }
144
+
145
+ interface StandaloneBottomSheetProps extends BottomSheetSharedProps {
146
+ isOpen: boolean;
147
+ onOpenChange: (isOpen: boolean) => void;
148
+ hasScrim?: boolean;
149
+ sheetId?: never;
150
+ }
151
+
152
+ interface SwitcherBottomSheetProps extends BottomSheetSharedProps {
153
+ sheetId: string;
154
+ isOpen?: never;
155
+ onOpenChange?: never;
156
+ hasScrim?: never;
157
+ }
158
+
159
+ export type BottomSheetProps =
160
+ StandaloneBottomSheetProps | SwitcherBottomSheetProps;
161
+
162
+ function panelStateForSwitcherPhase(
163
+ phase: BottomSheetSwitcherPhase,
164
+ alignmentOffset: number,
165
+ ): BottomSheetPanelState {
166
+ switch (phase) {
167
+ case 'active':
168
+ return {kind: 'open', entering: false};
169
+ case 'entering':
170
+ return {kind: 'open', entering: true};
171
+ case 'covered':
172
+ case 'aligning':
173
+ case 'fading':
174
+ return {kind: 'retained', motion: phase, alignmentOffset};
175
+ case 'exiting':
176
+ return {kind: 'exiting'};
177
+ case 'hidden':
178
+ return {kind: 'hidden'};
179
+ }
180
+ }
181
+
182
+ // preventScroll on both: presenting a sheet must not scroll the page to reveal
183
+ // what it just focused. For an autofocused field that reveal is the mobile
184
+ // keyboard's, and it moves the document under a fixed sheet; useMobileKeyboard
185
+ // brings the field into view within the sheet instead.
186
+ function focusPanel(panel: HTMLElement | null, isModal: boolean): void {
187
+ const activeElement = document.activeElement;
188
+ if (activeElement != null && panel?.contains(activeElement)) {
189
+ return;
190
+ }
191
+ const autofocus = panel?.querySelector<HTMLElement>('[data-autofocus]');
192
+ if (autofocus != null) {
193
+ autofocus.focus({preventScroll: true});
194
+ } else if (isModal) {
195
+ panel?.focus({preventScroll: true});
196
+ }
197
+ }
198
+
199
+ function StandaloneBottomSheet({
200
+ ref,
201
+ isOpen,
202
+ onOpenChange,
203
+ label,
204
+ children,
205
+ height = 'capped',
206
+ snapPoints,
207
+ hasScrim = true,
208
+ purpose = 'info',
209
+ xstyle,
210
+ ...props
211
+ }: StandaloneBottomSheetProps) {
212
+ const dialogRef = useRef<HTMLDialogElement | null>(null);
213
+ const panelRef = useRef<HTMLDivElement | null>(null);
214
+ const triggerRef = useRef<HTMLElement | null>(null);
215
+ const [isPresented, setIsPresented] = useState(isOpen);
216
+ const shouldPresent = isOpen || isPresented;
217
+ const panelState: BottomSheetPanelState = isOpen
218
+ ? {kind: 'open', entering: false}
219
+ : isPresented
220
+ ? {kind: 'exiting'}
221
+ : {kind: 'hidden'};
222
+
223
+ const dismissOnEscape = useCallback(() => {
224
+ if (purpose !== 'required') {
225
+ onOpenChange(false);
226
+ }
227
+ }, [onOpenChange, purpose]);
228
+ const dismissOnLightInteraction = useCallback(() => {
229
+ if (purpose === 'info') {
230
+ onOpenChange(false);
231
+ }
232
+ }, [onOpenChange, purpose]);
233
+ const handlePanelElementChange = useCallback(
234
+ (element: HTMLDivElement | null) => {
235
+ panelRef.current = element;
236
+ },
237
+ [],
238
+ );
239
+ const handleScrimOpacity = useCallback((opacity: number) => {
240
+ dialogRef.current?.style.setProperty(
241
+ '--_sheet-scrim-opacity',
242
+ String(opacity),
243
+ );
244
+ }, []);
245
+
246
+ useEffect(() => {
247
+ const dialog = dialogRef.current;
248
+ if (dialog == null || !isOpen) {
249
+ return;
250
+ }
251
+
252
+ // The controlled prop opens an already-mounted dialog; presentation state
253
+ // must latch here so a later close can keep it mounted through its exit.
254
+ // eslint-disable-next-line @eslint-react/set-state-in-effect -- latches controlled open state for exit animation
255
+ setIsPresented(true);
256
+ dialog.style.setProperty('--_sheet-scrim-opacity', '1');
257
+ const wasOpen = dialog.open;
258
+ if (!wasOpen) {
259
+ if (hasScrim) {
260
+ triggerRef.current = document.activeElement as HTMLElement | null;
261
+ dialog.showModal();
262
+ } else {
263
+ dialog.show();
264
+ }
265
+ focusPanel(panelRef.current, hasScrim);
266
+ }
267
+ }, [hasScrim, isOpen]);
268
+
269
+ useEffect(() => {
270
+ if (!isOpen && isPresented && hasScrim) {
271
+ handleScrimOpacity(0);
272
+ }
273
+ }, [handleScrimOpacity, hasScrim, isOpen, isPresented]);
274
+
275
+ const handleMotionComplete = useCallback(
276
+ (motion: BottomSheetPanelMotion) => {
277
+ if (motion !== 'exiting' || isOpen) {
278
+ return;
279
+ }
280
+ const dialog = dialogRef.current;
281
+ if (dialog?.open) {
282
+ dialog.close();
283
+ }
284
+ setIsPresented(false);
285
+ triggerRef.current?.focus();
286
+ triggerRef.current = null;
287
+ },
288
+ [isOpen],
289
+ );
290
+
291
+ useScrollLock(shouldPresent && hasScrim);
292
+ useDevWarning(
293
+ 'BottomSheet',
294
+ 'requires a non-empty `label` for an accessible name; the open sheet ' +
295
+ 'has no built-in heading to derive one from.',
296
+ isOpen && !label,
297
+ );
298
+
299
+ const handleCancel = useCallback(
300
+ (event: React.SyntheticEvent<HTMLDialogElement>) => {
301
+ event.preventDefault();
302
+ dismissOnEscape();
303
+ },
304
+ [dismissOnEscape],
305
+ );
306
+ const handleKeyDown = useCallback(
307
+ (event: React.KeyboardEvent<HTMLDialogElement>) => {
308
+ if (event.key === 'Escape') {
309
+ event.preventDefault();
310
+ dismissOnEscape();
311
+ }
312
+ },
313
+ [dismissOnEscape],
314
+ );
315
+ const handleClick = useCallback(
316
+ (event: React.MouseEvent<HTMLDialogElement>) => {
317
+ if (hasScrim && event.target === event.currentTarget) {
318
+ dismissOnLightInteraction();
319
+ }
320
+ },
321
+ [dismissOnLightInteraction, hasScrim],
322
+ );
323
+
324
+ return (
325
+ <dialog
326
+ {...stylex.props(
327
+ styles.dialog,
328
+ shouldPresent && styles.dialogOpen,
329
+ hasScrim && styles.scrim,
330
+ !hasScrim && styles.dialogNonModal,
331
+ )}
332
+ ref={dialogRef}
333
+ aria-label={label}
334
+ aria-hidden={!isOpen && isPresented ? 'true' : undefined}
335
+ aria-modal={hasScrim && isOpen ? 'true' : undefined}
336
+ role={purpose === 'required' ? 'alertdialog' : undefined}
337
+ inert={!isOpen && isPresented ? true : undefined}
338
+ onCancel={handleCancel}
339
+ onClick={handleClick}
340
+ onKeyDown={handleKeyDown}>
341
+ <div {...stylex.props(styles.positioner)}>
342
+ <BottomSheetPanel
343
+ {...props}
344
+ ref={ref}
345
+ state={panelState}
346
+ height={height}
347
+ snapPoints={snapPoints}
348
+ isSwipeDismissAllowed={purpose === 'info'}
349
+ isPageScrollLocked={shouldPresent && hasScrim}
350
+ xstyle={xstyle}
351
+ onDismiss={dismissOnLightInteraction}
352
+ onScrimOpacity={handleScrimOpacity}
353
+ onElementChange={handlePanelElementChange}
354
+ onMotionComplete={handleMotionComplete}>
355
+ {children}
356
+ </BottomSheetPanel>
357
+ </div>
358
+ </dialog>
359
+ );
360
+ }
361
+
362
+ interface SwitcherBottomSheetItemProps extends SwitcherBottomSheetProps {
363
+ switcher: BottomSheetSwitcherContextValue;
364
+ }
365
+
366
+ function SwitcherBottomSheetItem({
367
+ switcher,
368
+ ref,
369
+ sheetId,
370
+ label,
371
+ children,
372
+ height = 'capped',
373
+ snapPoints,
374
+ purpose = 'info',
375
+ xstyle,
376
+ ...props
377
+ }: SwitcherBottomSheetItemProps) {
378
+ const {
379
+ activeSheet,
380
+ hasScrim,
381
+ onActiveSheetChange,
382
+ getSheetPhase,
383
+ getSheetAlignmentOffset,
384
+ registerSheetElement,
385
+ registerSheetLabel,
386
+ registerSheetPurpose,
387
+ onSheetEnterStart,
388
+ onSheetTransitionComplete,
389
+ onSheetScrimOpacityChange,
390
+ } = switcher;
391
+ const hasValidSheetId = typeof sheetId === 'string' && sheetId.length > 0;
392
+ const phase = hasValidSheetId ? getSheetPhase(sheetId) : 'hidden';
393
+ const alignmentOffset = hasValidSheetId
394
+ ? getSheetAlignmentOffset(sheetId)
395
+ : 0;
396
+ const panelState = panelStateForSwitcherPhase(phase, alignmentOffset);
397
+ const isInteractive = phase === 'active' || phase === 'entering';
398
+ const isInactive =
399
+ phase === 'covered' ||
400
+ phase === 'aligning' ||
401
+ phase === 'fading' ||
402
+ phase === 'exiting';
403
+ const isPresented = phase !== 'hidden';
404
+ const isTopSheet = phase === 'active' || phase === 'entering';
405
+ const panelRef = useRef<HTMLDivElement | null>(null);
406
+ const previousPhaseRef = useRef(phase);
407
+ const previousPhase = previousPhaseRef.current;
408
+ const hasPresentedRef = useRef(false);
409
+
410
+ useLayoutEffect(() => {
411
+ previousPhaseRef.current = phase;
412
+ }, [phase]);
413
+
414
+ const dismissOnSwipe = useCallback(() => {
415
+ if (purpose === 'info' && hasValidSheetId && activeSheet === sheetId) {
416
+ onActiveSheetChange(null);
417
+ }
418
+ }, [activeSheet, hasValidSheetId, onActiveSheetChange, purpose, sheetId]);
419
+ const handlePanelElementChange = useCallback(
420
+ (element: HTMLDivElement | null) => {
421
+ panelRef.current = element;
422
+ if (hasValidSheetId) {
423
+ registerSheetElement(sheetId, element);
424
+ }
425
+ },
426
+ [hasValidSheetId, registerSheetElement, sheetId],
427
+ );
428
+ const handleMotionStart = useCallback(
429
+ (motion: BottomSheetPanelMotion) => {
430
+ if (motion === 'entering' && hasValidSheetId) {
431
+ onSheetEnterStart(sheetId);
432
+ }
433
+ },
434
+ [hasValidSheetId, onSheetEnterStart, sheetId],
435
+ );
436
+ const handleMotionComplete = useCallback(
437
+ (motion: BottomSheetPanelMotion) => {
438
+ if (hasValidSheetId) {
439
+ onSheetTransitionComplete({sheetId, phase: motion});
440
+ }
441
+ },
442
+ [hasValidSheetId, onSheetTransitionComplete, sheetId],
443
+ );
444
+ const handleScrimOpacity = useCallback(
445
+ (opacity: number) => {
446
+ if (hasValidSheetId) {
447
+ onSheetScrimOpacityChange(sheetId, opacity);
448
+ }
449
+ },
450
+ [hasValidSheetId, onSheetScrimOpacityChange, sheetId],
451
+ );
452
+
453
+ useLayoutEffect(() => {
454
+ if (!hasValidSheetId) {
455
+ return;
456
+ }
457
+ registerSheetLabel(sheetId, label);
458
+ return () => registerSheetLabel(sheetId, null);
459
+ }, [hasValidSheetId, label, registerSheetLabel, sheetId]);
460
+
461
+ useLayoutEffect(() => {
462
+ if (!hasValidSheetId) {
463
+ return;
464
+ }
465
+ registerSheetPurpose(sheetId, purpose);
466
+ return () => registerSheetPurpose(sheetId, null);
467
+ }, [hasValidSheetId, purpose, registerSheetPurpose, sheetId]);
468
+
469
+ useEffect(() => {
470
+ if (isInteractive) {
471
+ const wasInteractive =
472
+ previousPhase === 'active' || previousPhase === 'entering';
473
+ if (!hasPresentedRef.current || !wasInteractive) {
474
+ focusPanel(panelRef.current, hasScrim);
475
+ }
476
+ hasPresentedRef.current = true;
477
+ } else if (phase === 'hidden') {
478
+ hasPresentedRef.current = false;
479
+ }
480
+ }, [hasScrim, isInteractive, phase, previousPhase]);
481
+
482
+ useDevWarning(
483
+ 'BottomSheet',
484
+ 'requires a non-empty `label` for an accessible name; the open sheet ' +
485
+ 'has no built-in heading to derive one from.',
486
+ isInteractive && !label,
487
+ );
488
+
489
+ return (
490
+ <div
491
+ {...stylex.props(
492
+ styles.positioner,
493
+ !isPresented && styles.positionerHidden,
494
+ isTopSheet && styles.positionerTop,
495
+ )}
496
+ hidden={!isPresented}
497
+ aria-hidden={isInactive ? 'true' : undefined}
498
+ inert={isInactive ? true : undefined}>
499
+ <BottomSheetPanel
500
+ {...props}
501
+ ref={ref}
502
+ state={panelState}
503
+ height={height}
504
+ snapPoints={snapPoints}
505
+ isSwipeDismissAllowed={purpose === 'info'}
506
+ isPageScrollLocked={hasScrim}
507
+ xstyle={xstyle}
508
+ onDismiss={dismissOnSwipe}
509
+ onScrimOpacity={handleScrimOpacity}
510
+ onElementChange={handlePanelElementChange}
511
+ onMotionStart={handleMotionStart}
512
+ onMotionComplete={handleMotionComplete}>
513
+ {/* A switcher item consumes its parent controller. Its content starts a
514
+ fresh ownership scope so a nested BottomSheet is standalone unless
515
+ it establishes a nested BottomSheetSwitcher of its own. */}
516
+ <BottomSheetSwitcherContext value={null}>
517
+ {children}
518
+ </BottomSheetSwitcherContext>
519
+ </BottomSheetPanel>
520
+ </div>
521
+ );
522
+ }
523
+
524
+ /**
525
+ * A mobile touch sheet that either owns a native dialog or participates in a
526
+ * BottomSheetSwitcher shared dialog when given a sheetId inside that context.
527
+ */
528
+ export function BottomSheet(props: BottomSheetProps) {
529
+ const switcher = use(BottomSheetSwitcherContext);
530
+ const runtimeSheetId = (props as {sheetId?: string}).sheetId;
531
+ const hasValidSheetId =
532
+ typeof runtimeSheetId === 'string' && runtimeSheetId.length > 0;
533
+
534
+ useDevWarning(
535
+ 'BottomSheet',
536
+ 'requires a non-empty `sheetId` when nested in ' +
537
+ 'BottomSheetSwitcher; standalone `isOpen` / `onOpenChange` props are ' +
538
+ 'ignored there.',
539
+ switcher != null && !hasValidSheetId,
540
+ );
541
+ useDevWarning(
542
+ 'BottomSheet',
543
+ '`sheetId` only works inside BottomSheetSwitcher. Use `isOpen` and ' +
544
+ '`onOpenChange` for a standalone sheet.',
545
+ switcher == null && runtimeSheetId != null,
546
+ );
547
+
548
+ if (switcher != null) {
549
+ return (
550
+ <SwitcherBottomSheetItem
551
+ {...(props as SwitcherBottomSheetProps)}
552
+ switcher={switcher}
553
+ />
554
+ );
555
+ }
556
+ return <StandaloneBottomSheet {...(props as StandaloneBottomSheetProps)} />;
557
+ }
558
+
559
+ BottomSheet.displayName = 'BottomSheet';