@astryxdesign/core 0.7.2 → 0.7.3-canary.02f5fe4
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.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +7 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +58 -9
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +34 -54
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +13 -10
- package/src/TimeInput/TimeInput.test.tsx +139 -7
- package/src/TimeInput/TimeInput.tsx +53 -24
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -61,7 +61,10 @@ import {
|
|
|
61
61
|
import {
|
|
62
62
|
DropdownMenuContext,
|
|
63
63
|
DropdownMenuRootOpenContext,
|
|
64
|
+
DropdownMenuSurfaceContext,
|
|
64
65
|
type DropdownMenuContextValue,
|
|
66
|
+
type DropdownMenuSurfaceHost,
|
|
67
|
+
type DropdownMenuSurfaceValue,
|
|
65
68
|
} from './DropdownMenuContext';
|
|
66
69
|
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
67
70
|
import {useListFocus} from '../hooks/useListFocus';
|
|
@@ -112,6 +115,14 @@ const MENU_MAX_BLOCK_SIZE = `min(${MENU_BLOCK_CAP}, ${layerViewportInset.maxBloc
|
|
|
112
115
|
const MENU_MAX_BLOCK_SIZE_FALLBACK = `min(${MENU_BLOCK_CAP}, ${layerViewportInset.maxBlockSizeFallback})`;
|
|
113
116
|
const MENU_TRIGGER_OPEN_BACKGROUND = `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`;
|
|
114
117
|
|
|
118
|
+
// A sub-menu's drilled-in view with a header or footer renders in the surface
|
|
119
|
+
// host that follows the menu, as the frame marked
|
|
120
|
+
// `data-astryx-submenu-regions="view"` (hidden while a deeper view shows).
|
|
121
|
+
// While one shows, the menu and its header and footer step aside. The rule
|
|
122
|
+
// reads that from the DOM, so no state mirrors which view shows.
|
|
123
|
+
const STEPS_ASIDE_FOR_VIEW =
|
|
124
|
+
':has(~ * > [data-astryx-submenu-regions="view"]:not([hidden]))';
|
|
125
|
+
|
|
115
126
|
const styles = stylex.create({
|
|
116
127
|
triggerOpen: {
|
|
117
128
|
backgroundImage: MENU_TRIGGER_OPEN_BACKGROUND,
|
|
@@ -125,7 +136,12 @@ const styles = stylex.create({
|
|
|
125
136
|
},
|
|
126
137
|
dropdown: {
|
|
127
138
|
boxSizing: 'border-box',
|
|
128
|
-
display:
|
|
139
|
+
display: {
|
|
140
|
+
default: 'flex',
|
|
141
|
+
// A sub-menu's drilled-in view with a header or footer shows in its
|
|
142
|
+
// place (see `stepsAsideForView`).
|
|
143
|
+
[STEPS_ASIDE_FOR_VIEW]: 'none',
|
|
144
|
+
},
|
|
129
145
|
flexDirection: 'column',
|
|
130
146
|
gap: spacingVars['--spacing-0-5'],
|
|
131
147
|
// Pointer opens focus this container so subsequent arrow keys and Escape
|
|
@@ -175,6 +191,22 @@ const styles = stylex.create({
|
|
|
175
191
|
flexShrink: 1,
|
|
176
192
|
minBlockSize: 0,
|
|
177
193
|
},
|
|
194
|
+
// Where a sub-menu with a header or footer renders its surface: beside the
|
|
195
|
+
// menu, never inside it. It owns no box, so the popover's layout is
|
|
196
|
+
// unchanged.
|
|
197
|
+
surfaceHost: {
|
|
198
|
+
display: 'contents',
|
|
199
|
+
},
|
|
200
|
+
// While a sub-menu's drilled-in view with its own header or footer shows
|
|
201
|
+
// in the surface host, the header and footer step aside for it, as the menu
|
|
202
|
+
// does (see `dropdown`). Derived from the DOM by the rule itself: the view
|
|
203
|
+
// is the frame in the host that is not hidden.
|
|
204
|
+
stepsAsideForView: {
|
|
205
|
+
display: {
|
|
206
|
+
default: null,
|
|
207
|
+
[STEPS_ASIDE_FOR_VIEW]: 'none',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
178
210
|
region: {
|
|
179
211
|
flexShrink: 0,
|
|
180
212
|
padding: spacingVars['--spacing-1'],
|
|
@@ -1455,12 +1487,39 @@ function DropdownMenuPopover({
|
|
|
1455
1487
|
|
|
1456
1488
|
// The drill-in view stack for sub-menus on a phone.
|
|
1457
1489
|
const {drillIn, wrapContent} = useMenuDrillIn(isOpen);
|
|
1490
|
+
// The surface host is owned through a ref. State keeps only a key that
|
|
1491
|
+
// changes whenever a host element attaches, the host included when it
|
|
1492
|
+
// moves (a header or footer coming or going), so sub-menus re-render into
|
|
1493
|
+
// the element that is mounted.
|
|
1494
|
+
const surfaceHostRef = useRef<HTMLDivElement | null>(null);
|
|
1495
|
+
const [surfaceHostKey, setSurfaceHostKey] = useState(0);
|
|
1496
|
+
const attachSurfaceHost = useCallback((node: HTMLDivElement | null) => {
|
|
1497
|
+
surfaceHostRef.current = node;
|
|
1498
|
+
if (node != null) {
|
|
1499
|
+
setSurfaceHostKey(key => key + 1);
|
|
1500
|
+
}
|
|
1501
|
+
}, []);
|
|
1502
|
+
const surfaceHost = useMemo<DropdownMenuSurfaceHost>(
|
|
1503
|
+
() => ({
|
|
1504
|
+
getElement: () => (surfaceHostKey > 0 ? surfaceHostRef.current : null),
|
|
1505
|
+
}),
|
|
1506
|
+
[surfaceHostKey],
|
|
1507
|
+
);
|
|
1458
1508
|
const resolvedMaxHeight = menuMaxHeight == null ? null : `${menuMaxHeight}px`;
|
|
1459
1509
|
// Context for compound items
|
|
1460
1510
|
const contextValue = useMemo<DropdownMenuContextValue>(
|
|
1461
1511
|
() => ({closeMenu, menuSize, drillIn, menuLabel}),
|
|
1462
1512
|
[closeMenu, menuSize, drillIn, menuLabel],
|
|
1463
1513
|
);
|
|
1514
|
+
// What a sub-menu with a header or footer needs, on an internal context.
|
|
1515
|
+
const surfaceValue = useMemo<DropdownMenuSurfaceValue>(
|
|
1516
|
+
() => ({
|
|
1517
|
+
host: surfaceHost,
|
|
1518
|
+
ownerId: menuId,
|
|
1519
|
+
menuMaxHeight: resolvedMaxHeight ?? undefined,
|
|
1520
|
+
}),
|
|
1521
|
+
[surfaceHost, menuId, resolvedMaxHeight],
|
|
1522
|
+
);
|
|
1464
1523
|
|
|
1465
1524
|
// Resolve menu content: data-driven items become components
|
|
1466
1525
|
const menuContent = wrapContent(
|
|
@@ -1469,7 +1528,13 @@ function DropdownMenuPopover({
|
|
|
1469
1528
|
const hasOverflow = useMenuOverflow(listRef, menuContent, popover.isOpen);
|
|
1470
1529
|
|
|
1471
1530
|
// The header and footer sit beside the menu inside the popover, never inside
|
|
1472
|
-
// the role="menu" element: a menu holds only its rows.
|
|
1531
|
+
// the role="menu" element: a menu holds only its rows. The surface host
|
|
1532
|
+
// follows the menu, so a sub-menu with a header or footer renders beside the
|
|
1533
|
+
// menu too; while such a sub-menu's drilled-in view shows there, the menu and
|
|
1534
|
+
// the regions step aside (STEPS_ASIDE_FOR_VIEW).
|
|
1535
|
+
const surfaceHostElement = (
|
|
1536
|
+
<div ref={attachSurfaceHost} {...stylex.props(styles.surfaceHost)} />
|
|
1537
|
+
);
|
|
1473
1538
|
const renderWithRegions = (menu: ReactNode): ReactNode =>
|
|
1474
1539
|
hasRegions ? (
|
|
1475
1540
|
<div
|
|
@@ -1482,7 +1547,7 @@ function DropdownMenuPopover({
|
|
|
1482
1547
|
<div
|
|
1483
1548
|
ref={headerRef}
|
|
1484
1549
|
onKeyDown={handleRegionKeyDown}
|
|
1485
|
-
{...stylex.props(styles.region)}>
|
|
1550
|
+
{...stylex.props(styles.region, styles.stepsAsideForView)}>
|
|
1486
1551
|
{header}
|
|
1487
1552
|
</div>
|
|
1488
1553
|
)}
|
|
@@ -1491,13 +1556,17 @@ function DropdownMenuPopover({
|
|
|
1491
1556
|
<div
|
|
1492
1557
|
ref={footerRef}
|
|
1493
1558
|
onKeyDown={handleRegionKeyDown}
|
|
1494
|
-
{...stylex.props(styles.region)}>
|
|
1559
|
+
{...stylex.props(styles.region, styles.stepsAsideForView)}>
|
|
1495
1560
|
{footer}
|
|
1496
1561
|
</div>
|
|
1497
1562
|
)}
|
|
1563
|
+
{surfaceHostElement}
|
|
1498
1564
|
</div>
|
|
1499
1565
|
) : (
|
|
1500
|
-
|
|
1566
|
+
<>
|
|
1567
|
+
{menu}
|
|
1568
|
+
{surfaceHostElement}
|
|
1569
|
+
</>
|
|
1501
1570
|
);
|
|
1502
1571
|
|
|
1503
1572
|
const triggerProps: DropdownMenuTriggerProps = {
|
|
@@ -1636,9 +1705,11 @@ function DropdownMenuPopover({
|
|
|
1636
1705
|
)}>
|
|
1637
1706
|
<MenuRegionsContext value={hasRegions}>
|
|
1638
1707
|
<DropdownMenuContext value={contextValue}>
|
|
1639
|
-
<
|
|
1640
|
-
{
|
|
1641
|
-
|
|
1708
|
+
<DropdownMenuSurfaceContext value={surfaceValue}>
|
|
1709
|
+
<DropdownMenuRootOpenContext value={isOpen}>
|
|
1710
|
+
{menuContent}
|
|
1711
|
+
</DropdownMenuRootOpenContext>
|
|
1712
|
+
</DropdownMenuSurfaceContext>
|
|
1642
1713
|
</DropdownMenuContext>
|
|
1643
1714
|
</MenuRegionsContext>
|
|
1644
1715
|
</div>,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenuContext.public.test.ts
|
|
5
|
+
* @input The DropdownMenu barrel and its public `DropdownMenuContextValue`
|
|
6
|
+
* @output Guards that the menu context keeps its members and that the internal
|
|
7
|
+
* surface context a sub-menu with a header or footer reads is not exported
|
|
8
|
+
* @position Public API test guarding @astryxdesign/core/DropdownMenu
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, expect, expectTypeOf, it} from 'vitest';
|
|
12
|
+
import * as dropdownMenu from './index';
|
|
13
|
+
import type {DropdownMenuContextValue} from './index';
|
|
14
|
+
|
|
15
|
+
describe('DropdownMenu public context', () => {
|
|
16
|
+
it('keeps its members and exports no surface context', () => {
|
|
17
|
+
expectTypeOf<keyof DropdownMenuContextValue>().toEqualTypeOf<
|
|
18
|
+
'closeMenu' | 'menuSize' | 'drillIn' | 'menuLabel'
|
|
19
|
+
>();
|
|
20
|
+
expect('DropdownMenuSurfaceContext' in dropdownMenu).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -45,7 +45,10 @@ export interface DropdownMenuDrillIn {
|
|
|
45
45
|
viewStack: ReadonlyArray<string>;
|
|
46
46
|
/** The element a shown view renders its rows into (a portal target). */
|
|
47
47
|
host: HTMLElement | null;
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Show a sub-menu's view in place of the current rows. A view with a header
|
|
50
|
+
* or footer renders into the root's surface host instead of `host`.
|
|
51
|
+
*/
|
|
49
52
|
push: (viewId: string) => void;
|
|
50
53
|
/** Return to the rows the shown view replaced. */
|
|
51
54
|
pop: () => void;
|
|
@@ -65,6 +68,41 @@ export interface DropdownMenuContextValue {
|
|
|
65
68
|
menuLabel?: string;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The root's surface host. The root owns the element through a ref; this
|
|
73
|
+
* object is new whenever a new element attaches, so a sub-menu re-renders
|
|
74
|
+
* into it.
|
|
75
|
+
*/
|
|
76
|
+
export interface DropdownMenuSurfaceHost {
|
|
77
|
+
/** The host element, or `null` before it attaches. */
|
|
78
|
+
getElement: () => HTMLElement | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What a sub-menu with a header or footer needs from the root. Internal. */
|
|
82
|
+
export interface DropdownMenuSurfaceValue {
|
|
83
|
+
/**
|
|
84
|
+
* An element inside the root's popover but outside every `role="menu"`
|
|
85
|
+
* element, where such a sub-menu renders its surface (its flyout layer, or
|
|
86
|
+
* its drilled-in view, while which the root's menu and regions step aside).
|
|
87
|
+
*/
|
|
88
|
+
host: DropdownMenuSurfaceHost;
|
|
89
|
+
/**
|
|
90
|
+
* The root menu's `id`, which a sub-menu's menu in the host names so the
|
|
91
|
+
* press model counts it, and only it, as nested in the root.
|
|
92
|
+
*/
|
|
93
|
+
ownerId: string;
|
|
94
|
+
/** The root's resolved `menuMaxHeight`, which a drilled-in view caps at. */
|
|
95
|
+
menuMaxHeight?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The root's surface; `null` under a root with none (ContextMenu). Internal,
|
|
100
|
+
* off the public menu context, and layer-scoped, so each flyout re-provides it.
|
|
101
|
+
*/
|
|
102
|
+
export const DropdownMenuSurfaceContext =
|
|
103
|
+
createLayerScopedContext<DropdownMenuSurfaceValue | null>(null);
|
|
104
|
+
DropdownMenuSurfaceContext.displayName = 'DropdownMenuSurfaceContext';
|
|
105
|
+
|
|
68
106
|
/**
|
|
69
107
|
* Whether the root menu of this layer tree is open; internal companion to
|
|
70
108
|
* `DropdownMenuContext`, on its own unexported context so the public
|
|
@@ -8,7 +8,50 @@ export const docs = {
|
|
|
8
8
|
displayName: 'Dropdown Menu Item',
|
|
9
9
|
isHiddenFromOverview: true,
|
|
10
10
|
description:
|
|
11
|
-
'
|
|
11
|
+
'One action or destination row in the pointer presentation of DropdownMenu. The parent menu owns keyboard navigation; the row owns activation, disabled state, destination behavior, content layout, and its theme target. Supported host attributes, DOM handlers, className, style, xstyle, and ref reach or compose on the role="menuitem" root.',
|
|
12
|
+
usage: {
|
|
13
|
+
description:
|
|
14
|
+
'Use DropdownMenuItem inside DropdownMenu compound mode for one action or destination. Keep label, description, icon, and endContent non-interactive; controls belong in separate rows or the menu header/footer.',
|
|
15
|
+
bestPractices: [
|
|
16
|
+
{
|
|
17
|
+
guidance: true,
|
|
18
|
+
description:
|
|
19
|
+
'Use href for a destination so LinkProvider, modified clicks, middle clicks, and browser link affordances remain available.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
guidance: true,
|
|
23
|
+
description:
|
|
24
|
+
'Use hasCloseOnSelect=false only when activation reports a result in place on the same row.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
guidance: false,
|
|
28
|
+
description:
|
|
29
|
+
'Put buttons, links, or other focusable controls inside label, description, or endContent. Each menu row is one interactive target.',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
anatomy: [
|
|
33
|
+
{
|
|
34
|
+
name: 'Row',
|
|
35
|
+
required: true,
|
|
36
|
+
description:
|
|
37
|
+
'The role="menuitem" root that owns activation, disabled state, destination behavior, and theme state.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Content',
|
|
41
|
+
required: true,
|
|
42
|
+
description:
|
|
43
|
+
'The non-interactive label plus optional icon, description, and end content.',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
theming: {
|
|
48
|
+
targets: [
|
|
49
|
+
{
|
|
50
|
+
className: 'astryx-dropdown-menu-item',
|
|
51
|
+
visualProps: ['size', 'variant'],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
12
55
|
playground: {
|
|
13
56
|
// Standalone DropdownMenuItem has no required props, so the properties-tab
|
|
14
57
|
// preview renders an empty row without seeded content. Seed a label and
|
|
@@ -26,6 +69,7 @@ export const docs = {
|
|
|
26
69
|
name: 'label',
|
|
27
70
|
type: 'ReactNode',
|
|
28
71
|
description: 'Primary label text.',
|
|
72
|
+
required: true,
|
|
29
73
|
},
|
|
30
74
|
{
|
|
31
75
|
name: 'description',
|
|
@@ -55,11 +99,18 @@ export const docs = {
|
|
|
55
99
|
description:
|
|
56
100
|
'Link relationship. noopener noreferrer is added for target="_blank". Only used with href.',
|
|
57
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: 'isDisabled',
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
description:
|
|
106
|
+
'Whether the row is unavailable. Disabled rows expose aria-disabled, remove link destinations, and suppress activation.',
|
|
107
|
+
default: 'false',
|
|
108
|
+
},
|
|
58
109
|
{
|
|
59
110
|
name: 'endContent',
|
|
60
111
|
type: 'ReactNode',
|
|
61
112
|
description:
|
|
62
|
-
'
|
|
113
|
+
'Non-interactive content rendered after the label and description.',
|
|
63
114
|
},
|
|
64
115
|
{
|
|
65
116
|
name: 'hasCloseOnSelect',
|
|
@@ -95,7 +146,7 @@ export const docsZh = {
|
|
|
95
146
|
isHiddenFromOverview: true,
|
|
96
147
|
displayName: 'Dropdown Menu Item',
|
|
97
148
|
description:
|
|
98
|
-
'
|
|
149
|
+
'DropdownMenu 指针呈现中的一个操作或目标行。父菜单负责键盘导航;该行负责激活、禁用状态、目标行为、内容布局和主题目标。支持的宿主属性、DOM 处理程序、className、style、xstyle 和 ref 会落在或组合到 role="menuitem" 根元素上。',
|
|
99
150
|
props: [
|
|
100
151
|
{
|
|
101
152
|
name: 'icon',
|
|
@@ -106,6 +157,7 @@ export const docsZh = {
|
|
|
106
157
|
name: 'label',
|
|
107
158
|
type: 'ReactNode',
|
|
108
159
|
description: '主标签文本。',
|
|
160
|
+
required: true,
|
|
109
161
|
},
|
|
110
162
|
{
|
|
111
163
|
name: 'description',
|
|
@@ -134,10 +186,16 @@ export const docsZh = {
|
|
|
134
186
|
type: 'string',
|
|
135
187
|
description: '链接关系。target="_blank" 时自动加入 noopener noreferrer。',
|
|
136
188
|
},
|
|
189
|
+
{
|
|
190
|
+
name: 'isDisabled',
|
|
191
|
+
type: 'boolean',
|
|
192
|
+
description: '该行是否不可用;不可用的行不会激活或导航。',
|
|
193
|
+
default: 'false',
|
|
194
|
+
},
|
|
137
195
|
{
|
|
138
196
|
name: 'endContent',
|
|
139
197
|
type: 'ReactNode',
|
|
140
|
-
description: '
|
|
198
|
+
description: '在标签和描述之后渲染的非交互式附加内容。',
|
|
141
199
|
},
|
|
142
200
|
{
|
|
143
201
|
name: 'hasCloseOnSelect',
|
|
@@ -168,9 +226,9 @@ export const docsZh = {
|
|
|
168
226
|
|
|
169
227
|
export const docsDense = {
|
|
170
228
|
name: 'DropdownMenuItem',
|
|
171
|
-
isHiddenFromOverview: true,
|
|
172
229
|
displayName: 'Dropdown Menu Item',
|
|
173
|
-
description:
|
|
230
|
+
description:
|
|
231
|
+
'one pointer-menu action/destination row; parent owns keyboard, row owns activation, state, destination, content, and root inputs',
|
|
174
232
|
propDescriptions: {
|
|
175
233
|
icon: 'icon before label',
|
|
176
234
|
label: 'primary label text',
|
|
@@ -180,7 +238,8 @@ export const docsDense = {
|
|
|
180
238
|
href: 'row is a real anchor w/ role=menuitem; modified/middle click keeps browser meaning',
|
|
181
239
|
target: "link target ('_blank' | '_self'), with href",
|
|
182
240
|
rel: 'link rel; noopener noreferrer added for _blank',
|
|
183
|
-
|
|
241
|
+
isDisabled: 'unavailable; removes destinations and suppresses activation',
|
|
242
|
+
endContent: 'non-interactive content after label+description',
|
|
184
243
|
hasCloseOnSelect:
|
|
185
244
|
'false keeps the menu open on activation (in-place result on the item)',
|
|
186
245
|
variant:
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:DropdownMenuItem
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior, accessibility, theming, testing]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/DropdownMenu/DropdownMenu.test.tsx,
|
|
16
|
+
packages/core/src/DropdownMenu/__tests__/Menu.a11y.test.tsx,
|
|
17
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
18
|
+
apps/storybook/stories/DropdownMenuItem.stories.tsx,
|
|
19
|
+
apps/storybook/rtl-audit/verified-not-applicable.json,
|
|
20
|
+
scripts/check-knowledge.mjs,
|
|
21
|
+
]
|
|
22
|
+
modules: []
|
|
23
|
+
families: [family:navigation-destinations]
|
|
24
|
+
design_specs: []
|
|
25
|
+
architecture:
|
|
26
|
+
[
|
|
27
|
+
architecture:public-component-api,
|
|
28
|
+
architecture:component-theming-surface,
|
|
29
|
+
architecture:component-test-sufficiency,
|
|
30
|
+
architecture:interaction-modality,
|
|
31
|
+
]
|
|
32
|
+
contributing: []
|
|
33
|
+
system_specs: [spec:AST-020, spec:AST-029]
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# DropdownMenuItem component contract
|
|
37
|
+
|
|
38
|
+
## Contract at a glance
|
|
39
|
+
|
|
40
|
+
| Area | Contract |
|
|
41
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| Public contract | One compound pointer-menu row for an action or destination, with label, supporting content, disabled state, destructive paint, optional close suppression, root inputs, and a forwarded ref. |
|
|
43
|
+
| Behavior | The parent menu owns roving focus and keyboard activation; the row owns click order, disabled suppression, close-on-select, destination handoff, hover focus, and its visible content composition. |
|
|
44
|
+
| End-user impact | Actions and destinations preserve keyboard, pointer, and browser link behavior while unavailable rows remain inert and destructive actions remain identifiable. |
|
|
45
|
+
| Builder impact | Destinations use `href`; supporting content stays non-interactive; `hasCloseOnSelect=false` is reserved for in-place result feedback. |
|
|
46
|
+
| Compatibility/readiness | Patch-compatible observational contract. Runtime, exports, defaults, and theme targets remain unchanged; authority stays `draft` until exact-head owner approval. |
|
|
47
|
+
| Review checks | Reject nested controls, disabled activation, lost destination safety, broken modified-click behavior, dropped root inputs, a second keyboard owner, or state/target reflection moved off the row. |
|
|
48
|
+
| Governing rules | `family:navigation-destinations/FR1–FR5`; `architecture:public-component-api/INV5–INV9`; `architecture:component-theming-surface/INV3–INV6`; `spec:AST-020/FR1,FR2,FR6,FR15`. |
|
|
49
|
+
|
|
50
|
+
This table is a review projection; the body below is authoritative.
|
|
51
|
+
|
|
52
|
+
## Intent
|
|
53
|
+
|
|
54
|
+
DropdownMenuItem renders one compound-mode row in the pointer presentation of
|
|
55
|
+
DropdownMenu. It owns the row element, content slots, action-versus-destination
|
|
56
|
+
branch, activation order, disabled and destructive state, root composition, and
|
|
57
|
+
`dropdown-menu-item` theme target. DropdownMenu owns the surrounding menu,
|
|
58
|
+
keyboard collection, focus movement, typeahead, presentation choice, and close
|
|
59
|
+
lifecycle.
|
|
60
|
+
|
|
61
|
+
## Compatibility and migration
|
|
62
|
+
|
|
63
|
+
- Released default preserved: yes.
|
|
64
|
+
- Compatibility class: observational patch; no behavior or public surface changes.
|
|
65
|
+
- Controlled/uncontrolled behavior: not applicable.
|
|
66
|
+
- Migration decision: none.
|
|
67
|
+
|
|
68
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
69
|
+
|
|
70
|
+
## Ownership boundary
|
|
71
|
+
|
|
72
|
+
**Owns**
|
|
73
|
+
|
|
74
|
+
- One `role="menuitem"` root and its label, description, icon, end content,
|
|
75
|
+
disabled state, destructive variant, ref, supported host inputs, and styling inputs.
|
|
76
|
+
- Action callback order and `hasCloseOnSelect` behavior.
|
|
77
|
+
- Link-row rendering, destination handoff, modified/middle-click preservation,
|
|
78
|
+
target and relationship handling, and disabled-link inertness.
|
|
79
|
+
- Pointer hover focus and the row's press/hover/focus paint.
|
|
80
|
+
- The `dropdown-menu-item` target and its size/variant state reflection.
|
|
81
|
+
|
|
82
|
+
**Does not own / non-goals**
|
|
83
|
+
|
|
84
|
+
- Menu naming, open/close state, focus entry, roving focus, typeahead, and keyboard
|
|
85
|
+
click synthesis — owned by DropdownMenu and its shared hooks.
|
|
86
|
+
- Touch and bottom-sheet row rendering — the parent presentation delegates those
|
|
87
|
+
rows to List.
|
|
88
|
+
- Destination acceptance and blocking — owned by `family:navigation-destinations`.
|
|
89
|
+
- Interactive controls inside label, description, or end content. Controls belong
|
|
90
|
+
beside the menu or as separate rows.
|
|
91
|
+
|
|
92
|
+
## Public concepts
|
|
93
|
+
|
|
94
|
+
| Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
|
|
95
|
+
| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------- | ------- | ------------------------------- | --------- | ------------------------------------------------------------ |
|
|
96
|
+
| Row act | action; destination | Runs a callback or exposes a browser/router destination from the row itself. | Every enabled row | action | component and navigation family | stable | A blocked destination is inert; a disabled link has no href. |
|
|
97
|
+
| Availability | enabled; disabled | Controls whether the row can receive focus or activate. | Every row | enabled | component and parent menu | stable | Disabled suppresses activation and destination. |
|
|
98
|
+
| Close policy | close; remain open | Whether successful activation closes the parent menu. | Action and destination rows | close | component | stable | Omission closes. |
|
|
99
|
+
| Visual state | default; destructive; hover; focus; press; disabled | Paints row meaning and interaction state without changing menuitem semantics. | Pointer row | default | component | stable | Unknown variants are rejected by the public type. |
|
|
100
|
+
| Root surface | ref; DOM/data/ARIA; class/style/xstyle/events | Extends the row root while preserving component-owned role and keyboard state. | Every pointer row | omitted | public component API | stable | Accepted inputs compose; owned role/tab stop remain fixed. |
|
|
101
|
+
|
|
102
|
+
## Behavioral and layout contract
|
|
103
|
+
|
|
104
|
+
Draft requirements identify their basis so observed code is not mistaken for an
|
|
105
|
+
intentional decision.
|
|
106
|
+
|
|
107
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
108
|
+
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
109
|
+
| FR1 | The pointer row MUST expose one `role="menuitem"` root. Label, description, icon, and end content remain presentational content of that row rather than nested controls. | WAI-ARIA menu pattern; released source/docs | verify |
|
|
110
|
+
| FR2 | Enabled activation MUST run the row callback before optional close. Disabled rows MUST expose `aria-disabled=true`, remove link destinations, and run neither callback nor close. | Released behavior; focused tests | verify |
|
|
111
|
+
| FR3 | A destination row MUST render as the link root through LinkProvider. Plain activation runs the callback then closes and navigates; modified and middle clicks preserve browser behavior without running the callback; blocked schemes are inert. | `family:navigation-destinations/FR1–FR5`; released tests | verify |
|
|
112
|
+
| FR4 | The parent menu MUST remain the only keyboard owner. It synthesizes click activation with modifiers, moves focus, and runs typeahead; the row MAY focus itself on pointer hover but MUST NOT create a second roving-focus model. | `architecture:interaction-modality`; current parent tests | verify |
|
|
113
|
+
| FR5 | The visible root MUST carry `dropdown-menu-item`; size and destructive state MUST reflect through `themeProps`; semantic tokens own layout and paint before caller styling inputs compose. | `architecture:component-theming-surface/INV3–INV6`; source/tests | verify |
|
|
114
|
+
| FR6 | Ref, supported host attributes and handlers, `className`, `style`, and `xstyle` MUST reach or compose on the row root without replacing owned role, tab stop, disabled behavior, destination handling, or activation order. | `architecture:public-component-api/INV5–INV9`; focused tests | verify |
|
|
115
|
+
| FR7 | Disabled menu rows remain inert. Whether the menu keyboard model should visit them is unresolved at the shared owner and MUST NOT be changed by a component-local audit. | WAI-ARIA menu pattern; parent `component:DropdownMenu/OQ1` | human API decision |
|
|
116
|
+
| FR8 | Keyboard focus MUST remain perceivable, including in forced-colors mode. The current background-only treatment is an objective gap; a remediation must preserve modality and row-content visibility without inventing new ordinary-theme appearance. | WCAG 2.2 SC 1.4.11 and 2.4.7; current rendered behavior | objective gap; owner must confirm the shared visual boundary |
|
|
117
|
+
|
|
118
|
+
### Allowed variation
|
|
119
|
+
|
|
120
|
+
- **AV1 — Content.** Non-interactive icons, text, descriptions, shortcuts, badges,
|
|
121
|
+
and status text may vary without changing the row's menuitem semantics.
|
|
122
|
+
- **AV2 — Theme paint.** A theme may restyle the target's admitted properties while
|
|
123
|
+
preserving visible state distinctions and forced-colors focus.
|
|
124
|
+
- **AV3 — Parent size.** The parent menu supplies `sm`, `md`, or `lg`; the row reflects
|
|
125
|
+
that state and may vary its tokenized insets.
|
|
126
|
+
|
|
127
|
+
### Representative states
|
|
128
|
+
|
|
129
|
+
| State | Required invariant | Allowed variation |
|
|
130
|
+
| ---------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
131
|
+
| Enabled action | Callback runs, then close follows the close policy. | Content, size, and visual variant. |
|
|
132
|
+
| In-place result action | Callback runs and the menu remains open. | Label/end content may update. |
|
|
133
|
+
| Disabled action | `aria-disabled=true`; callback, hover focus, and close are suppressed. | Content remains visible. |
|
|
134
|
+
| Destination | One anchor root preserves safe destination and browser activation behavior. | Target, rel, router provider, and content. |
|
|
135
|
+
| Destructive | Error-role paint applies to label, icon, and description without changing behavior. | Other row content remains consumer-authored. |
|
|
136
|
+
| Keyboard focus | Parent focus lands on the row and remains visible. | Exact theme-owned paint may vary. |
|
|
137
|
+
| RTL / narrow viewport | Logical insets and start alignment follow direction without horizontal overflow. | Menu placement remains parent-owned. |
|
|
138
|
+
|
|
139
|
+
### Transformation and precedence order
|
|
140
|
+
|
|
141
|
+
- **ORD1 — Activation.** Disabled guard → browser-navigation classification → row
|
|
142
|
+
callback where applicable → close policy → browser/router navigation.
|
|
143
|
+
- **ORD2 — Root composition.** Parent size → base row styles → variant/disabled state
|
|
144
|
+
→ caller `xstyle`; target metadata and caller class/style compose on the same root.
|
|
145
|
+
|
|
146
|
+
### Performance and resources
|
|
147
|
+
|
|
148
|
+
- **PR1 — Shared press lifecycle.** DropdownMenuItem uses the shared counted press
|
|
149
|
+
controller through `usePressFeedback`; the first mounted pressable installs the
|
|
150
|
+
window listeners and the last removes them. The item adds no per-row listener,
|
|
151
|
+
observer, timer, request, or local resource owner.
|
|
152
|
+
|
|
153
|
+
## Accessibility contract
|
|
154
|
+
|
|
155
|
+
- **AR1 — Menu item semantics.** The root is one named `menuitem`; supporting content
|
|
156
|
+
does not introduce a nested interactive descendant.
|
|
157
|
+
- **AR2 — Disabled state.** Disabled rows expose state and cannot activate or navigate.
|
|
158
|
+
- **AR3 — Keyboard ownership.** DropdownMenu owns focus collection and activation;
|
|
159
|
+
the item remains a single row in that model.
|
|
160
|
+
- **AR4 — Focus perception.** Focus remains visually perceivable in supported themes
|
|
161
|
+
and platform accessibility modes; the open forced-colors gap remains explicit.
|
|
162
|
+
|
|
163
|
+
## Design relationships
|
|
164
|
+
|
|
165
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
166
|
+
| ---------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------- | ------------------ |
|
|
167
|
+
| Row | One full-width action/destination target with tokenized insets and start alignment. | Current source and parent menu composition | prominent | FR1–FR6 |
|
|
168
|
+
| Supporting text | Description and end content remain subordinate to the row label. | Delegated Item layout and current consumer docs | supporting | FR1 |
|
|
169
|
+
| Interaction | Hover, focus, press, disabled, and destructive states remain distinguishable. | Current source; forced-colors focus is an unresolved objective gap | prominent | FR5, FR8, AR4 |
|
|
170
|
+
|
|
171
|
+
No new component-local visual decision is settled by this draft.
|
|
172
|
+
|
|
173
|
+
## Family and system relationships
|
|
174
|
+
|
|
175
|
+
- `family:navigation-destinations` owns destination acceptance and blocked-scheme
|
|
176
|
+
behavior; this component supplies the destination-bearing row.
|
|
177
|
+
- `architecture:public-component-api` owns root input composition, refs, and released
|
|
178
|
+
compatibility.
|
|
179
|
+
- `architecture:component-theming-surface` owns target placement and state reflection.
|
|
180
|
+
- `architecture:component-test-sufficiency` owns proportionate observable evidence.
|
|
181
|
+
- `architecture:interaction-modality` and DropdownMenu own parent keyboard behavior.
|
|
182
|
+
- `spec:AST-020` supplies WCAG/APG adoption and evidence-layer boundaries.
|
|
183
|
+
- `spec:AST-029` keeps this observational draft separate from the audit receipt.
|
|
184
|
+
|
|
185
|
+
## Verification map
|
|
186
|
+
|
|
187
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
188
|
+
| ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
189
|
+
| FR1–FR4, AR1–AR3 | `DropdownMenu.test.tsx`; reusable Menu accessibility tests; scoped axe | action, destination, disabled, keyboard, modified clicks | A nested control, activation while disabled, lost browser affordance, or second keyboard owner fails observable tests | `audit:DropdownMenuItem/§1,§4,§6` |
|
|
190
|
+
| FR5, FR8, AR4 | theming-target tests; exact-head Neutral/Butter/RTL/narrow/focus/hover frames | default, destructive, disabled, focus, hover | Target/state reflection disappears, paint is absent, or focus becomes unobservable | `audit:DropdownMenuItem/§2,§5` |
|
|
191
|
+
| FR6 | root/ref/handler tests; lint and typecheck | ref, host attributes, class/style/xstyle, pointer handlers | An accepted input is dropped or overrides component-owned semantics | `audit:DropdownMenuItem/§3` |
|
|
192
|
+
| RTL / responsive | source-hashed verified-N/A receipt plus exact-head browser frames | LTR, RTL, 320px | Physical direction leaks into the row or narrow layout overflows | `audit:DropdownMenuItem/§9,§10` |
|
|
193
|
+
|
|
194
|
+
## Decision log
|
|
195
|
+
|
|
196
|
+
None. This draft records current facts and introduces no product decision.
|
|
197
|
+
|
|
198
|
+
## Open questions
|
|
199
|
+
|
|
200
|
+
- **OQ1 — Which shared keyboard owner should keep disabled menu items discoverable
|
|
201
|
+
while preserving their inert behavior?** (`human-api`)
|
|
202
|
+
- **OQ2 — Should forced-colors focus use a component-specific system outline, or a
|
|
203
|
+
shared row-focus treatment that also governs ordinary themes?** (`human-design`)
|
|
204
|
+
|
|
205
|
+
## Content boundary
|
|
206
|
+
|
|
207
|
+
This file does not duplicate consumer prop tables/examples, current audit scores,
|
|
208
|
+
screenshot matrices, implementation steps, or family/system rules. The audit pull
|
|
209
|
+
request owns the closed run evidence required by `spec:AST-029/FR5`.
|
|
@@ -67,6 +67,18 @@ export const docs = {
|
|
|
67
67
|
"How the sub-menu shows its rows. 'flyout' opens them beside the row; 'drill-in' replaces the menu's rows with them and a Back row, in the same box; 'adaptive' drills in when a finger opened the menu (a coarse pointer, decided when the menu opened) and flies out otherwise.",
|
|
68
68
|
default: "'adaptive'",
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'header',
|
|
72
|
+
type: 'ReactNode',
|
|
73
|
+
description:
|
|
74
|
+
'Content above the sub-menu\'s rows and outside its menu, a field that filters them, say. It keeps its own keys and presses. With a header or footer the sub-menu\'s surface (the flyout, or the drilled-in view on a phone) is a modal dialog named by the row, beside the menu rather than inside it: the row says aria-haspopup="dialog", Tab and Shift+Tab move through the header, the rows and the footer and wrap, Escape returns to the row, and a pointer leaving the flyout closes it only once focus leaves too. A ContextMenu does not show it.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'footer',
|
|
78
|
+
type: 'ReactNode',
|
|
79
|
+
description:
|
|
80
|
+
"Content below the sub-menu's rows and outside its menu, a door to where the options are managed, say. Same rules as header.",
|
|
81
|
+
},
|
|
70
82
|
{
|
|
71
83
|
name: 'xstyle',
|
|
72
84
|
type: 'StyleXStyles',
|
|
@@ -77,7 +89,7 @@ export const docs = {
|
|
|
77
89
|
name: 'ref',
|
|
78
90
|
type: 'React.Ref<HTMLElement>',
|
|
79
91
|
description:
|
|
80
|
-
'Ref forwarded to the trigger row, the element carrying role="menuitem" and aria-haspopup="menu".',
|
|
92
|
+
'Ref forwarded to the trigger row, the element carrying role="menuitem" and aria-haspopup="menu" ("dialog" with a header or footer).',
|
|
81
93
|
},
|
|
82
94
|
],
|
|
83
95
|
};
|
|
@@ -100,6 +112,9 @@ export const docsDense = {
|
|
|
100
112
|
onOpenChange: 'called when flyout opens/closes',
|
|
101
113
|
presentation:
|
|
102
114
|
"'flyout' beside the row | 'drill-in' replaces the rows + Back row | 'adaptive' (default) drills in on a coarse pointer",
|
|
115
|
+
header:
|
|
116
|
+
'content above the rows, outside the menu; surface becomes a modal dialog named by the row (Tab wraps, Escape returns to the row)',
|
|
117
|
+
footer: 'content below the rows, outside the menu; same rules as header',
|
|
103
118
|
xstyle: 'StyleX styles for the trigger row',
|
|
104
119
|
ref: 'ref to the trigger row (role="menuitem")',
|
|
105
120
|
},
|