@astryxdesign/core 0.7.0-canary.fe57f54 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/dist/BottomSheet/BottomSheet.d.ts +5 -4
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -1
- package/dist/Calendar/styles.d.ts +6 -6
- package/dist/Chat/ChatComposerInput.d.ts +0 -11
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +0 -11
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +1 -1
- package/dist/CommandPalette/CommandPaletteList.d.ts +3 -2
- package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteList.js +3 -2
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +4 -1
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +29 -13
- package/dist/DropdownMenu/DropdownMenu.d.ts +11 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +173 -11
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +13 -2
- package/dist/DropdownMenu/menuRegionsContext.d.ts +2 -0
- package/dist/DropdownMenu/menuRegionsContext.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionsContext.js +17 -0
- package/dist/Field/inputStyles.stylex.d.ts +3 -3
- package/dist/Layout/container.stylex.d.ts +18 -20
- package/dist/Layout/container.stylex.d.ts.map +1 -1
- package/dist/Layout/container.stylex.js +15 -25
- package/dist/Layout/padding.stylex.d.ts +231 -231
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +15 -6
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -1
- package/dist/Markdown/plugins/protocol.js +33 -8
- package/dist/NavItem/navItemStyles.stylex.d.ts +5 -5
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +21 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +30 -15
- package/dist/Stack/stack.stylex.d.ts +22 -22
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +89 -14
- package/dist/Table/table.stylex.d.ts +6 -6
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +4 -1
- package/dist/Tokenizer/Tokenizer.d.ts +1 -12
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +1 -13
- package/dist/Typeahead/BaseTypeahead.d.ts +0 -11
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +1 -9
- package/dist/Typeahead/Typeahead.d.ts +1 -12
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +1 -13
- package/dist/astryx.css +17 -12
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +5 -1
- package/dist/hooks/useRenamedProp.d.ts +6 -6
- package/dist/hooks/useRenamedProp.js +6 -6
- package/dist/reset.a11y.chromium.spec.d.ts +2 -0
- package/dist/reset.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/reset.a11y.chromium.spec.js +176 -0
- package/dist/theme/tokens.stylex.d.ts +30 -30
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +18 -15
- package/dist/theme/useTheme.d.ts +1 -1
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +4 -4
- package/src/BottomSheet/BottomSheet.tsx +5 -4
- package/src/BottomSheet/BottomSheetPanel.test.tsx +6 -9
- package/src/BottomSheet/BottomSheetPanel.tsx +2 -3
- package/src/Chat/ChatComposerInput.doc.mjs +3 -3
- package/src/Chat/ChatComposerInput.test.tsx +7 -55
- package/src/Chat/ChatComposerInput.tsx +0 -36
- package/src/Chat/useTriggerMenu.tsx +1 -4
- package/src/CommandPalette/CommandPalette.spec.md +2 -0
- package/src/CommandPalette/CommandPaletteList.spec.md +189 -0
- package/src/CommandPalette/CommandPaletteList.test.tsx +47 -0
- package/src/CommandPalette/CommandPaletteList.tsx +3 -2
- package/src/DateInput/TouchDateField.tsx +3 -1
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +60 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +31 -14
- package/src/DropdownMenu/DropdownMenu.doc.mjs +12 -0
- package/src/DropdownMenu/DropdownMenu.spec.md +65 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +457 -0
- package/src/DropdownMenu/DropdownMenu.tsx +278 -49
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +6 -3
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +2 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -1
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +195 -0
- package/src/DropdownMenu/menuRegionsContext.ts +17 -0
- package/src/Layout/container.stylex.ts +11 -21
- package/src/Lightbox/Lightbox.test.tsx +58 -0
- package/src/Lightbox/Lightbox.tsx +12 -6
- package/src/Markdown/plugins/protocol.ts +45 -12
- package/src/Markdown/plugins/sourceLines.test.tsx +121 -0
- package/src/PowerSearch/PowerSearch.tsx +28 -1
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +284 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +41 -13
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +33 -9
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +81 -18
- package/src/TimeInput/TouchTimeField.tsx +3 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +0 -16
- package/src/Tokenizer/Tokenizer.test.tsx +12 -63
- package/src/Tokenizer/Tokenizer.tsx +1 -25
- package/src/Toolbar/Toolbar.test.tsx +29 -0
- package/src/Typeahead/BaseTypeahead.doc.mjs +0 -16
- package/src/Typeahead/BaseTypeahead.test.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.tsx +1 -21
- package/src/Typeahead/Typeahead.doc.mjs +0 -7
- package/src/Typeahead/Typeahead.test.tsx +24 -42
- package/src/Typeahead/Typeahead.tsx +1 -25
- package/src/hooks/useFocusTrap.ts +5 -1
- package/src/hooks/useRenamedProp.ts +6 -6
- package/src/reset.a11y.chromium.spec.ts +178 -0
- package/src/reset.css +8 -8
- package/src/reset.test.ts +112 -0
- package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +198 -0
- package/src/theme/tokens.stylex.ts +18 -15
- package/src/theme/tokens.test.ts +1 -1
- package/src/theme/useTheme.test.tsx +4 -4
- package/src/theme/useTheme.ts +1 -1
|
@@ -69,6 +69,8 @@ import type {MenuPressPointerType} from '../hooks/menuPressGesture';
|
|
|
69
69
|
import {useTypeahead} from '../hooks/useTypeahead';
|
|
70
70
|
import {useFocusReturnVisibility} from '../hooks/useFocusReturnVisibility';
|
|
71
71
|
import {useMenuOverflow} from './useMenuOverflow';
|
|
72
|
+
import {MenuRegionsContext} from './menuRegionsContext';
|
|
73
|
+
import {getFocusableElements} from '../hooks/useFocusTrap';
|
|
72
74
|
import {useDevWarning} from '../hooks/useDevWarning';
|
|
73
75
|
import {useMenuDrillIn} from './useMenuDrillIn';
|
|
74
76
|
import {resolveMenuWidth} from './menuWidth';
|
|
@@ -88,10 +90,13 @@ import {
|
|
|
88
90
|
durationVars,
|
|
89
91
|
easeVars,
|
|
90
92
|
} from '../theme/tokens.stylex';
|
|
91
|
-
import {mergeProps, rtlStyles} from '../utils';
|
|
93
|
+
import {isRenderable, mergeProps, rtlStyles} from '../utils';
|
|
92
94
|
import type {BaseProps} from '../BaseProps';
|
|
93
95
|
import {themeProps} from '../utils/themeProps';
|
|
94
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
getInteractionModality,
|
|
98
|
+
useInteractionModalityTracking,
|
|
99
|
+
} from '../utils/interactionModality';
|
|
95
100
|
import {useTranslator} from '../i18n';
|
|
96
101
|
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
97
102
|
|
|
@@ -102,6 +107,54 @@ const MENU_MAX_BLOCK_SIZE = `min(${MENU_BLOCK_CAP}, ${layerViewportInset.maxBloc
|
|
|
102
107
|
const MENU_MAX_BLOCK_SIZE_FALLBACK = `min(${MENU_BLOCK_CAP}, ${layerViewportInset.maxBlockSizeFallback})`;
|
|
103
108
|
const MENU_TRIGGER_OPEN_BACKGROUND = `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`;
|
|
104
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The elements Tab reaches inside a header or footer region, in order. Hidden
|
|
112
|
+
* controls (`hidden`, `display: none`, `visibility: hidden`, inert or
|
|
113
|
+
* aria-hidden subtrees) are not reachable, judged by the popover focus trap's
|
|
114
|
+
* own test so the two never disagree about where Tab can go.
|
|
115
|
+
*/
|
|
116
|
+
function tabbablesIn(region: HTMLElement | null): HTMLElement[] {
|
|
117
|
+
if (region == null) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return getFocusableElements(region).filter(
|
|
121
|
+
element => element.tabIndex >= 0 && !isInsideHiddenWrapper(element, region),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether a `display: none` ancestor between the element and its region hides
|
|
127
|
+
* it — a control in a wrapper hidden at some widths, say. The trap's test reads
|
|
128
|
+
* only the element's own display.
|
|
129
|
+
*/
|
|
130
|
+
function isInsideHiddenWrapper(
|
|
131
|
+
element: HTMLElement,
|
|
132
|
+
region: HTMLElement,
|
|
133
|
+
): boolean {
|
|
134
|
+
if (typeof window === 'undefined' || !window.getComputedStyle) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
for (
|
|
138
|
+
let node = element.parentElement;
|
|
139
|
+
node != null && node !== region.parentElement;
|
|
140
|
+
node = node.parentElement
|
|
141
|
+
) {
|
|
142
|
+
if (window.getComputedStyle(node).display === 'none') {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Move focus to a region control. Reports whether focus actually arrived, so a
|
|
151
|
+
* caller can close the menu instead of stranding focus on the page.
|
|
152
|
+
*/
|
|
153
|
+
function focusRegionControl(element: HTMLElement): boolean {
|
|
154
|
+
element.focus();
|
|
155
|
+
return element.ownerDocument.activeElement === element;
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
const styles = stylex.create({
|
|
106
159
|
triggerOpen: {
|
|
107
160
|
backgroundImage: MENU_TRIGGER_OPEN_BACKGROUND,
|
|
@@ -148,6 +201,27 @@ const styles = stylex.create({
|
|
|
148
201
|
overflowX: 'hidden',
|
|
149
202
|
overscrollBehavior: 'contain',
|
|
150
203
|
},
|
|
204
|
+
// With a header or footer, the regions and the menu share one column under
|
|
205
|
+
// the menu's block cap: the regions keep their height and the menu shrinks
|
|
206
|
+
// and scrolls between them.
|
|
207
|
+
regionsFrame: {
|
|
208
|
+
boxSizing: 'border-box',
|
|
209
|
+
display: 'flex',
|
|
210
|
+
flexDirection: 'column',
|
|
211
|
+
maxHeight: stylex.firstThatWorks(
|
|
212
|
+
MENU_MAX_BLOCK_SIZE,
|
|
213
|
+
MENU_MAX_BLOCK_SIZE_FALLBACK,
|
|
214
|
+
),
|
|
215
|
+
},
|
|
216
|
+
menuInFrame: {
|
|
217
|
+
flexGrow: 1,
|
|
218
|
+
flexShrink: 1,
|
|
219
|
+
minBlockSize: 0,
|
|
220
|
+
},
|
|
221
|
+
region: {
|
|
222
|
+
flexShrink: 0,
|
|
223
|
+
padding: spacingVars['--spacing-1'],
|
|
224
|
+
},
|
|
151
225
|
// Scroll ownership by the browser's own signal. A menu whose rows fit keeps
|
|
152
226
|
// every one-finger move: a slide over its rows stays a slide. One that
|
|
153
227
|
// scrolls lets the browser pan it vertically and cancel the press when it
|
|
@@ -216,6 +290,12 @@ const bottomSheetStyles = stylex.create({
|
|
|
216
290
|
viewHeading: {
|
|
217
291
|
outline: 'none',
|
|
218
292
|
},
|
|
293
|
+
// Caller content above or below the action list, inset to the list's
|
|
294
|
+
// content edge.
|
|
295
|
+
region: {
|
|
296
|
+
paddingInline: spacingVars['--spacing-3'],
|
|
297
|
+
paddingBlock: spacingVars['--spacing-2'],
|
|
298
|
+
},
|
|
219
299
|
});
|
|
220
300
|
|
|
221
301
|
// =============================================================================
|
|
@@ -371,6 +451,17 @@ interface DropdownMenuBaseProps extends BaseProps {
|
|
|
371
451
|
* @default 'start'
|
|
372
452
|
*/
|
|
373
453
|
alignment?: LayerAlignment;
|
|
454
|
+
/**
|
|
455
|
+
* Content above the menu's rows and outside the menu itself — a filter
|
|
456
|
+
* field, a toggle. The menu holds only its rows, so a control that is not
|
|
457
|
+
* a row goes here or in `footer`, never among the rows.
|
|
458
|
+
*/
|
|
459
|
+
header?: ReactNode;
|
|
460
|
+
/**
|
|
461
|
+
* Content below the menu's rows and outside the menu itself — a "Create"
|
|
462
|
+
* button. See `header`.
|
|
463
|
+
*/
|
|
464
|
+
footer?: ReactNode;
|
|
374
465
|
|
|
375
466
|
'data-testid'?: string;
|
|
376
467
|
}
|
|
@@ -508,6 +599,8 @@ function DropdownMenuBottomSheet({
|
|
|
508
599
|
menuMaxHeight: _menuMaxHeight,
|
|
509
600
|
placement: _placement,
|
|
510
601
|
alignment: _alignment,
|
|
602
|
+
header,
|
|
603
|
+
footer,
|
|
511
604
|
className,
|
|
512
605
|
style,
|
|
513
606
|
xstyle,
|
|
@@ -718,11 +811,17 @@ function DropdownMenuBottomSheet({
|
|
|
718
811
|
{currentTitle}
|
|
719
812
|
</Heading>
|
|
720
813
|
</div>
|
|
814
|
+
{isRenderable(header) && (
|
|
815
|
+
<div {...stylex.props(bottomSheetStyles.region)}>{header}</div>
|
|
816
|
+
)}
|
|
721
817
|
<MenuBottomSheetActionList
|
|
722
818
|
items={currentItems}
|
|
723
819
|
onSelect={handleSelect}
|
|
724
820
|
onOpenSubmenu={item => setSubmenuPath(path => [...path, item])}
|
|
725
821
|
/>
|
|
822
|
+
{isRenderable(footer) && (
|
|
823
|
+
<div {...stylex.props(bottomSheetStyles.region)}>{footer}</div>
|
|
824
|
+
)}
|
|
726
825
|
</div>
|
|
727
826
|
</MenuBottomSheet>
|
|
728
827
|
</>
|
|
@@ -741,6 +840,8 @@ function DropdownMenuPopover({
|
|
|
741
840
|
placement = 'below',
|
|
742
841
|
alignment = 'start',
|
|
743
842
|
presentation: _presentation,
|
|
843
|
+
header,
|
|
844
|
+
footer,
|
|
744
845
|
className,
|
|
745
846
|
style,
|
|
746
847
|
xstyle,
|
|
@@ -786,6 +887,16 @@ function DropdownMenuPopover({
|
|
|
786
887
|
resetFocusReturn,
|
|
787
888
|
} = useFocusReturnVisibility();
|
|
788
889
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
890
|
+
const headerRef = useRef<HTMLDivElement | null>(null);
|
|
891
|
+
const footerRef = useRef<HTMLDivElement | null>(null);
|
|
892
|
+
const hasRegions = useMemo(
|
|
893
|
+
() => isRenderable(header) || isRenderable(footer),
|
|
894
|
+
[header, footer],
|
|
895
|
+
);
|
|
896
|
+
// Whether the menu element itself took focus from the keyboard. A pointer
|
|
897
|
+
// open focuses it programmatically, and pointer history must not paint the
|
|
898
|
+
// keyboard ring (architecture:interaction-modality INV1).
|
|
899
|
+
const [isMenuKeyboardFocused, setIsMenuKeyboardFocused] = useState(false);
|
|
789
900
|
// The press model is mounted below (it needs the popover); the hide path
|
|
790
901
|
// above it ends the gesture in flight through this ref.
|
|
791
902
|
const cancelMenuPressRef = useRef<() => void>(() => {});
|
|
@@ -966,6 +1077,23 @@ function DropdownMenuPopover({
|
|
|
966
1077
|
// A submenu flyout renders inline inside this menu; its key events bubble
|
|
967
1078
|
// up here. Let that level own them — only handle events from this level.
|
|
968
1079
|
if (!ownsEvent(e)) {
|
|
1080
|
+
// Except Tab with a header or footer: each open flyout has closed
|
|
1081
|
+
// itself as the key bubbled up, and the root moves focus on as Tab
|
|
1082
|
+
// from one of its own rows would.
|
|
1083
|
+
if (e.key === 'Tab' && hasRegions) {
|
|
1084
|
+
const region = e.shiftKey ? headerRef.current : footerRef.current;
|
|
1085
|
+
const tabbables = tabbablesIn(region);
|
|
1086
|
+
e.stopPropagation();
|
|
1087
|
+
const target = e.shiftKey
|
|
1088
|
+
? tabbables[tabbables.length - 1]
|
|
1089
|
+
: tabbables[0];
|
|
1090
|
+
if (target != null) {
|
|
1091
|
+
e.preventDefault();
|
|
1092
|
+
}
|
|
1093
|
+
if (target == null || !focusRegionControl(target)) {
|
|
1094
|
+
closeMenu();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
969
1097
|
return;
|
|
970
1098
|
}
|
|
971
1099
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -991,7 +1119,27 @@ function DropdownMenuPopover({
|
|
|
991
1119
|
// otherwise leak into the page while the menu stayed open (menus-5).
|
|
992
1120
|
// Do NOT preventDefault — closing restores focus to the trigger, and the
|
|
993
1121
|
// browser's default Tab then continues from there to the next element.
|
|
1122
|
+
// A header or footer with something Tab can reach in that direction
|
|
1123
|
+
// takes the Tab instead, and the menu stays open.
|
|
994
1124
|
if (e.key === 'Tab') {
|
|
1125
|
+
const region = e.shiftKey ? headerRef.current : footerRef.current;
|
|
1126
|
+
const tabbables = tabbablesIn(region);
|
|
1127
|
+
if (tabbables.length > 0) {
|
|
1128
|
+
// From a row, the browser's Shift+Tab would stop on the menu element
|
|
1129
|
+
// itself — the row's ancestor and a Tab stop — before reaching the
|
|
1130
|
+
// header. The menu is one stop, so go straight to the header.
|
|
1131
|
+
if (e.shiftKey && e.target !== listRef.current) {
|
|
1132
|
+
e.preventDefault();
|
|
1133
|
+
// The popover's focus trap listens on the document and would read
|
|
1134
|
+
// this same Shift+Tab against the field just focused — the trap's
|
|
1135
|
+
// first element — and wrap to the footer. This key is handled.
|
|
1136
|
+
e.stopPropagation();
|
|
1137
|
+
if (!focusRegionControl(tabbables[tabbables.length - 1])) {
|
|
1138
|
+
closeMenu();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
995
1143
|
closeMenu();
|
|
996
1144
|
return;
|
|
997
1145
|
}
|
|
@@ -1002,7 +1150,33 @@ function DropdownMenuPopover({
|
|
|
1002
1150
|
}
|
|
1003
1151
|
listNavKeyDown(e);
|
|
1004
1152
|
},
|
|
1005
|
-
[listNavKeyDown, closeMenu, typeahead, ownsEvent],
|
|
1153
|
+
[listNavKeyDown, closeMenu, typeahead, ownsEvent, listRef, hasRegions],
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
// Tab past the last control of the footer, or Shift+Tab before the first
|
|
1157
|
+
// control of the header, leaves the menu as Tab from its rows does: the
|
|
1158
|
+
// menu closes, focus returns to the trigger, and the browser's own Tab
|
|
1159
|
+
// continues from there. Between the regions, Tab moves through the menu
|
|
1160
|
+
// as one stop.
|
|
1161
|
+
const handleRegionKeyDown = useCallback(
|
|
1162
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
1163
|
+
if (e.key !== 'Tab') {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const tabbables = tabbablesIn(e.currentTarget);
|
|
1167
|
+
const isHeader = e.currentTarget === headerRef.current;
|
|
1168
|
+
const exit = isHeader
|
|
1169
|
+
? e.shiftKey
|
|
1170
|
+
? tabbables[0]
|
|
1171
|
+
: undefined
|
|
1172
|
+
: e.shiftKey
|
|
1173
|
+
? undefined
|
|
1174
|
+
: tabbables[tabbables.length - 1];
|
|
1175
|
+
if (exit != null && exit === document.activeElement) {
|
|
1176
|
+
closeMenu();
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
[closeMenu],
|
|
1006
1180
|
);
|
|
1007
1181
|
|
|
1008
1182
|
const openAndFocus = useCallback(
|
|
@@ -1215,6 +1389,38 @@ function DropdownMenuPopover({
|
|
|
1215
1389
|
);
|
|
1216
1390
|
const hasOverflow = useMenuOverflow(listRef, menuContent, popover.isOpen);
|
|
1217
1391
|
|
|
1392
|
+
// The header and footer sit beside the menu inside the popover, never inside
|
|
1393
|
+
// the role="menu" element: a menu holds only its rows.
|
|
1394
|
+
const renderWithRegions = (menu: ReactNode): ReactNode =>
|
|
1395
|
+
hasRegions ? (
|
|
1396
|
+
<div
|
|
1397
|
+
{...stylex.props(
|
|
1398
|
+
styles.regionsFrame,
|
|
1399
|
+
resolvedMaxHeight != null &&
|
|
1400
|
+
dynamicStyles.menuMaxHeight(resolvedMaxHeight),
|
|
1401
|
+
)}>
|
|
1402
|
+
{isRenderable(header) && (
|
|
1403
|
+
<div
|
|
1404
|
+
ref={headerRef}
|
|
1405
|
+
onKeyDown={handleRegionKeyDown}
|
|
1406
|
+
{...stylex.props(styles.region)}>
|
|
1407
|
+
{header}
|
|
1408
|
+
</div>
|
|
1409
|
+
)}
|
|
1410
|
+
{menu}
|
|
1411
|
+
{isRenderable(footer) && (
|
|
1412
|
+
<div
|
|
1413
|
+
ref={footerRef}
|
|
1414
|
+
onKeyDown={handleRegionKeyDown}
|
|
1415
|
+
{...stylex.props(styles.region)}>
|
|
1416
|
+
{footer}
|
|
1417
|
+
</div>
|
|
1418
|
+
)}
|
|
1419
|
+
</div>
|
|
1420
|
+
) : (
|
|
1421
|
+
menu
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1218
1424
|
const triggerProps: DropdownMenuTriggerProps = {
|
|
1219
1425
|
ref: el => {
|
|
1220
1426
|
buttonRef.current = el;
|
|
@@ -1286,52 +1492,75 @@ function DropdownMenuPopover({
|
|
|
1286
1492
|
/>
|
|
1287
1493
|
)}
|
|
1288
1494
|
{popover.render(
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1495
|
+
renderWithRegions(
|
|
1496
|
+
<div
|
|
1497
|
+
{...rest}
|
|
1498
|
+
ref={el => {
|
|
1499
|
+
listRef.current = el;
|
|
1500
|
+
contentRef.current =
|
|
1501
|
+
el?.closest<HTMLDivElement>('[popover]') ?? null;
|
|
1502
|
+
}}
|
|
1503
|
+
id={menuId}
|
|
1504
|
+
role="menu"
|
|
1505
|
+
// Pointer opens focus the container so arrows, typeahead, Escape and
|
|
1506
|
+
// Tab remain owned by the menu without pre-highlighting an item.
|
|
1507
|
+
// An overflowing menu joins the Tab order so its scrollable region is
|
|
1508
|
+
// keyboard-accessible; Tab still dismisses through listKeyDown. With
|
|
1509
|
+
// a header or footer the menu is one Tab stop between them.
|
|
1510
|
+
tabIndex={hasOverflow || hasRegions ? 0 : -1}
|
|
1511
|
+
// Give the menu an accessible name from its trigger's label, so
|
|
1512
|
+
// screen readers announce e.g. "Actions menu" rather than an unnamed
|
|
1513
|
+
// menu (menus-13). A custom trigger names it by reference.
|
|
1514
|
+
aria-label={
|
|
1515
|
+
renderTrigger != null
|
|
1516
|
+
? (rest['aria-label'] as string | undefined)
|
|
1517
|
+
: button.label
|
|
1518
|
+
}
|
|
1519
|
+
aria-labelledby={
|
|
1520
|
+
renderTrigger != null && rest['aria-label'] == null
|
|
1521
|
+
? triggerId
|
|
1522
|
+
: (rest['aria-labelledby'] as string | undefined)
|
|
1523
|
+
}
|
|
1524
|
+
onKeyDown={listKeyDown}
|
|
1525
|
+
onFocus={event => {
|
|
1526
|
+
(
|
|
1527
|
+
rest.onFocus as
|
|
1528
|
+
React.FocusEventHandler<HTMLDivElement> | undefined
|
|
1529
|
+
)?.(event);
|
|
1530
|
+
if (event.target === event.currentTarget) {
|
|
1531
|
+
setIsMenuKeyboardFocused(
|
|
1532
|
+
getInteractionModality() === 'keyboard',
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
}}
|
|
1536
|
+
{...menuPress.menuProps}
|
|
1537
|
+
{...mergeProps(
|
|
1538
|
+
themeProps('dropdown-menu'),
|
|
1539
|
+
stylex.props(
|
|
1540
|
+
styles.dropdown,
|
|
1541
|
+
resolvedMaxHeight != null &&
|
|
1542
|
+
dynamicStyles.menuMaxHeight(resolvedMaxHeight),
|
|
1543
|
+
hasOverflow ? styles.touchPanYPinchZoom : styles.touchPinchZoom,
|
|
1544
|
+
hasOverflow && styles.scrollable,
|
|
1545
|
+
hasRegions && styles.menuInFrame,
|
|
1546
|
+
// Between a header and a footer the menu is a Tab stop, so
|
|
1547
|
+
// keyboard focus on it must be visible (the base style removes
|
|
1548
|
+
// the ring for the pointer-open focus target).
|
|
1549
|
+
hasRegions &&
|
|
1550
|
+
isMenuKeyboardFocused &&
|
|
1551
|
+
focusOutlineStyles.focusVisible,
|
|
1552
|
+
xstyle,
|
|
1553
|
+
),
|
|
1554
|
+
className,
|
|
1555
|
+
style,
|
|
1556
|
+
)}>
|
|
1557
|
+
<MenuRegionsContext value={hasRegions}>
|
|
1558
|
+
<DropdownMenuContext value={contextValue}>
|
|
1559
|
+
{menuContent}
|
|
1560
|
+
</DropdownMenuContext>
|
|
1561
|
+
</MenuRegionsContext>
|
|
1562
|
+
</div>,
|
|
1563
|
+
),
|
|
1335
1564
|
{
|
|
1336
1565
|
placement,
|
|
1337
1566
|
alignment,
|
|
@@ -29,12 +29,14 @@ export const docs = {
|
|
|
29
29
|
{
|
|
30
30
|
name: 'value',
|
|
31
31
|
type: 'boolean',
|
|
32
|
-
description:
|
|
32
|
+
description:
|
|
33
|
+
'Whether the item is checked. Controlled; pair with onChange.',
|
|
33
34
|
},
|
|
34
35
|
{
|
|
35
36
|
name: 'onChange',
|
|
36
37
|
type: '(checked: boolean) => void',
|
|
37
|
-
description:
|
|
38
|
+
description:
|
|
39
|
+
'Callback fired with the next checked state when the item is toggled.',
|
|
38
40
|
},
|
|
39
41
|
{
|
|
40
42
|
name: 'isDisabled',
|
|
@@ -88,7 +90,8 @@ export const docsDense = {
|
|
|
88
90
|
name: 'DropdownMenuCheckboxItem',
|
|
89
91
|
isHiddenFromOverview: true,
|
|
90
92
|
displayName: 'Dropdown Menu Checkbox Item',
|
|
91
|
-
description:
|
|
93
|
+
description:
|
|
94
|
+
'checkable menu item (menuitemcheckbox); independent boolean toggle',
|
|
92
95
|
propDescriptions: {
|
|
93
96
|
label: 'primary label text',
|
|
94
97
|
description: 'secondary text below label',
|
|
@@ -78,7 +78,8 @@ export const docsZh = {
|
|
|
78
78
|
propDescriptions: {
|
|
79
79
|
value: '组中当前选中的值。尚无选中项时传 undefined。',
|
|
80
80
|
onChange: '所选值变化时触发的回调。',
|
|
81
|
-
label:
|
|
81
|
+
label:
|
|
82
|
+
'组的无障碍名称,作为 aria-label 应用,以便屏幕阅读器将单选项作为命名集合朗读,例如“Sort by”。必需。当名称已作为可见元素存在时,可改用 aria-labelledby(通过基础属性)。',
|
|
82
83
|
hasCloseOnSelect: '选择某值是否关闭菜单。默认关闭。',
|
|
83
84
|
children: '组成该组的 DropdownMenuRadioItem。',
|
|
84
85
|
},
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import React, {
|
|
53
|
+
use,
|
|
53
54
|
useCallback,
|
|
54
55
|
useEffect,
|
|
55
56
|
useId,
|
|
@@ -63,6 +64,7 @@ import {createPortal} from 'react-dom';
|
|
|
63
64
|
import * as stylex from '@stylexjs/stylex';
|
|
64
65
|
import {Icon, renderIconSlot, type IconType} from '../Icon';
|
|
65
66
|
import {Spinner} from '../Spinner';
|
|
67
|
+
import {MenuRegionsContext} from './menuRegionsContext';
|
|
66
68
|
import {Item} from '../Item';
|
|
67
69
|
import {useLayer} from '../Layer/useLayer';
|
|
68
70
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
@@ -356,6 +358,7 @@ export function DropdownMenuSubMenu(
|
|
|
356
358
|
|
|
357
359
|
const t = useTranslator();
|
|
358
360
|
const menuCtx = useDropdownMenuContext();
|
|
361
|
+
const rootHasRegions = use(MenuRegionsContext);
|
|
359
362
|
const menuSize = menuCtx?.menuSize ?? 'md';
|
|
360
363
|
const canOpen = !isDisabled;
|
|
361
364
|
|
|
@@ -655,6 +658,13 @@ export function DropdownMenuSubMenu(
|
|
|
655
658
|
// stopPropagation needed — each level self-filters.
|
|
656
659
|
const handleContentKeyDown = useCallback(
|
|
657
660
|
(e: React.KeyboardEvent) => {
|
|
661
|
+
// In a menu with a header or footer, Tab leaves the whole flyout stack:
|
|
662
|
+
// every open level closes as the key bubbles past it — this one's own
|
|
663
|
+
// rows or a deeper level's — and the root menu moves focus to a region.
|
|
664
|
+
if (e.key === 'Tab' && rootHasRegions) {
|
|
665
|
+
close({focusTrigger: false});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
658
668
|
if (!ownsEvent(e)) {
|
|
659
669
|
return;
|
|
660
670
|
}
|
|
@@ -702,7 +712,7 @@ export function DropdownMenuSubMenu(
|
|
|
702
712
|
}
|
|
703
713
|
listNavKeyDown(e);
|
|
704
714
|
},
|
|
705
|
-
[ownsEvent, close, listNavKeyDown, typeahead, menuRef],
|
|
715
|
+
[ownsEvent, close, listNavKeyDown, typeahead, menuRef, rootHasRegions],
|
|
706
716
|
);
|
|
707
717
|
|
|
708
718
|
// Re-provide the menu context so nested items behave exactly like top-level
|