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

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 (200) 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 +66 -0
  7. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  8. package/dist/BottomSheet/BottomSheet.js +392 -0
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts +60 -0
  10. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
  11. package/dist/BottomSheet/BottomSheetPanel.js +398 -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 +72 -0
  22. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
  23. package/dist/BottomSheet/snapOffsets.js +130 -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 +180 -0
  28. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  29. package/dist/BottomSheet/useSheetGestures.js +989 -0
  30. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  31. package/dist/Chat/ChatComposerInput.js +3 -4
  32. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  33. package/dist/ComplexSelector/ComplexSelector.js +12 -6
  34. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  35. package/dist/ContextMenu/ContextMenu.js +4 -2
  36. package/dist/DateInput/DateInput.d.ts.map +1 -1
  37. package/dist/DateInput/DateInput.js +9 -1
  38. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  39. package/dist/DateTimeInput/DateTimeInput.js +31 -1
  40. package/dist/Dialog/Dialog.d.ts.map +1 -1
  41. package/dist/Dialog/Dialog.js +2 -1
  42. package/dist/Field/InputClearButton.d.ts +2 -1
  43. package/dist/Field/InputClearButton.d.ts.map +1 -1
  44. package/dist/Field/InputClearButton.js +6 -1
  45. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  46. package/dist/Lightbox/Lightbox.js +9 -3
  47. package/dist/MobileNav/MobileNav.d.ts +12 -0
  48. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  49. package/dist/MobileNav/MobileNav.js +102 -30
  50. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  51. package/dist/MultiSelector/MultiSelector.js +21 -7
  52. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  53. package/dist/NumberInput/NumberInput.js +9 -1
  54. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  55. package/dist/Selector/Selector.d.ts.map +1 -1
  56. package/dist/Selector/Selector.js +14 -3
  57. package/dist/StatusDot/StatusDot.d.ts +43 -3
  58. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  59. package/dist/StatusDot/StatusDot.js +42 -5
  60. package/dist/Switch/Switch.d.ts.map +1 -1
  61. package/dist/Switch/Switch.js +5 -1
  62. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  63. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  64. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  65. package/dist/TimeInput/TimeInput.js +8 -1
  66. package/dist/Tokenizer/Tokenizer.js +11 -5
  67. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  68. package/dist/Tooltip/useTooltip.js +4 -1
  69. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  70. package/dist/Typeahead/BaseTypeahead.js +5 -3
  71. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  72. package/dist/Typeahead/Typeahead.js +10 -1
  73. package/dist/astryx.css +21 -0
  74. package/dist/hooks/index.d.ts +8 -1
  75. package/dist/hooks/index.d.ts.map +1 -1
  76. package/dist/hooks/index.js +8 -1
  77. package/dist/hooks/useFocusTrap.d.ts +0 -11
  78. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  79. package/dist/hooks/useFocusTrap.js +1 -11
  80. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  81. package/dist/hooks/useInputStatusIcon.js +1 -0
  82. package/dist/index.d.ts +1 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +1 -0
  85. package/dist/theme/defineTheme.d.ts +22 -4
  86. package/dist/theme/defineTheme.d.ts.map +1 -1
  87. package/dist/theme/expandColorScale.d.ts +17 -2
  88. package/dist/theme/expandColorScale.d.ts.map +1 -1
  89. package/dist/theme/expandColorScale.js +63 -33
  90. package/dist/utils/ime.d.ts +5 -0
  91. package/dist/utils/ime.d.ts.map +1 -0
  92. package/dist/utils/ime.js +63 -0
  93. package/dist/utils/index.d.ts +1 -0
  94. package/dist/utils/index.d.ts.map +1 -1
  95. package/dist/utils/index.js +2 -1
  96. package/locales/af-ZA.json +958 -0
  97. package/locales/ar-SA.json +998 -0
  98. package/locales/ca-ES.json +974 -0
  99. package/locales/cs-CZ.json +990 -0
  100. package/locales/da-DK.json +966 -0
  101. package/locales/de-DE.json +966 -0
  102. package/locales/el-GR.json +990 -0
  103. package/locales/en.json +48 -0
  104. package/locales/es-ES.json +978 -0
  105. package/locales/fi-FI.json +990 -0
  106. package/locales/fr-FR.json +928 -0
  107. package/locales/he-IL.json +990 -0
  108. package/locales/hu-HU.json +990 -0
  109. package/locales/it-IT.json +978 -0
  110. package/locales/ja-JP.json +998 -0
  111. package/locales/ko-KR.json +990 -0
  112. package/locales/nl-NL.json +954 -0
  113. package/locales/no-NO.json +978 -0
  114. package/locales/pl-PL.json +982 -0
  115. package/locales/pseudo.json +36 -0
  116. package/locales/pt-BR.json +982 -0
  117. package/locales/pt-PT.json +982 -0
  118. package/locales/ro-RO.json +982 -0
  119. package/locales/ru-RU.json +990 -0
  120. package/locales/sr-SP.json +990 -0
  121. package/locales/sv-SE.json +986 -0
  122. package/locales/tr-TR.json +990 -0
  123. package/locales/uk-UA.json +990 -0
  124. package/locales/vi-VN.json +986 -0
  125. package/locales/zh-CN.json +998 -0
  126. package/locales/zh-TW.json +998 -0
  127. package/package.json +9 -8
  128. package/src/Banner/Banner.doc.mjs +3 -3
  129. package/src/Banner/Banner.test.tsx +40 -0
  130. package/src/Banner/Banner.tsx +27 -2
  131. package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
  132. package/src/BottomSheet/BottomSheet.test.tsx +2114 -0
  133. package/src/BottomSheet/BottomSheet.tsx +545 -0
  134. package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
  135. package/src/BottomSheet/BottomSheetPanel.tsx +547 -0
  136. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
  137. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
  138. package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
  139. package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
  140. package/src/BottomSheet/index.ts +15 -0
  141. package/src/BottomSheet/snapOffsets.test.ts +187 -0
  142. package/src/BottomSheet/snapOffsets.ts +159 -0
  143. package/src/BottomSheet/useMobileKeyboard.ts +499 -0
  144. package/src/BottomSheet/useSheetGestures.test.ts +1059 -0
  145. package/src/BottomSheet/useSheetGestures.ts +1459 -0
  146. package/src/Chat/ChatComposerInput.tsx +3 -4
  147. package/src/ComplexSelector/ComplexSelector.tsx +12 -5
  148. package/src/ContextMenu/ContextMenu.tsx +4 -2
  149. package/src/DateInput/DateInput.test.tsx +21 -0
  150. package/src/DateInput/DateInput.tsx +9 -1
  151. package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
  152. package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
  153. package/src/DateTimeInput/DateTimeInput.tsx +39 -2
  154. package/src/Dialog/Dialog.tsx +2 -1
  155. package/src/Field/Field.doc.mjs +1 -0
  156. package/src/Field/InputClearButton.test.tsx +25 -0
  157. package/src/Field/InputClearButton.tsx +4 -1
  158. package/src/FileInput/FileInput.test.tsx +53 -9
  159. package/src/Lightbox/Lightbox.test.tsx +49 -28
  160. package/src/Lightbox/Lightbox.tsx +7 -3
  161. package/src/MobileNav/MobileNav.tsx +128 -37
  162. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
  163. package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
  164. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
  165. package/src/MultiSelector/MultiSelector.test.tsx +195 -51
  166. package/src/MultiSelector/MultiSelector.tsx +22 -8
  167. package/src/NumberInput/NumberInput.test.tsx +18 -0
  168. package/src/NumberInput/NumberInput.tsx +9 -1
  169. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  170. package/src/Selector/Selector.test.tsx +85 -26
  171. package/src/Selector/Selector.tsx +13 -4
  172. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  173. package/src/StatusDot/StatusDot.test.tsx +115 -8
  174. package/src/StatusDot/StatusDot.tsx +74 -9
  175. package/src/Switch/Switch.test.tsx +16 -0
  176. package/src/Switch/Switch.tsx +7 -0
  177. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  178. package/src/TimeInput/TimeInput.test.tsx +23 -0
  179. package/src/TimeInput/TimeInput.tsx +8 -0
  180. package/src/Tokenizer/Tokenizer.test.tsx +50 -32
  181. package/src/Tokenizer/Tokenizer.tsx +5 -5
  182. package/src/Tooltip/useTooltip.tsx +4 -1
  183. package/src/Typeahead/BaseTypeahead.tsx +10 -3
  184. package/src/Typeahead/Typeahead.test.tsx +116 -7
  185. package/src/Typeahead/Typeahead.tsx +10 -1
  186. package/src/__tests__/stubMatchMedia.test.ts +79 -0
  187. package/src/__tests__/stubMatchMedia.ts +95 -0
  188. package/src/hooks/index.ts +9 -5
  189. package/src/hooks/useFocusTrap.ts +1 -14
  190. package/src/hooks/useInputStatusIcon.test.tsx +19 -0
  191. package/src/hooks/useInputStatusIcon.tsx +7 -0
  192. package/src/index.ts +1 -0
  193. package/src/theme/defineTheme.ts +22 -4
  194. package/src/theme/expandColorScale.test.ts +138 -0
  195. package/src/theme/expandColorScale.ts +89 -44
  196. package/src/utils/ime.test.ts +32 -0
  197. package/src/utils/ime.ts +69 -0
  198. package/src/utils/index.ts +2 -0
  199. package/dist/astryx.umd.js +0 -147
  200. package/dist/astryx.umd.js.map +0 -7
