@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.
- package/CHANGELOG.md +60 -0
- package/README.md +79 -45
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +13 -3
- package/dist/BottomSheet/BottomSheet.d.ts +66 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +392 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +60 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +398 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +72 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +130 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +373 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +180 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +989 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +12 -6
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +9 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +31 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +21 -7
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +9 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +14 -3
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +8 -1
- package/dist/Tokenizer/Tokenizer.js +11 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +21 -0
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +22 -4
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +48 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +36 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +9 -8
- package/src/Banner/Banner.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +40 -0
- package/src/Banner/Banner.tsx +27 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
- package/src/BottomSheet/BottomSheet.test.tsx +2114 -0
- package/src/BottomSheet/BottomSheet.tsx +545 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +547 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +15 -0
- package/src/BottomSheet/snapOffsets.test.ts +187 -0
- package/src/BottomSheet/snapOffsets.ts +159 -0
- package/src/BottomSheet/useMobileKeyboard.ts +499 -0
- package/src/BottomSheet/useSheetGestures.test.ts +1059 -0
- package/src/BottomSheet/useSheetGestures.ts +1459 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/ComplexSelector/ComplexSelector.tsx +12 -5
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +9 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +39 -2
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FileInput/FileInput.test.tsx +53 -9
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +195 -51
- package/src/MultiSelector/MultiSelector.tsx +22 -8
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +9 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/Selector/Selector.test.tsx +85 -26
- package/src/Selector/Selector.tsx +13 -4
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +8 -0
- package/src/Tokenizer/Tokenizer.test.tsx +50 -32
- package/src/Tokenizer/Tokenizer.tsx +5 -5
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/Typeahead/BaseTypeahead.tsx +10 -3
- package/src/Typeahead/Typeahead.test.tsx +116 -7
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/hooks/useInputStatusIcon.test.tsx +19 -0
- package/src/hooks/useInputStatusIcon.tsx +7 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.ts +22 -4
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +2 -0
- package/dist/astryx.umd.js +0 -147
- 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
|
+
});
|