@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e4f8e4e
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/dist/BottomSheet/BottomSheet.d.ts +3 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -3
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/Button/Button.d.ts +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -4
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +2 -2
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +34 -17
- package/dist/ContextMenu/ContextMenu.d.ts +14 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +149 -26
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +340 -26
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
- package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
- package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/menuWidth.d.ts +14 -0
- package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
- package/dist/DropdownMenu/menuWidth.js +35 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
- package/dist/DropdownMenu/useMenuOverflow.js +60 -0
- package/dist/MoreMenu/MoreMenu.d.ts +8 -2
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts +11 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +97 -68
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts +10 -6
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +141 -16
- package/dist/Popover/usePopover.d.ts +4 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +52 -5
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +106 -80
- package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
- package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
- package/dist/Selector/SelectorBottomSheet.js +83 -0
- package/dist/Selector/index.d.ts +1 -1
- package/dist/Selector/index.d.ts.map +1 -1
- package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
- package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
- package/dist/Selector/selectorPresentation.stylex.js +21 -0
- package/dist/Selector/useSelectorPresentation.d.ts +32 -0
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
- package/dist/Selector/useSelectorPresentation.js +90 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.js +46 -9
- package/dist/astryx.css +19 -1
- package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
- package/dist/hooks/useAdaptivePresentation.js +19 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
- package/dist/hooks/useFocusReturnVisibility.js +35 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +12 -0
- package/locales/en.json +20 -0
- package/locales/pseudo.json +15 -0
- package/package.json +2 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +7 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
- package/src/Button/Button.doc.mjs +56 -0
- package/src/Button/Button.test.tsx +10 -0
- package/src/Button/Button.tsx +13 -5
- package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
- package/src/Carousel/Carousel.test.tsx +83 -0
- package/src/Carousel/Carousel.tsx +8 -2
- package/src/Chat/ChatToolCalls.test.tsx +44 -0
- package/src/Chat/ChatToolCalls.tsx +36 -15
- package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
- package/src/DropdownMenu/DropdownMenu.tsx +459 -28
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
- package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
- package/src/DropdownMenu/index.ts +2 -0
- package/src/DropdownMenu/menuWidth.ts +51 -0
- package/src/DropdownMenu/useMenuOverflow.ts +77 -0
- package/src/IconButton/IconButton.doc.mjs +38 -0
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
- package/src/MultiSelector/MultiSelector.test.tsx +127 -0
- package/src/MultiSelector/MultiSelector.tsx +121 -73
- package/src/MultiSelector/index.ts +1 -0
- package/src/Popover/Popover.doc.mjs +11 -11
- package/src/Popover/Popover.test.tsx +397 -2
- package/src/Popover/Popover.tsx +228 -21
- package/src/Popover/usePopover.tsx +92 -15
- package/src/ProgressBar/ProgressBar.spec.md +208 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
- package/src/Selector/Selector.doc.mjs +12 -0
- package/src/Selector/Selector.test.tsx +135 -0
- package/src/Selector/Selector.tsx +141 -82
- package/src/Selector/SelectorBottomSheet.tsx +97 -0
- package/src/Selector/index.ts +1 -0
- package/src/Selector/selectorPresentation.stylex.ts +20 -0
- package/src/Selector/useSelectorPresentation.ts +147 -0
- package/src/Toast/ToastViewport.test.tsx +118 -4
- package/src/Toast/useToastGesture.ts +61 -8
- package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
- package/src/hooks/useAdaptivePresentation.ts +35 -0
- package/src/hooks/useFocusReturnVisibility.ts +45 -0
- package/src/hooks/useFocusTrap.test.tsx +40 -0
- package/src/hooks/useFocusTrap.ts +13 -0
- package/src/theme/themingTargets.test.ts +91 -25
|
@@ -74,6 +74,44 @@ export const docs = {
|
|
|
74
74
|
|
|
75
75
|
usage: {
|
|
76
76
|
description: 'A button that shows only an icon with no visible text. Use IconButton in toolbars, table rows, and compact UI where space is tight and the icon is universally understood.',
|
|
77
|
+
accessibility: [
|
|
78
|
+
{
|
|
79
|
+
name: 'Essential icon or spinner arc',
|
|
80
|
+
category: 'Color contrast',
|
|
81
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
82
|
+
requirement: '3:1',
|
|
83
|
+
states: ['Rest', 'Hover', 'Pointer down', 'Loading'],
|
|
84
|
+
description:
|
|
85
|
+
'IconButton has no visible label. Its icon must have at least 3:1 contrast with the button background in Rest, Hover, and Pointer down. The moving spinner arc must also meet 3:1 while loading.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Visible control boundary',
|
|
89
|
+
category: 'Color contrast',
|
|
90
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
91
|
+
requirement: '3:1 if needed',
|
|
92
|
+
states: ['Rest'],
|
|
93
|
+
description:
|
|
94
|
+
'The button edge needs 3:1 contrast only when users need it to see the control.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Keyboard focus indicator',
|
|
98
|
+
category: 'Color contrast',
|
|
99
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
100
|
+
requirement: '3:1',
|
|
101
|
+
states: ['Focus visible'],
|
|
102
|
+
description:
|
|
103
|
+
'The focus outline must have at least 3:1 contrast with the area around the button. Check the red outline on destructive buttons too.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Disabled appearance',
|
|
107
|
+
category: 'Color contrast',
|
|
108
|
+
criterion: '1.4.3 and 1.4.11 exceptions',
|
|
109
|
+
requirement: 'Not required',
|
|
110
|
+
states: ['Disabled'],
|
|
111
|
+
description:
|
|
112
|
+
'Disabled controls do not need to meet these contrast ratios.',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
77
115
|
bestPractices: [
|
|
78
116
|
{ guidance: true, description: 'Make the aria-label specific: a trash icon labeled "Delete conversation" is clearer than just "Delete" for screen readers.' },
|
|
79
117
|
{ guidance: true, description: 'Add a tooltip: even a gear icon can mean Settings, Preferences, or Configure.' },
|
|
@@ -61,6 +61,12 @@ export const docs = {
|
|
|
61
61
|
"Alignment along the placement axis. Use 'end' to align the menu with the trigger's trailing edge, which is usually what an overflow menu wants.",
|
|
62
62
|
default: "'start'",
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'presentation',
|
|
66
|
+
type: "'popover' | 'bottom-sheet' | 'adaptive'",
|
|
67
|
+
description: 'Presentation policy forwarded to DropdownMenu. `adaptive` keeps the anchored popover on pointer-based layouts and uses a BottomSheet at 768px and below when the primary pointer is coarse.',
|
|
68
|
+
default: "'popover'",
|
|
69
|
+
},
|
|
64
70
|
{
|
|
65
71
|
name: 'onOpenChange',
|
|
66
72
|
type: '(isOpen: boolean) => void',
|
|
@@ -93,6 +99,7 @@ export const docs = {
|
|
|
93
99
|
bestPractices: [
|
|
94
100
|
{ guidance: true, description: 'Use for overflow or secondary actions; keep primary actions visible outside the menu.' },
|
|
95
101
|
{ guidance: true, description: 'Use dividers or sections to group related actions when the menu has many items.' },
|
|
102
|
+
{ guidance: true, description: 'Use `presentation="adaptive"` when the visible overflow trigger should open a thumb-reachable BottomSheet on compact touch devices.' },
|
|
96
103
|
{ guidance: false, description: 'Hide primary actions inside a MoreMenu; they should be directly visible.' },
|
|
97
104
|
],
|
|
98
105
|
},
|
|
@@ -153,6 +160,12 @@ export const docsZh = {
|
|
|
153
160
|
"沿放置轴的对齐方式。使用 'end' 让菜单与触发器的尾部边缘对齐,这通常是溢出菜单所需的效果。",
|
|
154
161
|
default: "'start'",
|
|
155
162
|
},
|
|
163
|
+
{
|
|
164
|
+
name: 'presentation',
|
|
165
|
+
type: "'popover' | 'bottom-sheet' | 'adaptive'",
|
|
166
|
+
description: '菜单呈现策略。`adaptive` 在指针布局中使用锚定浮层,在主指针为粗略指针且宽度不超过 768px 时使用 BottomSheet。',
|
|
167
|
+
default: "'popover'",
|
|
168
|
+
},
|
|
156
169
|
{
|
|
157
170
|
name: 'xstyle',
|
|
158
171
|
type: 'StyleXStyles',
|
|
@@ -171,6 +184,7 @@ export const docsZh = {
|
|
|
171
184
|
bestPractices: [
|
|
172
185
|
{ guidance: true, description: 'Use for overflow or secondary actions; keep primary actions visible outside the menu.' },
|
|
173
186
|
{ guidance: true, description: 'Use dividers or sections to group related actions when the menu has many items.' },
|
|
187
|
+
{ guidance: true, description: 'Use `presentation="adaptive"` when the visible overflow trigger should open a thumb-reachable BottomSheet on compact touch devices.' },
|
|
174
188
|
{ guidance: false, description: 'Hide primary actions inside a MoreMenu; they should be directly visible.' },
|
|
175
189
|
],
|
|
176
190
|
},
|
|
@@ -186,6 +200,7 @@ export const docsDense = {
|
|
|
186
200
|
bestPractices: [
|
|
187
201
|
{ guidance: true, description: 'Use for overflow or secondary actions; keep primary actions visible outside the menu.' },
|
|
188
202
|
{ guidance: true, description: 'Use dividers or sections to group related actions when the menu has many items.' },
|
|
203
|
+
{ guidance: true, description: 'Use `presentation="adaptive"` when the visible overflow trigger should open a thumb-reachable BottomSheet on compact touch devices.' },
|
|
189
204
|
{ guidance: false, description: 'Hide primary actions inside a MoreMenu; they should be directly visible.' },
|
|
190
205
|
],
|
|
191
206
|
},
|
|
@@ -199,6 +214,8 @@ export const docsDense = {
|
|
|
199
214
|
placement: 'Menu position relative to trigger. Logical (RTL mirrors).',
|
|
200
215
|
alignment:
|
|
201
216
|
"Alignment along the placement axis. 'end' aligns with the trigger's trailing edge.",
|
|
217
|
+
presentation:
|
|
218
|
+
'Popover, BottomSheet, or adaptive compact-touch presentation.',
|
|
202
219
|
xstyle:
|
|
203
220
|
'StyleX styles for layout customization (margins, positioning, sizing). Must be stylex.create() value.',
|
|
204
221
|
},
|
|
@@ -17,6 +17,30 @@ import {DropdownMenu} from '../DropdownMenu/DropdownMenu';
|
|
|
17
17
|
|
|
18
18
|
// Mock showPopover and hidePopover methods since they're not implemented in jsdom
|
|
19
19
|
beforeEach(() => {
|
|
20
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
21
|
+
this: HTMLDialogElement,
|
|
22
|
+
) {
|
|
23
|
+
this.setAttribute('open', '');
|
|
24
|
+
});
|
|
25
|
+
HTMLDialogElement.prototype.show = vi.fn(function (this: HTMLDialogElement) {
|
|
26
|
+
this.setAttribute('open', '');
|
|
27
|
+
});
|
|
28
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
29
|
+
this.removeAttribute('open');
|
|
30
|
+
});
|
|
31
|
+
vi.stubGlobal(
|
|
32
|
+
'matchMedia',
|
|
33
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
34
|
+
matches: false,
|
|
35
|
+
media: query,
|
|
36
|
+
onchange: null,
|
|
37
|
+
addEventListener: vi.fn(),
|
|
38
|
+
removeEventListener: vi.fn(),
|
|
39
|
+
addListener: vi.fn(),
|
|
40
|
+
removeListener: vi.fn(),
|
|
41
|
+
dispatchEvent: vi.fn(),
|
|
42
|
+
})),
|
|
43
|
+
);
|
|
20
44
|
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
21
45
|
this.setAttribute('popover-open', '');
|
|
22
46
|
const event = new Event('toggle', {bubbles: false});
|
|
@@ -85,6 +109,29 @@ describe('MoreMenu', () => {
|
|
|
85
109
|
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
86
110
|
});
|
|
87
111
|
|
|
112
|
+
it('keeps the trigger visually pressed while the menu is open', async () => {
|
|
113
|
+
const user = userEvent.setup();
|
|
114
|
+
render(<MoreMenu items={defaultItems} />);
|
|
115
|
+
|
|
116
|
+
const trigger = screen.getByRole('button', {name: 'More options'});
|
|
117
|
+
expect(trigger.className).not.toContain('DropdownMenu__styles.triggerOpen');
|
|
118
|
+
|
|
119
|
+
await user.click(trigger);
|
|
120
|
+
|
|
121
|
+
expect(trigger.className).toContain('DropdownMenu__styles.triggerOpen');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('forwards BottomSheet presentation to DropdownMenu', async () => {
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
render(<MoreMenu items={defaultItems} presentation="bottom-sheet" />);
|
|
127
|
+
|
|
128
|
+
await user.click(screen.getByRole('button', {name: 'More options'}));
|
|
129
|
+
await waitFor(() =>
|
|
130
|
+
expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledOnce(),
|
|
131
|
+
);
|
|
132
|
+
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
88
135
|
it('calls onClick when item is clicked', async () => {
|
|
89
136
|
const handleEdit = vi.fn();
|
|
90
137
|
const items = [
|
|
@@ -22,7 +22,10 @@ import type {ReactNode} from 'react';
|
|
|
22
22
|
import {useIcon} from '../Icon';
|
|
23
23
|
import {DropdownMenu} from '../DropdownMenu/DropdownMenu';
|
|
24
24
|
import {useSize} from '../SizeContext/SizeContext';
|
|
25
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
DropdownMenuOption,
|
|
27
|
+
DropdownMenuPresentation,
|
|
28
|
+
} from '../DropdownMenu';
|
|
26
29
|
import type {LayerAlignment, LayerPlacement} from '../Layer';
|
|
27
30
|
import type {ButtonVariant, ButtonSize} from '../Button';
|
|
28
31
|
import type {BaseProps} from '../BaseProps';
|
|
@@ -87,6 +90,13 @@ export interface MoreMenuProps extends Pick<
|
|
|
87
90
|
*/
|
|
88
91
|
alignment?: LayerAlignment;
|
|
89
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Menu presentation policy. `adaptive` uses a BottomSheet on compact
|
|
95
|
+
* coarse-pointer viewports and an anchored popover elsewhere.
|
|
96
|
+
* @default 'popover'
|
|
97
|
+
*/
|
|
98
|
+
presentation?: DropdownMenuPresentation;
|
|
99
|
+
|
|
90
100
|
/**
|
|
91
101
|
* Controlled open state for the menu.
|
|
92
102
|
*/
|
|
@@ -125,6 +135,7 @@ export function MoreMenu({
|
|
|
125
135
|
isDisabled = false,
|
|
126
136
|
placement,
|
|
127
137
|
alignment,
|
|
138
|
+
presentation,
|
|
128
139
|
isMenuOpen,
|
|
129
140
|
onOpenChange,
|
|
130
141
|
xstyle,
|
|
@@ -151,6 +162,7 @@ export function MoreMenu({
|
|
|
151
162
|
onOpenChange={onOpenChange}
|
|
152
163
|
placement={placement}
|
|
153
164
|
alignment={alignment}
|
|
165
|
+
presentation={presentation}
|
|
154
166
|
button={{
|
|
155
167
|
label,
|
|
156
168
|
icon: icon ?? moreIcon,
|
|
@@ -224,6 +224,13 @@ export const docs = {
|
|
|
224
224
|
'Which edge of the option row carries the checkbox. end pushes it to the far edge of the row, including on the select-all row.',
|
|
225
225
|
default: "'start'",
|
|
226
226
|
},
|
|
227
|
+
{
|
|
228
|
+
name: 'presentation',
|
|
229
|
+
type: "'popover' | 'bottom-sheet' | 'adaptive'",
|
|
230
|
+
description:
|
|
231
|
+
'How the option list is presented. adaptive uses a bottom sheet on compact touch screens and an anchored popover otherwise.',
|
|
232
|
+
default: "'popover'",
|
|
233
|
+
},
|
|
227
234
|
{
|
|
228
235
|
name: 'width',
|
|
229
236
|
type: 'SizeValue',
|
|
@@ -290,6 +297,11 @@ export const docs = {
|
|
|
290
297
|
description:
|
|
291
298
|
'Use variant="ghost" when a multi-selector sits in a toolbar with ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
|
|
292
299
|
},
|
|
300
|
+
{
|
|
301
|
+
guidance: true,
|
|
302
|
+
description:
|
|
303
|
+
'Use presentation="adaptive" when the multi-selector should become a bottom sheet on compact touch screens.',
|
|
304
|
+
},
|
|
293
305
|
{
|
|
294
306
|
guidance: false,
|
|
295
307
|
description: 'Use for single-value selection; use Selector instead.',
|
|
@@ -349,6 +361,8 @@ export const docsZh = {
|
|
|
349
361
|
'状态消息的放置方式:attached 直接叠加在输入框下方;detached 作为独立元素浮于下方并留有间距。',
|
|
350
362
|
renderOption:
|
|
351
363
|
'每个可选选项的自定义渲染函数。不会用于分隔线、分组或全选行。',
|
|
364
|
+
presentation:
|
|
365
|
+
'选项列表的呈现方式:锚定弹出框、底部抽屉,或根据紧凑触控屏自适应。',
|
|
352
366
|
xstyle: '布局自定义的 StyleX 样式,必须是 stylex.create() 值。',
|
|
353
367
|
},
|
|
354
368
|
},
|
|
@@ -487,6 +501,8 @@ export const docsDense = {
|
|
|
487
501
|
'status message placement; ghost detaches attached by default; use tooltip for compact toolbars.',
|
|
488
502
|
renderOption:
|
|
489
503
|
'custom render fn per selectable option; not dividers/sections/select-all',
|
|
504
|
+
presentation:
|
|
505
|
+
'popover, bottom-sheet, or compact-touch adaptive presentation',
|
|
490
506
|
xstyle: 'StyleX layout styles; stylex.create() only',
|
|
491
507
|
},
|
|
492
508
|
},
|
|
@@ -19,12 +19,15 @@ import {
|
|
|
19
19
|
within,
|
|
20
20
|
} from '@testing-library/react';
|
|
21
21
|
import userEvent from '@testing-library/user-event';
|
|
22
|
+
import * as stylex from '@stylexjs/stylex';
|
|
22
23
|
import {MultiSelector} from './MultiSelector';
|
|
23
24
|
import {Icon} from '../Icon';
|
|
24
25
|
import {InternationalizationProvider} from '../i18n';
|
|
25
26
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
27
|
+
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
26
28
|
import {defineTheme} from '../theme/defineTheme';
|
|
27
29
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
30
|
+
import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
|
|
28
31
|
|
|
29
32
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
30
33
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -40,6 +43,28 @@ function politeRegion(): HTMLElement | null {
|
|
|
40
43
|
|
|
41
44
|
// Mock showPopover and hidePopover methods since they're not implemented in jsdom
|
|
42
45
|
beforeEach(() => {
|
|
46
|
+
__resetInteractionModalityForTest();
|
|
47
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
48
|
+
this: HTMLDialogElement,
|
|
49
|
+
) {
|
|
50
|
+
this.setAttribute('open', '');
|
|
51
|
+
});
|
|
52
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
53
|
+
this.removeAttribute('open');
|
|
54
|
+
});
|
|
55
|
+
vi.stubGlobal(
|
|
56
|
+
'matchMedia',
|
|
57
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
58
|
+
matches: false,
|
|
59
|
+
media: query,
|
|
60
|
+
onchange: null,
|
|
61
|
+
addEventListener: vi.fn(),
|
|
62
|
+
removeEventListener: vi.fn(),
|
|
63
|
+
addListener: vi.fn(),
|
|
64
|
+
removeListener: vi.fn(),
|
|
65
|
+
dispatchEvent: vi.fn(),
|
|
66
|
+
})),
|
|
67
|
+
);
|
|
43
68
|
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
44
69
|
this.setAttribute('popover-open', '');
|
|
45
70
|
const event = new Event('toggle', {bubbles: false});
|
|
@@ -75,6 +100,108 @@ const h = {hidden: true} as const;
|
|
|
75
100
|
describe('MultiSelector', () => {
|
|
76
101
|
const defaultOptions = ['Apple', 'Banana', 'Orange'];
|
|
77
102
|
|
|
103
|
+
it('uses a bottom sheet and stays open while toggling options', async () => {
|
|
104
|
+
const user = userEvent.setup();
|
|
105
|
+
const onChange = vi.fn();
|
|
106
|
+
render(
|
|
107
|
+
<MultiSelector
|
|
108
|
+
label="Fruit"
|
|
109
|
+
options={defaultOptions}
|
|
110
|
+
value={[]}
|
|
111
|
+
onChange={onChange}
|
|
112
|
+
presentation="bottom-sheet"
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const trigger = screen.getByRole('combobox');
|
|
117
|
+
await user.click(trigger);
|
|
118
|
+
|
|
119
|
+
expect(
|
|
120
|
+
await screen.findByRole('dialog', {name: 'Fruit'}),
|
|
121
|
+
).toBeInTheDocument();
|
|
122
|
+
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
123
|
+
|
|
124
|
+
await user.click(screen.getByRole('option', {name: 'Banana'}));
|
|
125
|
+
expect(onChange).toHaveBeenCalledWith(['Banana']);
|
|
126
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('uses a bottom sheet for adaptive presentation on compact touch', async () => {
|
|
130
|
+
vi.stubGlobal(
|
|
131
|
+
'matchMedia',
|
|
132
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
133
|
+
matches: query === '(max-width: 768px) and (pointer: coarse)',
|
|
134
|
+
media: query,
|
|
135
|
+
onchange: null,
|
|
136
|
+
addEventListener: vi.fn(),
|
|
137
|
+
removeEventListener: vi.fn(),
|
|
138
|
+
addListener: vi.fn(),
|
|
139
|
+
removeListener: vi.fn(),
|
|
140
|
+
dispatchEvent: vi.fn(),
|
|
141
|
+
})),
|
|
142
|
+
);
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
render(
|
|
145
|
+
<MultiSelector
|
|
146
|
+
label="Fruit"
|
|
147
|
+
options={defaultOptions}
|
|
148
|
+
value={[]}
|
|
149
|
+
onChange={() => {}}
|
|
150
|
+
presentation="adaptive"
|
|
151
|
+
/>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await user.click(screen.getByRole('combobox'));
|
|
155
|
+
expect(
|
|
156
|
+
await screen.findByRole('dialog', {name: 'Fruit'}),
|
|
157
|
+
).toBeInTheDocument();
|
|
158
|
+
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('moves keyboard focus into a bottom-sheet listbox', async () => {
|
|
162
|
+
const user = userEvent.setup();
|
|
163
|
+
render(
|
|
164
|
+
<MultiSelector
|
|
165
|
+
label="Fruit"
|
|
166
|
+
options={defaultOptions}
|
|
167
|
+
value={[]}
|
|
168
|
+
onChange={() => {}}
|
|
169
|
+
presentation="bottom-sheet"
|
|
170
|
+
/>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const trigger = screen.getByRole('combobox');
|
|
174
|
+
trigger.focus();
|
|
175
|
+
await user.keyboard('{Enter}');
|
|
176
|
+
|
|
177
|
+
await waitFor(() => expect(screen.getByRole('listbox')).toHaveFocus());
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('restores touch focus without painting a trigger focus ring', async () => {
|
|
181
|
+
render(
|
|
182
|
+
<MultiSelector
|
|
183
|
+
label="Fruit"
|
|
184
|
+
options={defaultOptions}
|
|
185
|
+
value={[]}
|
|
186
|
+
onChange={() => {}}
|
|
187
|
+
presentation="bottom-sheet"
|
|
188
|
+
/>,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const trigger = screen.getByRole('combobox');
|
|
192
|
+
fireEvent.pointerDown(trigger, {pointerType: 'touch'});
|
|
193
|
+
fireEvent.click(trigger, {detail: 1});
|
|
194
|
+
const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
|
|
195
|
+
fireEvent.pointerDown(dialog, {pointerType: 'touch'});
|
|
196
|
+
fireEvent.click(dialog, {detail: 1});
|
|
197
|
+
|
|
198
|
+
trigger.focus();
|
|
199
|
+
expect(trigger).toHaveFocus();
|
|
200
|
+
expect(trigger.parentElement).toHaveClass(
|
|
201
|
+
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
78
205
|
it('renders with label', () => {
|
|
79
206
|
render(
|
|
80
207
|
<MultiSelector
|