@@ -0,0 +1,850 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file BottomSheetSwitcher.test.tsx
5
+ * @input Uses vitest, Testing Library, BottomSheet, BottomSheetSwitcher
6
+ * @output Tests mutually exclusive sheet selection, dismissal, and focus handoff
7
+ * @position Core tests for BottomSheetSwitcher
8
+ *
9
+ * SYNC: When BottomSheetSwitcher.tsx or its BottomSheet integration changes,
10
+ * update these tests to match the public behavior.
11
+ */
12
+
13
+ import {fireEvent, render, screen} from '@testing-library/react';
14
+ import {createRef, useState} from 'react';
15
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
16
+ import {useFocusTrap} from '../hooks';
17
+ import {BottomSheet} from './BottomSheet';
18
+ import {BottomSheetSwitcher} from './BottomSheetSwitcher';
19
+
20
+ beforeEach(() => {
21
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
22
+ this: HTMLDialogElement,
23
+ ) {
24
+ this.setAttribute('open', '');
25
+ });
26
+ HTMLDialogElement.prototype.show = vi.fn(function (this: HTMLDialogElement) {
27
+ this.setAttribute('open', '');
28
+ });
29
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
30
+ this.removeAttribute('open');
31
+ });
32
+ window.scrollTo = vi.fn();
33
+ vi.stubGlobal(
34
+ 'matchMedia',
35
+ vi.fn().mockReturnValue({
36
+ matches: false,
37
+ media: '',
38
+ onchange: null,
39
+ addEventListener: vi.fn(),
40
+ removeEventListener: vi.fn(),
41
+ addListener: vi.fn(),
42
+ removeListener: vi.fn(),
43
+ dispatchEvent: vi.fn(),
44
+ }),
45
+ );
46
+ });
47
+
48
+ afterEach(() => {
49
+ vi.unstubAllGlobals();
50
+ });
51
+
52
+ function Flow() {
53
+ const [activeSheet, setActiveSheet] = useState<string | null>(null);
54
+ return (
55
+ <>
56
+ <button type="button" onClick={() => setActiveSheet('details')}>
57
+ Start flow
58
+ </button>
59
+ <BottomSheetSwitcher
60
+ activeSheet={activeSheet}
61
+ onActiveSheetChange={setActiveSheet}>
62
+ <BottomSheet
63
+ sheetId="details"
64
+ label="Details"
65
+ data-testid="details-sheet">
66
+ <button type="button" onClick={() => setActiveSheet('confirm')}>
67
+ Continue
68
+ </button>
69
+ </BottomSheet>
70
+ <BottomSheet
71
+ sheetId="confirm"
72
+ label="Confirm"
73
+ data-testid="confirm-sheet">
74
+ <button type="button" onClick={() => setActiveSheet('details')}>
75
+ Back
76
+ </button>
77
+ </BottomSheet>
78
+ </BottomSheetSwitcher>
79
+ </>
80
+ );
81
+ }
82
+
83
+ function ConditionalFlow() {
84
+ const [activeSheet, setActiveSheet] = useState<string | null>(null);
85
+ return (
86
+ <>
87
+ <button type="button" onClick={() => setActiveSheet('details')}>
88
+ Start conditional flow
89
+ </button>
90
+ <BottomSheetSwitcher
91
+ activeSheet={activeSheet}
92
+ onActiveSheetChange={setActiveSheet}>
93
+ {activeSheet != null && (
94
+ <BottomSheet sheetId={activeSheet} label="Conditional details">
95
+ Content
96
+ </BottomSheet>
97
+ )}
98
+ </BottomSheetSwitcher>
99
+ </>
100
+ );
101
+ }
102
+
103
+ function NestedStandaloneSheetFlow() {
104
+ const [isNestedOpen, setIsNestedOpen] = useState(false);
105
+ return (
106
+ <BottomSheetSwitcher activeSheet="details" onActiveSheetChange={() => {}}>
107
+ <BottomSheet sheetId="details" label="Details">
108
+ <button type="button" onClick={() => setIsNestedOpen(true)}>
109
+ Open nested sheet
110
+ </button>
111
+ <BottomSheet
112
+ isOpen={isNestedOpen}
113
+ onOpenChange={setIsNestedOpen}
114
+ label="Nested standalone sheet">
115
+ Nested content
116
+ </BottomSheet>
117
+ </BottomSheet>
118
+ </BottomSheetSwitcher>
119
+ );
120
+ }
121
+
122
+ function NestedEscapeTrap({onEscape}: {onEscape: () => void}) {
123
+ const {containerRef} = useFocusTrap<HTMLDivElement>({
124
+ isActive: true,
125
+ onEscape,
126
+ });
127
+ return (
128
+ <div ref={containerRef} data-testid="nested-escape-trap">
129
+ Nested layer
130
+ </div>
131
+ );
132
+ }
133
+
134
+ const panelRefA = (_element: HTMLDivElement | null) => {};
135
+ const panelRefB = (_element: HTMLDivElement | null) => {};
136
+
137
+ function CallbackRefFlow() {
138
+ const [activeSheet, setActiveSheet] = useState<string | null>('details');
139
+ const [useAlternateRef, setUseAlternateRef] = useState(false);
140
+ return (
141
+ <>
142
+ <button
143
+ type="button"
144
+ onClick={() => setUseAlternateRef(current => !current)}>
145
+ Rerender parent
146
+ </button>
147
+ <BottomSheetSwitcher
148
+ activeSheet={activeSheet}
149
+ onActiveSheetChange={setActiveSheet}>
150
+ <BottomSheet
151
+ ref={useAlternateRef ? panelRefB : panelRefA}
152
+ sheetId="details"
153
+ label="Details"
154
+ data-testid="callback-details-sheet">
155
+ <button type="button" onClick={() => setActiveSheet('confirm')}>
156
+ Continue with callback ref
157
+ </button>
158
+ </BottomSheet>
159
+ <BottomSheet
160
+ sheetId="confirm"
161
+ label="Confirm"
162
+ data-testid="callback-confirm-sheet">
163
+ Confirmation
164
+ </BottomSheet>
165
+ </BottomSheetSwitcher>
166
+ </>
167
+ );
168
+ }
169
+
170
+ function ModeSwitchFlow() {
171
+ const [activeSheet, setActiveSheet] = useState<string | null>(null);
172
+ const [hasScrim, setHasScrim] = useState(true);
173
+ return (
174
+ <>
175
+ <button type="button" onClick={() => setActiveSheet('details')}>
176
+ Open first modal
177
+ </button>
178
+ <button
179
+ type="button"
180
+ onClick={() => {
181
+ setHasScrim(true);
182
+ setActiveSheet('details');
183
+ }}>
184
+ Open second modal
185
+ </button>
186
+ <BottomSheetSwitcher
187
+ activeSheet={activeSheet}
188
+ onActiveSheetChange={setActiveSheet}
189
+ hasScrim={hasScrim}>
190
+ <BottomSheet
191
+ sheetId="details"
192
+ label="Details"
193
+ data-testid="mode-details-sheet">
194
+ <button type="button" onClick={() => setHasScrim(false)}>
195
+ Make non-modal
196
+ </button>
197
+ <button type="button" onClick={() => setActiveSheet(null)}>
198
+ Close flow
199
+ </button>
200
+ </BottomSheet>
201
+ </BottomSheetSwitcher>
202
+ </>
203
+ );
204
+ }
205
+
206
+ function getSharedDialog(): HTMLDialogElement {
207
+ return screen.getByRole<HTMLDialogElement>('dialog');
208
+ }
209
+
210
+ function finishSheetTransition(
211
+ element: HTMLElement,
212
+ propertyName: 'transform' | 'opacity',
213
+ ) {
214
+ const sheet = getSheetPanel(element);
215
+ fireEvent.transitionEnd(sheet, {propertyName});
216
+ }
217
+
218
+ function getSheetPanel(element: HTMLElement): HTMLElement {
219
+ if (element.classList.contains('astryx-bottom-sheet')) {
220
+ return element;
221
+ }
222
+ const sheet = element.querySelector<HTMLElement>('.astryx-bottom-sheet');
223
+ if (!sheet) {
224
+ throw new Error('sheet panel not found');
225
+ }
226
+ return sheet;
227
+ }
228
+
229
+ function getSheetLayer(testId: string): HTMLElement {
230
+ const panel = screen.getByTestId(testId);
231
+ const layer = panel.parentElement;
232
+ if (layer == null) {
233
+ throw new Error('sheet layer not found');
234
+ }
235
+ return layer;
236
+ }
237
+
238
+ function mockSheetTop(sheet: HTMLElement, top: number) {
239
+ const rect = {
240
+ x: 0,
241
+ y: top,
242
+ top,
243
+ right: 640,
244
+ bottom: 800,
245
+ left: 0,
246
+ width: 640,
247
+ height: 800 - top,
248
+ toJSON: () => {},
249
+ };
250
+ vi.spyOn(sheet, 'getBoundingClientRect').mockReturnValue(rect);
251
+ if (sheet.parentElement) {
252
+ vi.spyOn(sheet.parentElement, 'getBoundingClientRect').mockReturnValue(
253
+ rect,
254
+ );
255
+ }
256
+ }
257
+
258
+ describe('BottomSheetSwitcher', () => {
259
+ it('opens only the sheet selected by activeSheet', () => {
260
+ render(<Flow />);
261
+
262
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
263
+
264
+ expect(getSheetLayer('details-sheet')).not.toHaveAttribute('hidden');
265
+ expect(getSheetLayer('confirm-sheet')).toHaveAttribute('hidden');
266
+ expect(getSheetLayer('confirm-sheet')).toHaveStyle({display: 'none'});
267
+ expect(document.querySelectorAll('dialog[open]')).toHaveLength(1);
268
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledTimes(1);
269
+ expect(HTMLDialogElement.prototype.show).not.toHaveBeenCalled();
270
+ expect(getSharedDialog()).toHaveAttribute('aria-modal', 'true');
271
+ expect(getSharedDialog()).toHaveAccessibleName('Details');
272
+ });
273
+
274
+ it('forwards sheet DOM props and refs to its panel in the shared dialog', () => {
275
+ const panelRef = createRef<HTMLDivElement>();
276
+
277
+ render(
278
+ <BottomSheetSwitcher activeSheet="details" onActiveSheetChange={() => {}}>
279
+ <BottomSheet
280
+ ref={panelRef}
281
+ sheetId="details"
282
+ label="Details"
283
+ data-testid="details-layer"
284
+ data-sheet-owner="settings">
285
+ Content
286
+ </BottomSheet>
287
+ </BottomSheetSwitcher>,
288
+ );
289
+
290
+ const panel = screen.getByTestId('details-layer');
291
+ expect(panelRef.current).toBe(panel);
292
+ expect(panel).toHaveClass('astryx-bottom-sheet');
293
+ expect(panel).toHaveAttribute('data-sheet-owner', 'settings');
294
+ expect(getSharedDialog()).toContainElement(panel);
295
+ });
296
+
297
+ it('forwards switcher DOM props, events, styles, and refs to the shared dialog', () => {
298
+ const dialogRef = createRef<HTMLDialogElement>();
299
+ const onClick = vi.fn();
300
+ const onActiveSheetChange = vi.fn();
301
+
302
+ render(
303
+ <BottomSheetSwitcher
304
+ ref={dialogRef}
305
+ activeSheet="details"
306
+ onActiveSheetChange={onActiveSheetChange}
307
+ aria-label="Notification setup"
308
+ data-flow-owner="settings"
309
+ className="consumer-switcher"
310
+ style={{insetInlineStart: 12}}
311
+ onClick={onClick}>
312
+ <BottomSheet sheetId="details" label="Details">
313
+ Content
314
+ </BottomSheet>
315
+ </BottomSheetSwitcher>,
316
+ );
317
+
318
+ const dialog = getSharedDialog();
319
+ expect(dialogRef.current).toBe(dialog);
320
+ expect(dialog).toHaveAccessibleName('Notification setup');
321
+ expect(dialog).toHaveAttribute('data-flow-owner', 'settings');
322
+ expect(dialog).toHaveClass('consumer-switcher');
323
+ expect(dialog).toHaveStyle({insetInlineStart: '12px'});
324
+
325
+ fireEvent.click(dialog);
326
+ expect(onClick).toHaveBeenCalledTimes(1);
327
+ expect(onActiveSheetChange).toHaveBeenCalledWith(null);
328
+ });
329
+
330
+ it('resets switcher context for a sheet nested inside item content', () => {
331
+ render(<NestedStandaloneSheetFlow />);
332
+ const opener = screen.getByRole('button', {name: 'Open nested sheet'});
333
+ opener.focus();
334
+ fireEvent.click(opener);
335
+
336
+ const nestedDialog = screen.getByRole('dialog', {
337
+ name: 'Nested standalone sheet',
338
+ });
339
+ expect(nestedDialog).toHaveAttribute('open');
340
+ expect(getSheetPanel(nestedDialog)).toHaveFocus();
341
+ expect(document.querySelectorAll('dialog[open]')).toHaveLength(2);
342
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledTimes(2);
343
+
344
+ fireEvent.keyDown(nestedDialog, {key: 'Escape'});
345
+ finishSheetTransition(nestedDialog, 'transform');
346
+
347
+ expect(nestedDialog).not.toHaveAttribute('open');
348
+ expect(getSharedDialog()).toHaveAttribute('open');
349
+ expect(opener).toHaveFocus();
350
+ });
351
+
352
+ it('keeps the previous sheet stationary until the new entrance finishes, then fades it', () => {
353
+ render(<Flow />);
354
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
355
+ const sharedDialog = getSharedDialog();
356
+ const detailsSheet = getSheetLayer('details-sheet');
357
+ const confirmSheet = getSheetLayer('confirm-sheet');
358
+
359
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
360
+
361
+ expect(detailsSheet).not.toHaveAttribute('hidden');
362
+ expect(detailsSheet).toHaveAttribute('inert');
363
+ expect(detailsSheet).toHaveAttribute('aria-hidden', 'true');
364
+ expect(confirmSheet).not.toHaveAttribute('hidden');
365
+ expect(confirmSheet).not.toHaveAttribute('inert');
366
+ expect(document.querySelectorAll('dialog[open]')).toHaveLength(1);
367
+ expect(getSharedDialog()).toBe(sharedDialog);
368
+ expect(sharedDialog).toHaveAccessibleName('Confirm');
369
+
370
+ // The previous sheet is covered, not exiting: neither transform nor
371
+ // opacity completion may release it before the new entrance completes.
372
+ finishSheetTransition(detailsSheet, 'transform');
373
+ finishSheetTransition(detailsSheet, 'opacity');
374
+ expect(detailsSheet).not.toHaveAttribute('hidden');
375
+
376
+ finishSheetTransition(confirmSheet, 'transform');
377
+ expect(detailsSheet).not.toHaveAttribute('hidden');
378
+
379
+ finishSheetTransition(detailsSheet, 'opacity');
380
+
381
+ expect(detailsSheet).toHaveAttribute('hidden');
382
+ expect(confirmSheet).not.toHaveAttribute('hidden');
383
+ expect(document.querySelectorAll('dialog[open]')).toHaveLength(1);
384
+ expect(getSharedDialog()).toBe(sharedDialog);
385
+
386
+ fireEvent.click(screen.getByRole('button', {name: 'Back'}));
387
+
388
+ expect(detailsSheet).not.toHaveAttribute('hidden');
389
+ expect(confirmSheet).not.toHaveAttribute('hidden');
390
+ expect(confirmSheet).toHaveAttribute('inert');
391
+ finishSheetTransition(detailsSheet, 'transform');
392
+ expect(confirmSheet).not.toHaveAttribute('hidden');
393
+ finishSheetTransition(confirmSheet, 'opacity');
394
+
395
+ expect(detailsSheet).not.toHaveAttribute('hidden');
396
+ expect(confirmSheet).toHaveAttribute('hidden');
397
+ expect(document.querySelectorAll('dialog[open]')).toHaveLength(1);
398
+ expect(getSharedDialog()).toBe(sharedDialog);
399
+ });
400
+
401
+ it('moves a taller previous sheet down while the shorter new sheet enters, then waits for both', () => {
402
+ render(<Flow />);
403
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
404
+ const detailsSheet = getSheetLayer('details-sheet');
405
+ const confirmSheet = getSheetLayer('confirm-sheet');
406
+ const detailsPanel = getSheetPanel(detailsSheet);
407
+ const confirmPanel = getSheetPanel(confirmSheet);
408
+ mockSheetTop(detailsPanel, 100);
409
+ mockSheetTop(confirmPanel, 300);
410
+
411
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
412
+
413
+ expect(detailsSheet).not.toHaveAttribute('hidden');
414
+ expect(detailsPanel).toHaveStyle({transform: 'translateY(200px)'});
415
+
416
+ // The incoming entrance may finish first, but opacity cannot hide the
417
+ // retained sheet until its concurrent alignment also completes.
418
+ finishSheetTransition(confirmSheet, 'transform');
419
+ finishSheetTransition(detailsSheet, 'opacity');
420
+ expect(detailsSheet).not.toHaveAttribute('hidden');
421
+ finishSheetTransition(detailsSheet, 'transform');
422
+ expect(detailsSheet).not.toHaveAttribute('hidden');
423
+ expect(detailsPanel).toHaveStyle({transform: 'translateY(200px)'});
424
+ finishSheetTransition(detailsSheet, 'opacity');
425
+
426
+ expect(detailsSheet).toHaveAttribute('hidden');
427
+ expect(confirmSheet).not.toHaveAttribute('hidden');
428
+ });
429
+
430
+ it('waits for the incoming entrance when top-edge alignment finishes first', () => {
431
+ render(<Flow />);
432
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
433
+ const detailsSheet = getSheetLayer('details-sheet');
434
+ const confirmSheet = getSheetLayer('confirm-sheet');
435
+ const detailsPanel = getSheetPanel(detailsSheet);
436
+ mockSheetTop(detailsPanel, 100);
437
+ mockSheetTop(getSheetPanel(confirmSheet), 300);
438
+
439
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
440
+ expect(detailsPanel).toHaveStyle({transform: 'translateY(200px)'});
441
+
442
+ finishSheetTransition(detailsSheet, 'transform');
443
+ finishSheetTransition(detailsSheet, 'opacity');
444
+ expect(detailsSheet).not.toHaveAttribute('hidden');
445
+ finishSheetTransition(confirmSheet, 'transform');
446
+ finishSheetTransition(detailsSheet, 'opacity');
447
+
448
+ expect(detailsSheet).toHaveAttribute('hidden');
449
+ expect(confirmSheet).not.toHaveAttribute('hidden');
450
+ });
451
+
452
+ it('replaces an unfinished outgoing sheet during rapid navigation', () => {
453
+ render(<Flow />);
454
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
455
+ const detailsSheet = getSheetLayer('details-sheet');
456
+ const confirmSheet = getSheetLayer('confirm-sheet');
457
+
458
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
459
+ fireEvent.click(screen.getByRole('button', {name: 'Back'}));
460
+
461
+ expect(detailsSheet).not.toHaveAttribute('hidden');
462
+ expect(detailsSheet).not.toHaveAttribute('inert');
463
+ expect(confirmSheet).not.toHaveAttribute('hidden');
464
+ expect(confirmSheet).toHaveAttribute('inert');
465
+ expect(document.querySelectorAll('dialog[open]')).toHaveLength(1);
466
+
467
+ finishSheetTransition(confirmSheet, 'opacity');
468
+ expect(detailsSheet).not.toHaveAttribute('hidden');
469
+ expect(confirmSheet).toHaveAttribute('hidden');
470
+ });
471
+
472
+ it('ignores late scrim updates from an outgoing sheet gesture', () => {
473
+ render(<Flow />);
474
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
475
+
476
+ const detailsPanel = getSheetPanel(getSheetLayer('details-sheet'));
477
+ mockSheetTop(detailsPanel, 100);
478
+ const handle = detailsPanel.firstElementChild;
479
+ if (!(handle instanceof HTMLElement)) {
480
+ throw new Error('sheet handle not found');
481
+ }
482
+
483
+ fireEvent.pointerDown(handle, {
484
+ pointerId: 1,
485
+ clientY: 100,
486
+ timeStamp: 0,
487
+ button: 0,
488
+ isPrimary: true,
489
+ });
490
+ fireEvent.pointerMove(handle, {
491
+ pointerId: 1,
492
+ clientY: 600,
493
+ timeStamp: 16,
494
+ });
495
+ expect(
496
+ getSharedDialog().style.getPropertyValue('--_sheet-scrim-opacity'),
497
+ ).not.toBe('1');
498
+
499
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
500
+ expect(getSharedDialog()).toHaveStyle({'--_sheet-scrim-opacity': '1'});
501
+
502
+ fireEvent.pointerMove(handle, {
503
+ pointerId: 1,
504
+ clientY: 680,
505
+ timeStamp: 32,
506
+ });
507
+ expect(getSharedDialog()).toHaveStyle({'--_sheet-scrim-opacity': '1'});
508
+ });
509
+
510
+ it('keeps an active handoff when a consumer callback ref changes', () => {
511
+ render(<CallbackRefFlow />);
512
+ const detailsSheet = getSheetLayer('callback-details-sheet');
513
+ const confirmSheet = getSheetLayer('callback-confirm-sheet');
514
+
515
+ fireEvent.click(
516
+ screen.getByRole('button', {name: 'Continue with callback ref'}),
517
+ );
518
+ expect(detailsSheet).not.toHaveAttribute('hidden');
519
+ expect(detailsSheet).toHaveAttribute('inert');
520
+ expect(confirmSheet).not.toHaveAttribute('hidden');
521
+
522
+ fireEvent.click(screen.getByRole('button', {name: 'Rerender parent'}));
523
+
524
+ expect(detailsSheet).not.toHaveAttribute('hidden');
525
+ expect(detailsSheet).toHaveAttribute('inert');
526
+ expect(confirmSheet).not.toHaveAttribute('hidden');
527
+ });
528
+
529
+ it('dismisses the flow from the one shared scrim', () => {
530
+ render(<Flow />);
531
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
532
+ const sharedDialog = getSharedDialog();
533
+
534
+ fireEvent.click(sharedDialog);
535
+
536
+ const outgoingSheet = getSheetLayer('details-sheet');
537
+ expect(outgoingSheet).not.toHaveAttribute('hidden');
538
+ expect(outgoingSheet).toHaveAttribute('inert');
539
+ expect(sharedDialog).toHaveStyle({'--_sheet-scrim-opacity': '0'});
540
+ expect(document.body.style.position).toBe('fixed');
541
+
542
+ finishSheetTransition(outgoingSheet, 'transform');
543
+
544
+ expect(sharedDialog).not.toHaveAttribute('open');
545
+ expect(document.body.style.position).not.toBe('fixed');
546
+ });
547
+
548
+ it('releases the shared modal layer when the closing sheet unmounts immediately', () => {
549
+ render(<ConditionalFlow />);
550
+ fireEvent.click(
551
+ screen.getByRole('button', {name: 'Start conditional flow'}),
552
+ );
553
+ const sharedDialog = screen.getByRole('dialog', {
554
+ name: 'Conditional details',
555
+ });
556
+
557
+ fireEvent.keyDown(sharedDialog, {key: 'Escape'});
558
+
559
+ expect(sharedDialog).not.toHaveAttribute('open');
560
+ expect(document.body.style.position).not.toBe('fixed');
561
+ });
562
+
563
+ it('keeps the shared dialog inline and opens it modally', () => {
564
+ render(
565
+ <div
566
+ data-testid="clipping-ancestor"
567
+ style={{overflow: 'hidden', transform: 'translateY(100px)'}}>
568
+ <BottomSheetSwitcher
569
+ activeSheet="details"
570
+ onActiveSheetChange={() => {}}>
571
+ <BottomSheet sheetId="details" label="Inline modal details">
572
+ Content
573
+ </BottomSheet>
574
+ </BottomSheetSwitcher>
575
+ </div>,
576
+ );
577
+
578
+ const clippingAncestor = screen.getByTestId('clipping-ancestor');
579
+ const dialog = screen.getByRole('dialog', {name: 'Inline modal details'});
580
+ expect(clippingAncestor).toContainElement(dialog);
581
+ expect(dialog).toHaveAttribute('open');
582
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledTimes(1);
583
+ });
584
+
585
+ it('keeps focus in a modal sheet that has no tabbable controls', () => {
586
+ render(
587
+ <>
588
+ <button type="button">Background action</button>
589
+ <BottomSheetSwitcher
590
+ activeSheet="details"
591
+ onActiveSheetChange={() => {}}>
592
+ <BottomSheet sheetId="details" label="Read-only details">
593
+ Read-only content
594
+ </BottomSheet>
595
+ </BottomSheetSwitcher>
596
+ </>,
597
+ );
598
+
599
+ const dialog = screen.getByRole('dialog', {name: 'Read-only details'});
600
+ const panel = getSheetPanel(dialog);
601
+ expect(panel).toHaveFocus();
602
+ expect(fireEvent.keyDown(panel, {key: 'Tab'})).toBe(false);
603
+ expect(panel).toHaveFocus();
604
+ expect(
605
+ screen.getByRole('button', {name: 'Background action'}),
606
+ ).not.toHaveFocus();
607
+ });
608
+
609
+ it('can coordinate a non-modal flow without rendering a scrim', () => {
610
+ render(
611
+ <BottomSheetSwitcher
612
+ activeSheet="details"
613
+ onActiveSheetChange={() => {}}
614
+ hasScrim={false}>
615
+ <BottomSheet sheetId="details" label="Details">
616
+ Content
617
+ </BottomSheet>
618
+ </BottomSheetSwitcher>,
619
+ );
620
+
621
+ const dialog = getSharedDialog();
622
+ expect(dialog).not.toHaveAttribute('aria-modal');
623
+ expect(dialog).toHaveAttribute('open');
624
+ expect(HTMLDialogElement.prototype.show).toHaveBeenCalledTimes(1);
625
+ expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled();
626
+ expect(document.body.style.position).not.toBe('fixed');
627
+ });
628
+
629
+ it('requests activeSheet=null when the active sheet dismisses', () => {
630
+ const onActiveSheetChange = vi.fn();
631
+ render(
632
+ <BottomSheetSwitcher
633
+ activeSheet="details"
634
+ onActiveSheetChange={onActiveSheetChange}>
635
+ <BottomSheet sheetId="details" label="Details">
636
+ Content
637
+ </BottomSheet>
638
+ <BottomSheet sheetId="confirm" label="Confirm">
639
+ Content
640
+ </BottomSheet>
641
+ </BottomSheetSwitcher>,
642
+ );
643
+
644
+ fireEvent.keyDown(screen.getByRole('dialog', {name: 'Details'}), {
645
+ key: 'Escape',
646
+ });
647
+
648
+ expect(onActiveSheetChange).toHaveBeenCalledWith(null);
649
+ });
650
+
651
+ it('honors purpose=form for a switcher-managed sheet', () => {
652
+ const onActiveSheetChange = vi.fn();
653
+ render(
654
+ <BottomSheetSwitcher
655
+ activeSheet="details"
656
+ onActiveSheetChange={onActiveSheetChange}>
657
+ <BottomSheet sheetId="details" label="Edit details" purpose="form">
658
+ Content
659
+ </BottomSheet>
660
+ </BottomSheetSwitcher>,
661
+ );
662
+ const dialog = getSharedDialog();
663
+ const panel = getSheetPanel(dialog);
664
+ const handle = panel.firstElementChild;
665
+ if (!(handle instanceof HTMLElement)) {
666
+ throw new Error('sheet handle not found');
667
+ }
668
+
669
+ fireEvent.click(dialog);
670
+ fireEvent.pointerDown(handle, {
671
+ pointerId: 1,
672
+ clientY: 0,
673
+ button: 0,
674
+ isPrimary: true,
675
+ });
676
+ fireEvent.pointerMove(handle, {pointerId: 1, clientY: 120});
677
+ fireEvent.pointerUp(handle, {pointerId: 1, clientY: 120});
678
+
679
+ expect(onActiveSheetChange).not.toHaveBeenCalled();
680
+ expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
681
+
682
+ fireEvent.keyDown(dialog, {key: 'Escape'});
683
+ fireEvent(dialog, new Event('cancel', {cancelable: true}));
684
+
685
+ expect(onActiveSheetChange).toHaveBeenCalledTimes(2);
686
+ expect(onActiveSheetChange).toHaveBeenNthCalledWith(1, null);
687
+ expect(onActiveSheetChange).toHaveBeenNthCalledWith(2, null);
688
+ });
689
+
690
+ it('honors purpose=required for a switcher-managed sheet', () => {
691
+ const onActiveSheetChange = vi.fn();
692
+ render(
693
+ <BottomSheetSwitcher
694
+ activeSheet="details"
695
+ onActiveSheetChange={onActiveSheetChange}>
696
+ <BottomSheet
697
+ sheetId="details"
698
+ label="Required details"
699
+ purpose="required">
700
+ Content
701
+ </BottomSheet>
702
+ </BottomSheetSwitcher>,
703
+ );
704
+ const dialog = screen.getByRole('alertdialog');
705
+ const panel = getSheetPanel(dialog);
706
+ const handle = panel.firstElementChild;
707
+ if (!(handle instanceof HTMLElement)) {
708
+ throw new Error('sheet handle not found');
709
+ }
710
+
711
+ fireEvent.click(dialog);
712
+ fireEvent.keyDown(dialog, {key: 'Escape'});
713
+ fireEvent(dialog, new Event('cancel', {cancelable: true}));
714
+ fireEvent.pointerDown(handle, {
715
+ pointerId: 1,
716
+ clientY: 0,
717
+ button: 0,
718
+ isPrimary: true,
719
+ });
720
+ fireEvent.pointerMove(handle, {pointerId: 1, clientY: 120});
721
+ fireEvent.pointerUp(handle, {pointerId: 1, clientY: 120});
722
+
723
+ expect(onActiveSheetChange).not.toHaveBeenCalled();
724
+ expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
725
+ });
726
+
727
+ it('restores the active sheet when a context menu interrupts its drag', () => {
728
+ const onActiveSheetChange = vi.fn();
729
+ render(
730
+ <BottomSheetSwitcher
731
+ activeSheet="details"
732
+ onActiveSheetChange={onActiveSheetChange}>
733
+ <BottomSheet sheetId="details" label="Details">
734
+ Content
735
+ </BottomSheet>
736
+ </BottomSheetSwitcher>,
737
+ );
738
+ const dialog = getSharedDialog();
739
+ const panel = getSheetPanel(dialog);
740
+ const handle = panel.firstElementChild;
741
+ if (!(handle instanceof HTMLElement)) {
742
+ throw new Error('sheet handle not found');
743
+ }
744
+
745
+ fireEvent.pointerDown(handle, {
746
+ pointerId: 1,
747
+ clientY: 0,
748
+ button: 0,
749
+ isPrimary: true,
750
+ });
751
+ fireEvent.pointerMove(handle, {pointerId: 1, clientY: 300});
752
+ expect(panel.style.transform).toBe('translateY(300px)');
753
+
754
+ expect(fireEvent.contextMenu(handle)).toBe(false);
755
+
756
+ expect(panel.style.transform).toBe('');
757
+ expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
758
+ expect(onActiveSheetChange).not.toHaveBeenCalled();
759
+ });
760
+
761
+ it('ignores Escape while an IME composition is active', () => {
762
+ const onActiveSheetChange = vi.fn();
763
+ render(
764
+ <BottomSheetSwitcher
765
+ activeSheet="details"
766
+ onActiveSheetChange={onActiveSheetChange}>
767
+ <BottomSheet sheetId="details" label="Details">
768
+ Content
769
+ </BottomSheet>
770
+ </BottomSheetSwitcher>,
771
+ );
772
+
773
+ const dialog = screen.getByRole('dialog', {name: 'Details'});
774
+ fireEvent.keyDown(dialog, {key: 'Escape', isComposing: true});
775
+ fireEvent.keyDown(dialog, {key: 'Escape', keyCode: 229});
776
+
777
+ expect(onActiveSheetChange).not.toHaveBeenCalled();
778
+ });
779
+
780
+ it('lets a nested focus trap handle Escape before the switcher', () => {
781
+ const onActiveSheetChange = vi.fn();
782
+ const onNestedEscape = vi.fn();
783
+ render(
784
+ <BottomSheetSwitcher
785
+ activeSheet="details"
786
+ onActiveSheetChange={onActiveSheetChange}>
787
+ <BottomSheet sheetId="details" label="Details">
788
+ <NestedEscapeTrap onEscape={onNestedEscape} />
789
+ </BottomSheet>
790
+ </BottomSheetSwitcher>,
791
+ );
792
+
793
+ fireEvent.keyDown(screen.getByTestId('nested-escape-trap'), {
794
+ key: 'Escape',
795
+ });
796
+
797
+ expect(onNestedEscape).toHaveBeenCalledTimes(1);
798
+ expect(onActiveSheetChange).not.toHaveBeenCalled();
799
+ });
800
+
801
+ it('returns focus to the original opener after a multi-sheet flow ends', () => {
802
+ render(<Flow />);
803
+ const opener = screen.getByRole('button', {name: 'Start flow'});
804
+ opener.focus();
805
+ fireEvent.click(opener);
806
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
807
+
808
+ fireEvent.keyDown(screen.getByRole('dialog', {name: 'Confirm'}), {
809
+ key: 'Escape',
810
+ });
811
+ expect(document.activeElement).not.toBe(opener);
812
+ finishSheetTransition(getSheetLayer('confirm-sheet'), 'transform');
813
+
814
+ expect(document.activeElement).toBe(opener);
815
+ });
816
+
817
+ it('captures a new focus trigger after switching through non-modal mode', () => {
818
+ render(<ModeSwitchFlow />);
819
+ const firstOpener = screen.getByRole('button', {name: 'Open first modal'});
820
+ const secondOpener = screen.getByRole('button', {
821
+ name: 'Open second modal',
822
+ });
823
+
824
+ firstOpener.focus();
825
+ fireEvent.click(firstOpener);
826
+ fireEvent.click(screen.getByRole('button', {name: 'Make non-modal'}));
827
+ fireEvent.click(screen.getByRole('button', {name: 'Close flow'}));
828
+ finishSheetTransition(getSheetLayer('mode-details-sheet'), 'transform');
829
+
830
+ secondOpener.focus();
831
+ fireEvent.click(secondOpener);
832
+ fireEvent.click(screen.getByRole('button', {name: 'Close flow'}));
833
+ finishSheetTransition(getSheetLayer('mode-details-sheet'), 'transform');
834
+
835
+ expect(document.activeElement).toBe(secondOpener);
836
+ });
837
+
838
+ it('does not refocus the panel when an incoming transition completes', () => {
839
+ render(<Flow />);
840
+ fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
841
+ fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
842
+
843
+ const confirmSheet = getSheetLayer('confirm-sheet');
844
+ const backButton = screen.getByRole('button', {name: 'Back'});
845
+ backButton.focus();
846
+ finishSheetTransition(confirmSheet, 'transform');
847
+
848
+ expect(backButton).toHaveFocus();
849
+ });
850
+ });