@astryxdesign/core 0.7.1 → 0.7.2-canary.01eab6a
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/AspectRatio/AspectRatio.d.ts +6 -8
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +44 -3
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +38 -14
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -2
- package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +20 -8
- package/dist/Drawer/Drawer.d.ts +191 -0
- package/dist/Drawer/Drawer.d.ts.map +1 -0
- package/dist/Drawer/Drawer.js +382 -0
- package/dist/Drawer/DrawerHeader.d.ts +85 -0
- package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
- package/dist/Drawer/DrawerHeader.js +134 -0
- package/dist/Drawer/index.d.ts +11 -0
- package/dist/Drawer/index.d.ts.map +1 -0
- package/dist/Drawer/index.js +11 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
- package/dist/Drawer/useDrawerDialogPresence.js +236 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +47 -4
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +4 -10
- package/dist/Field/Field.d.ts +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +8 -4
- package/dist/Grid/GridSpan.d.ts.map +1 -1
- package/dist/Grid/GridSpan.js +1 -0
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -2
- package/dist/Item/Item.d.ts +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -4
- package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
- package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
- package/dist/Layer/layerViewportInset.stylex.js +6 -2
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +13 -7
- package/dist/Layer/useLayerDismissal.d.ts +1 -1
- package/dist/Layer/useLayerDismissal.js +1 -1
- package/dist/Layout/container.stylex.d.ts +5 -5
- package/dist/Layout/container.stylex.js +4 -4
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +13 -5
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +21 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
- package/dist/Markdown/plugins/frontmatter.js +7 -3
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +5 -1
- package/dist/MetadataList/MetadataListContext.d.ts +1 -1
- package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +22 -8
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +3 -1
- package/dist/Pagination/Pagination.d.ts +8 -5
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +21 -8
- package/dist/Popover/Popover.js +2 -2
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +6 -2
- package/dist/RadioList/RadioListItem.d.ts +9 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +2 -0
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +6 -2
- package/dist/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +71 -9
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -1
- package/dist/TabList/TabMenu.d.ts +2 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +6 -3
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- package/dist/Text/useTruncation.d.ts +7 -5
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +16 -5
- package/dist/ToggleButton/ToggleButton.d.ts +3 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +12 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -0
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +3 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +26 -0
- package/dist/astryx.css +47 -13
- package/dist/hooks/useEntryAnimation.d.ts +2 -1
- package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
- package/dist/hooks/useEntryAnimation.js +14 -12
- package/dist/hooks/useIsHydrating.d.ts +6 -0
- package/dist/hooks/useIsHydrating.d.ts.map +1 -0
- package/dist/hooks/useIsHydrating.js +30 -0
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +112 -1
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +0 -3
- package/dist/utils/focusOutline.stylex.d.ts +4 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +4 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/dist/utils/sharedMutationObserver.d.ts +21 -0
- package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
- package/dist/utils/sharedMutationObserver.js +69 -0
- package/locales/en.json +444 -0
- package/locales/pseudo.json +333 -0
- package/package.json +9 -4
- package/src/AspectRatio/AspectRatio.test.tsx +44 -43
- package/src/AspectRatio/AspectRatio.tsx +38 -18
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
- package/src/Avatar/Avatar.doc.mjs +5 -3
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.test.tsx +155 -9
- package/src/Button/Button.tsx +39 -16
- package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
- package/src/Button/__tests__/Button.a11y.states.ts +0 -10
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +3 -1
- package/src/CheckboxList/CheckboxList.test.tsx +22 -0
- package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
- package/src/CheckboxList/CheckboxListItem.tsx +10 -0
- package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
- package/src/ContextMenu/ContextMenu.tsx +4 -1
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
- package/src/Dialog/Dialog.test.tsx +46 -0
- package/src/Dialog/Dialog.tsx +20 -7
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
- package/src/Drawer/Drawer.doc.mjs +472 -0
- package/src/Drawer/Drawer.spec.md +357 -0
- package/src/Drawer/Drawer.test.tsx +1300 -0
- package/src/Drawer/Drawer.tsx +613 -0
- package/src/Drawer/DrawerDirection.test.tsx +127 -0
- package/src/Drawer/DrawerHeader.doc.mjs +293 -0
- package/src/Drawer/DrawerHeader.test.tsx +102 -0
- package/src/Drawer/DrawerHeader.tsx +235 -0
- package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
- package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
- package/src/Drawer/index.ts +13 -0
- package/src/Drawer/useDrawerDialogPresence.ts +284 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +62 -13
- package/src/DropdownMenu/DropdownMenu.tsx +47 -4
- package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
- package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
- package/src/EmptyState/EmptyState.doc.mjs +3 -2
- package/src/EmptyState/EmptyState.spec.md +189 -0
- package/src/EmptyState/EmptyState.test.tsx +74 -3
- package/src/EmptyState/EmptyState.tsx +4 -8
- package/src/Field/Field.doc.mjs +14 -8
- package/src/Field/Field.spec.md +217 -0
- package/src/Field/Field.test.tsx +60 -2
- package/src/Field/Field.tsx +17 -8
- package/src/Grid/GridSpan.tsx +3 -0
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- package/src/HoverCard/HoverCard.test.tsx +58 -0
- package/src/HoverCard/useHoverCard.tsx +10 -2
- package/src/IconButton/IconButton.doc.mjs +2 -2
- package/src/IconButton/IconButton.test.tsx +3 -1
- package/src/Item/Item.doc.mjs +12 -8
- package/src/Item/Item.spec.md +226 -0
- package/src/Item/Item.test.tsx +61 -0
- package/src/Item/Item.tsx +40 -4
- package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
- package/src/Layer/layerViewportInset.stylex.ts +6 -2
- package/src/Layer/layerViewportInset.test.ts +1 -0
- package/src/Layer/useLayer.tsx +24 -13
- package/src/Layer/useLayerDismissal.ts +1 -1
- package/src/Layout/container.stylex.ts +6 -6
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +163 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.doc.mjs +24 -5
- package/src/List/ListItem.tsx +56 -27
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
- package/src/Markdown/plugins/frontmatter.ts +7 -3
- package/src/MetadataList/MetadataList.doc.mjs +2 -2
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MultiSelector/MultiSelector.test.tsx +159 -0
- package/src/MultiSelector/MultiSelector.tsx +40 -7
- package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
- package/src/MultiSelector/hooks.ts +3 -1
- package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
- package/src/Pagination/Pagination.doc.mjs +7 -7
- package/src/Pagination/Pagination.spec.md +9 -9
- package/src/Pagination/Pagination.test.tsx +242 -0
- package/src/Pagination/Pagination.tsx +27 -11
- package/src/Popover/Popover.test.tsx +2 -2
- package/src/Popover/Popover.tsx +4 -4
- package/src/PowerSearch/PowerSearch.test.tsx +85 -0
- package/src/PowerSearch/PowerSearch.tsx +2 -2
- package/src/RadioList/RadioList.fr10.test.tsx +45 -2
- package/src/RadioList/RadioList.test.tsx +22 -0
- package/src/RadioList/RadioList.tsx +4 -1
- package/src/RadioList/RadioListItem.doc.mjs +16 -0
- package/src/RadioList/RadioListItem.tsx +10 -1
- package/src/Selector/hooks.ts +6 -2
- package/src/Selector/useCombobox.doc.mjs +237 -0
- package/src/SideNav/SideNav.test.tsx +179 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +96 -17
- package/src/Slider/Slider.spec.md +39 -18
- package/src/Switch/Switch.test.tsx +49 -0
- package/src/Switch/Switch.tsx +11 -0
- package/src/TabList/TabList.spec.md +57 -44
- package/src/TabList/TabList.test.tsx +67 -0
- package/src/TabList/TabMenu.doc.mjs +4 -3
- package/src/TabList/TabMenu.tsx +7 -4
- package/src/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/Text/useTruncation.test.tsx +69 -0
- package/src/Text/useTruncation.ts +16 -5
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/Tokenizer/Tokenizer.test.tsx +54 -1
- package/src/Tooltip/Tooltip.test.tsx +31 -0
- package/src/Tooltip/useTooltip.test.tsx +13 -0
- package/src/Tooltip/useTooltip.tsx +11 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
- package/src/TopNav/TopNavMegaMenu.tsx +7 -3
- package/src/TopNav/TopNavMenu.test.tsx +19 -0
- package/src/TopNav/TopNavMenu.tsx +11 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.spec.md +29 -17
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/Typeahead/BaseTypeahead.tsx +26 -0
- package/src/Typeahead/Typeahead.test.tsx +260 -2
- package/src/docPropLiterals.test.ts +36 -10
- package/src/hooks/useEntryAnimation.doc.mjs +4 -4
- package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
- package/src/hooks/useEntryAnimation.ts +15 -16
- package/src/hooks/useIsHydrating.test.tsx +70 -0
- package/src/hooks/useIsHydrating.ts +31 -0
- package/src/hooks/useMenuHover.ts +18 -13
- package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
- package/src/i18n/InternationalizationProvider.spec.md +252 -0
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
- package/src/i18n/generated-locales/en.generated.ts +121 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
- package/src/i18n/useTranslator.test.tsx +37 -0
- package/src/index.ts +1 -0
- package/src/reset.css +0 -29
- package/src/theme/derivedVarRegistry.test.ts +7 -3
- package/src/theme/derivedVarRegistry.ts +0 -3
- package/src/utils/focusOutline.stylex.ts +4 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
- package/src/utils/sharedMutationObserver.test.ts +53 -0
- package/src/utils/sharedMutationObserver.ts +78 -0
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
} from './menuItemRoles';
|
|
61
61
|
import {
|
|
62
62
|
DropdownMenuContext,
|
|
63
|
+
DropdownMenuRootOpenContext,
|
|
63
64
|
type DropdownMenuContextValue,
|
|
64
65
|
} from './DropdownMenuContext';
|
|
65
66
|
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
@@ -229,8 +230,8 @@ const styles = stylex.create({
|
|
|
229
230
|
WebkitTouchCallout: 'none',
|
|
230
231
|
userSelect: 'none',
|
|
231
232
|
maxInlineSize: stylex.firstThatWorks(
|
|
232
|
-
layerViewportInset.maxInlineSize
|
|
233
|
-
layerViewportInset.maxInlineSizeFallback
|
|
233
|
+
`calc(${layerViewportInset.maxInlineSize})`,
|
|
234
|
+
`calc(${layerViewportInset.maxInlineSizeFallback})`,
|
|
234
235
|
),
|
|
235
236
|
maxHeight: stylex.firstThatWorks(
|
|
236
237
|
MENU_MAX_BLOCK_SIZE,
|
|
@@ -987,6 +988,43 @@ function DropdownMenuPopover({
|
|
|
987
988
|
// above it ends the gesture in flight through this ref.
|
|
988
989
|
const cancelMenuPressRef = useRef<() => void>(() => {});
|
|
989
990
|
|
|
991
|
+
// Submenu flyouts are `popover="manual"` elements nested inside the menu's
|
|
992
|
+
// `popover="auto"` element, so the browser's light dismiss of the menu never
|
|
993
|
+
// reaches them: without a sweep they keep their open state and their
|
|
994
|
+
// top-layer entry while the menu is hidden, and re-anchor to the viewport
|
|
995
|
+
// corner when the menu reopens (#6893). Closing them through the popover
|
|
996
|
+
// API lets useLayer's toggle handling reset their state. Only flyouts that
|
|
997
|
+
// are actually open are touched: `:popover-open` ships wherever
|
|
998
|
+
// `hidePopover` does (they are one API), and the display fallback keeps its
|
|
999
|
+
// own open signal (`display: block`) so it is guarded the same way.
|
|
1000
|
+
const hideOpenFlyouts = useCallback(() => {
|
|
1001
|
+
const menuPopover = contentRef.current;
|
|
1002
|
+
if (!menuPopover) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
// Innermost first: hiding a flyout may unmount the flyouts nested inside
|
|
1006
|
+
// it, and a detached element no longer has the toggle listener that
|
|
1007
|
+
// resets its open state.
|
|
1008
|
+
const flyouts = [
|
|
1009
|
+
...menuPopover.querySelectorAll<HTMLElement>('[popover]'),
|
|
1010
|
+
].reverse();
|
|
1011
|
+
for (const flyout of flyouts) {
|
|
1012
|
+
if (typeof flyout.hidePopover === 'function') {
|
|
1013
|
+
if (flyout.matches(':popover-open')) {
|
|
1014
|
+
flyout.hidePopover();
|
|
1015
|
+
}
|
|
1016
|
+
} else if (flyout.style.display === 'block') {
|
|
1017
|
+
flyout.style.display = 'none';
|
|
1018
|
+
const toggle = new Event('toggle', {bubbles: false});
|
|
1019
|
+
Object.defineProperty(toggle, 'newState', {
|
|
1020
|
+
value: 'closed',
|
|
1021
|
+
configurable: true,
|
|
1022
|
+
});
|
|
1023
|
+
flyout.dispatchEvent(toggle);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}, []);
|
|
1027
|
+
|
|
990
1028
|
// Focus lands somewhere stated when the menu closes. A press outside that
|
|
991
1029
|
// landed on a focusable control has already moved focus there; leave it.
|
|
992
1030
|
// Otherwise — a row acted, Escape, a press on nothing — focus returns to
|
|
@@ -994,6 +1032,9 @@ function DropdownMenuPopover({
|
|
|
994
1032
|
// keyboard (the modality tracker decides; the suppressed style hides the
|
|
995
1033
|
// pointer case, which Safari would otherwise paint after a touch pick).
|
|
996
1034
|
const handleLayerHide = useCallback(() => {
|
|
1035
|
+
// Whatever closed the menu — light dismiss, the dismissal stack, Escape
|
|
1036
|
+
// or a controlled close — its flyouts close with it.
|
|
1037
|
+
hideOpenFlyouts();
|
|
997
1038
|
if (suppressControlledRollbackHideRef.current) {
|
|
998
1039
|
suppressControlledRollbackHideRef.current = false;
|
|
999
1040
|
return;
|
|
@@ -1024,7 +1065,7 @@ function DropdownMenuPopover({
|
|
|
1024
1065
|
// also reaches a control the caller renders, which the browser would
|
|
1025
1066
|
// otherwise ring (Chromium after a mouse pick, WebKit after a touch).
|
|
1026
1067
|
focusForLastInput(trigger, {preventScroll: true});
|
|
1027
|
-
}, [isControlled, onOpenChange, prepareFocusReturn]);
|
|
1068
|
+
}, [hideOpenFlyouts, isControlled, onOpenChange, prepareFocusReturn]);
|
|
1028
1069
|
|
|
1029
1070
|
// Defer item focus until the layer has committed open, so focus restore
|
|
1030
1071
|
// captures the trigger instead of the first menu item.
|
|
@@ -1694,7 +1735,9 @@ function DropdownMenuPopover({
|
|
|
1694
1735
|
)}>
|
|
1695
1736
|
<MenuRegionsContext value={hasRegions}>
|
|
1696
1737
|
<DropdownMenuContext value={contextValue}>
|
|
1697
|
-
{
|
|
1738
|
+
<DropdownMenuRootOpenContext value={isOpen}>
|
|
1739
|
+
{menuContent}
|
|
1740
|
+
</DropdownMenuRootOpenContext>
|
|
1698
1741
|
</DropdownMenuContext>
|
|
1699
1742
|
</MenuRegionsContext>
|
|
1700
1743
|
</div>,
|
|
@@ -65,6 +65,30 @@ export interface DropdownMenuContextValue {
|
|
|
65
65
|
menuLabel?: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether the root menu of this layer tree is open; internal companion to
|
|
70
|
+
* `DropdownMenuContext`, on its own unexported context so the public
|
|
71
|
+
* `DropdownMenuContextValue` surface stays unchanged. A sub-menu's hover
|
|
72
|
+
* intent must die with the menu: a hover-open scheduled on its trigger
|
|
73
|
+
* before the menu closed from elsewhere would otherwise fire into the
|
|
74
|
+
* hidden menu and reopen the flyout behind it (#6893). `null` means no
|
|
75
|
+
* signal, treated as open, matching providers that have no state to give.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export const DropdownMenuRootOpenContext = createLayerScopedContext<
|
|
79
|
+
boolean | null
|
|
80
|
+
>(null);
|
|
81
|
+
DropdownMenuRootOpenContext.displayName = 'DropdownMenuRootOpenContext';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether the root menu backing these compound items is open. True when no
|
|
85
|
+
* provider gave a signal.
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
export function useDropdownMenuRootOpen(): boolean {
|
|
89
|
+
return use(DropdownMenuRootOpenContext) !== false;
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
export const DropdownMenuContext =
|
|
69
93
|
createLayerScopedContext<DropdownMenuContextValue | null>(null);
|
|
70
94
|
DropdownMenuContext.displayName = 'DropdownMenuContext';
|
|
@@ -8,7 +8,7 @@ export const docs = {
|
|
|
8
8
|
displayName: 'Dropdown Menu Divider',
|
|
9
9
|
isHiddenFromOverview: true,
|
|
10
10
|
description:
|
|
11
|
-
'A horizontal rule separating groups of rows in a compound menu. Renders role="separator", so it is never a stop in the arrow-key order. The data-driven equivalent is the `{type: "divider"}` entry in `items`; both modes render this component, so they look and theme identically.',
|
|
11
|
+
'A horizontal rule separating groups of rows in a compound menu. Renders role="separator", so it is never a stop in the arrow-key order. The data-driven equivalent is the `{type: "divider"}` entry in `items`; both modes render this component, so they look and theme identically. Host attributes and DOM handlers (id, data-*, aria-*) reach the separator; its role and orientation stay component-owned.',
|
|
12
12
|
props: [
|
|
13
13
|
{
|
|
14
14
|
name: 'xstyle',
|
|
@@ -27,7 +27,7 @@ export const docsDense = {
|
|
|
27
27
|
isHiddenFromOverview: true,
|
|
28
28
|
displayName: 'Dropdown Menu Divider',
|
|
29
29
|
description:
|
|
30
|
-
'separator row for compound menus; compound peer of the data API\'s {type: "divider"}',
|
|
30
|
+
'separator row for compound menus; compound peer of the data API\'s {type: "divider"}; host attributes reach the separator, role/orientation stay owned',
|
|
31
31
|
propDescriptions: {
|
|
32
32
|
xstyle: 'StyleX styles applied after the menu spacing',
|
|
33
33
|
},
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:DropdownMenuDivider
|
|
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, theming, accessibility, testing]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/DropdownMenu/DropdownMenu.test.tsx,
|
|
16
|
+
packages/core/src/ContextMenu/ContextMenu.test.tsx,
|
|
17
|
+
packages/core/src/Breadcrumbs/Breadcrumbs.test.tsx,
|
|
18
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
19
|
+
apps/storybook/stories/DropdownMenuDivider.stories.tsx,
|
|
20
|
+
scripts/check-knowledge.mjs,
|
|
21
|
+
]
|
|
22
|
+
modules: []
|
|
23
|
+
families: []
|
|
24
|
+
design_specs: []
|
|
25
|
+
architecture:
|
|
26
|
+
[
|
|
27
|
+
architecture:public-component-api,
|
|
28
|
+
architecture:component-theming-surface,
|
|
29
|
+
architecture:component-test-sufficiency,
|
|
30
|
+
]
|
|
31
|
+
contributing: []
|
|
32
|
+
system_specs: [spec:AST-020, spec:AST-029]
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# DropdownMenuDivider component contract
|
|
36
|
+
|
|
37
|
+
## Contract at a glance
|
|
38
|
+
|
|
39
|
+
| Area | Contract |
|
|
40
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| Public contract | `DropdownMenuDivider` / `DropdownMenuDividerProps`, plus the `ContextMenuDivider` and `BreadcrumbMenuDivider` aliases and their props; `BaseProps<HTMLDivElement>` minus caller-owned `role` and `aria-orientation`; a root `ref`. |
|
|
42
|
+
| Behavior | One unlabelled horizontal separator divides pointer-menu rows and stays out of arrow-key order; data and compound pointer modes render the same component. |
|
|
43
|
+
| End-user impact | Pointer-menu sections remain visually separated without adding a focus stop or changing row operation. |
|
|
44
|
+
| Builder impact | No new caller choice. Host attributes, handlers, and styling inputs continue to compose on the separator. |
|
|
45
|
+
| Compatibility/readiness | Patch-compatible observational contract. Runtime behavior, exports, defaults, and target names remain unchanged; authority stays `draft` until exact-head owner approval. |
|
|
46
|
+
| Review checks | Reject a focusable separator, changed role/orientation, pointer data/compound divergence, dropped root inputs, or a target moved off the visible separator. |
|
|
47
|
+
| Governing rules | `architecture:public-component-api/INV1,INV5–INV9`; `architecture:component-theming-surface/INV3–INV6`; WCAG 2.2 SC 1.3.1 and 4.1.2 through `spec:AST-020/FR1`. |
|
|
48
|
+
|
|
49
|
+
This table is a review projection; the body below is authoritative.
|
|
50
|
+
|
|
51
|
+
## Intent
|
|
52
|
+
|
|
53
|
+
DropdownMenuDivider provides the visual and programmatic boundary between groups
|
|
54
|
+
of rows in pointer-menu presentations. It ships directly under the DropdownMenu
|
|
55
|
+
name and through ContextMenu and Breadcrumbs aliases. It owns the separator
|
|
56
|
+
semantics, menu spacing, root input composition, and the
|
|
57
|
+
`dropdown-menu-divider` theme target. The surrounding DropdownMenu, ContextMenu,
|
|
58
|
+
or BreadcrumbItem owns menu lifecycle and row navigation; Divider supplies the
|
|
59
|
+
base rule geometry and paint.
|
|
60
|
+
|
|
61
|
+
Consumer syntax and examples belong in `DropdownMenuDivider.doc.mjs`.
|
|
62
|
+
|
|
63
|
+
## Compatibility and migration
|
|
64
|
+
|
|
65
|
+
- Released default preserved: yes — one horizontal, unlabelled separator.
|
|
66
|
+
- Compatibility class: observational patch; no behavior or public surface changes.
|
|
67
|
+
- Controlled/uncontrolled behavior: not applicable.
|
|
68
|
+
- Migration decision: none.
|
|
69
|
+
|
|
70
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
71
|
+
|
|
72
|
+
## Ownership boundary
|
|
73
|
+
|
|
74
|
+
**Owns**
|
|
75
|
+
|
|
76
|
+
- One horizontal separator root and its public ref, DOM/data/ARIA, class, style,
|
|
77
|
+
event, and `xstyle` composition.
|
|
78
|
+
- Menu-specific block-axis spacing.
|
|
79
|
+
- The `dropdown-menu-divider` target on the same visible root.
|
|
80
|
+
- Pointer-menu data/compound rendering parity for the divider row across
|
|
81
|
+
DropdownMenu, ContextMenu, and breadcrumb menus.
|
|
82
|
+
|
|
83
|
+
**Does not own / non-goals**
|
|
84
|
+
|
|
85
|
+
- Menu open state, focus entry, row activation, or arrow-key navigation — owned by
|
|
86
|
+
DropdownMenu, ContextMenu, BreadcrumbItem, and their shared hooks.
|
|
87
|
+
- Touch and bottom-sheet data dividers — those presentations render Divider
|
|
88
|
+
directly and are owned by their parent menu surface.
|
|
89
|
+
- Base separator rule geometry and token paint — delegated to Divider.
|
|
90
|
+
- Caller-authored grouping meaning — owned by the product callsite.
|
|
91
|
+
|
|
92
|
+
## Public concepts
|
|
93
|
+
|
|
94
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
95
|
+
| ------------- | --------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------- | ---------------------- | -------------------- | --------- | ------------------------------------------------------------------------------- |
|
|
96
|
+
| Separator row | one horizontal unlabelled separator | Divides adjacent pointer-menu row groups without becoming a row or focus stop. | Data and compound pointer-menu modes | present where authored | component | stable | Role and orientation are excluded from caller props and remain component-owned. |
|
|
97
|
+
| Root surface | ref; DOM/data/ARIA; class/style/xstyle/events | Extends the separator root while preserving component semantics. | Every separator row | omitted | public component API | stable | Accepted inputs compose; role and orientation remain authoritative. |
|
|
98
|
+
|
|
99
|
+
## Behavioral and layout contract
|
|
100
|
+
|
|
101
|
+
Draft requirements identify their basis so observed code is not mistaken for an
|
|
102
|
+
intentional decision.
|
|
103
|
+
|
|
104
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
105
|
+
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
|
|
106
|
+
| FR1 | The component MUST render one unlabelled horizontal separator with pointer-menu-specific block spacing. | Released source/docs and focused tests | verify |
|
|
107
|
+
| FR2 | The root MUST expose `role="separator"` and `aria-orientation="horizontal"`, remain outside menu-item collection and arrow-key order, and never become focusable. | WCAG 2.2 SC 1.3.1 and 4.1.2; focused tests | verify |
|
|
108
|
+
| FR3 | In pointer-menu presentation, `{type: "divider"}` in the data API and the compound divider component or alias MUST render the same component DOM, spacing, semantics, and targets. | Released docs/source; data/compound parity test | verify |
|
|
109
|
+
| FR4 | The visible root MUST carry both the delegated `divider` target and the menu-specific `dropdown-menu-divider` target; menu spacing applies before caller `xstyle`. | `architecture:component-theming-surface/INV3–INV6`; source/tests | verify |
|
|
110
|
+
| FR5 | Accepted ref, DOM/data/ARIA, class, style, events, and `xstyle` MUST reach or compose on the separator root without replacing component-owned role or orientation. | `architecture:public-component-api/INV5–INV8` | verify |
|
|
111
|
+
|
|
112
|
+
### Allowed variation
|
|
113
|
+
|
|
114
|
+
- **AV1 — Theme paint.** The delegated Divider and menu-specific targets may restyle
|
|
115
|
+
their owned surface without changing separator semantics or focus behavior.
|
|
116
|
+
- **AV2 — Caller root inputs.** Supported host attributes, handlers, and styling
|
|
117
|
+
inputs may vary while the owned role and orientation remain fixed.
|
|
118
|
+
|
|
119
|
+
### Representative states
|
|
120
|
+
|
|
121
|
+
| State | Required invariant | Allowed variation |
|
|
122
|
+
| ------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
123
|
+
| Pointer data-mode divider | One horizontal separator between data-driven rows. | Adjacent row content and active theme may vary. |
|
|
124
|
+
| Pointer compound divider | DOM and behavior match the pointer data-mode divider. | Host attributes and supported styling inputs may be supplied. |
|
|
125
|
+
| Public aliases | ContextMenuDivider and BreadcrumbMenuDivider render this component. | The containing menu owns lifecycle and navigation. |
|
|
126
|
+
| Touch data divider | Outside this component; the parent surface renders Divider directly. | Touch-menu layout follows the parent contract. |
|
|
127
|
+
| RTL | The horizontal, full-width rule remains direction-neutral. | Surrounding text and menu alignment may mirror. |
|
|
128
|
+
| Narrow viewport | The rule remains inside the menu without horizontal overflow. | Menu width follows its owner. |
|
|
129
|
+
|
|
130
|
+
### Transformation and precedence order
|
|
131
|
+
|
|
132
|
+
- **ORD1 — Root composition.** Apply menu spacing, then caller `xstyle`; combine the
|
|
133
|
+
menu-specific target class with caller `className`; preserve caller `style` and
|
|
134
|
+
host props; keep role and orientation component-owned.
|
|
135
|
+
|
|
136
|
+
### Performance and resources
|
|
137
|
+
|
|
138
|
+
- **PR1 — No external lifecycle.** DropdownMenuDivider installs no listener,
|
|
139
|
+
observer, timer, request, state machine, or Effect.
|
|
140
|
+
|
|
141
|
+
## Accessibility contract
|
|
142
|
+
|
|
143
|
+
- **AR1 — Separator semantics.** The root exposes a horizontal separator and no
|
|
144
|
+
interactive role or tab stop.
|
|
145
|
+
- **AR2 — Navigation exclusion.** Menu navigation collects menu-item roles only, so
|
|
146
|
+
ArrowDown and ArrowUp pass across the separator without focusing it.
|
|
147
|
+
|
|
148
|
+
## Design relationships
|
|
149
|
+
|
|
150
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
151
|
+
| ---------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------- | ------------------ |
|
|
152
|
+
| Menu separator | Visually separates adjacent menu row groups without competing with row content. | Current source and delegated Divider tokens; no component-local visual decision added | supporting | FR1–FR4 |
|
|
153
|
+
|
|
154
|
+
No component-local visual decision is settled by this draft.
|
|
155
|
+
|
|
156
|
+
## Family and system relationships
|
|
157
|
+
|
|
158
|
+
- `architecture:public-component-api` owns exports and root input composition.
|
|
159
|
+
- `architecture:component-theming-surface` owns target placement and composed target
|
|
160
|
+
ownership.
|
|
161
|
+
- `architecture:component-test-sufficiency` owns proportionate, observable evidence.
|
|
162
|
+
- DropdownMenu exports the implementation directly. ContextMenu and Breadcrumbs
|
|
163
|
+
re-export it as `ContextMenuDivider` and `BreadcrumbMenuDivider`, with matching
|
|
164
|
+
props aliases; their pointer presentations retain the same component contract.
|
|
165
|
+
- Touch and bottom-sheet data dividers bypass this component and delegate directly
|
|
166
|
+
to Divider under the parent menu contract.
|
|
167
|
+
- Divider provides the base separator implementation; its draft record is
|
|
168
|
+
observational context, not authority for this contract.
|
|
169
|
+
- `spec:AST-029` keeps this observational draft separate from the audit receipt.
|
|
170
|
+
|
|
171
|
+
## Verification map
|
|
172
|
+
|
|
173
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
174
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
175
|
+
| FR1–FR3, AR1–AR2 | `DropdownMenu.test.tsx`; ContextMenu and Breadcrumbs alias tests; dedicated Storybook state; scoped axe | pointer data/compound, aliases, open menu, keyboard traversal | Pointer DOM modes drift, an alias disappears, the role/orientation changes, or the separator enters arrow-key order | `audit:DropdownMenuDivider/§1,§3,§4,§6` |
|
|
176
|
+
| FR4 | Focused root-composition test; exact-head Neutral/Butter browser frames | light/dark themes; menu and delegated targets | A target leaves the visible root or caller `xstyle` stops composing after menu spacing | `audit:DropdownMenuDivider/§2,§5` |
|
|
177
|
+
| FR5 | Focused root-composition test; lint and typecheck | ref, host attributes, class/style/xstyle/events | An accepted input is dropped or owned semantics are overwritten | `audit:DropdownMenuDivider/§3` |
|
|
178
|
+
| PR1 | Source review | every state | A lifecycle mechanism appears without a resource contract | `audit:DropdownMenuDivider/§7` |
|
|
179
|
+
|
|
180
|
+
## Decision log
|
|
181
|
+
|
|
182
|
+
None. This draft records current facts and introduces no product decision.
|
|
183
|
+
|
|
184
|
+
## Open questions
|
|
185
|
+
|
|
186
|
+
None.
|
|
187
|
+
|
|
188
|
+
## Content boundary
|
|
189
|
+
|
|
190
|
+
This file does not duplicate consumer prop tables/examples, current audit scores,
|
|
191
|
+
screenshot matrices, implementation steps, or system rules. The audit pull request
|
|
192
|
+
owns the closed run evidence required by `spec:AST-029/FR5`.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file DropdownMenuDivider.tsx
|
|
7
|
-
* @input Uses Divider, spacing tokens, themeProps
|
|
7
|
+
* @input Uses Divider, spacing tokens, themeProps; forwards host props to Divider
|
|
8
8
|
* @output Exports DropdownMenuDivider component and DropdownMenuDividerProps
|
|
9
9
|
* @position Sub-component; used inside DropdownMenu
|
|
10
10
|
*
|
|
@@ -37,9 +37,9 @@ const styles = stylex.create({
|
|
|
37
37
|
|
|
38
38
|
const THEME_CLASS_NAME = themeProps('dropdown-menu-divider').className;
|
|
39
39
|
|
|
40
|
-
export interface DropdownMenuDividerProps extends
|
|
41
|
-
BaseProps
|
|
42
|
-
'
|
|
40
|
+
export interface DropdownMenuDividerProps extends Omit<
|
|
41
|
+
BaseProps<HTMLDivElement>,
|
|
42
|
+
'role' | 'aria-orientation'
|
|
43
43
|
> {
|
|
44
44
|
/** Ref forwarded to the separator element. */
|
|
45
45
|
ref?: React.Ref<HTMLDivElement>;
|
|
@@ -49,7 +49,9 @@ export interface DropdownMenuDividerProps extends Pick<
|
|
|
49
49
|
* A horizontal rule separating groups of menu rows.
|
|
50
50
|
*
|
|
51
51
|
* Renders `role="separator"`, so it is never a stop in the menu's arrow-key
|
|
52
|
-
* order. Equivalent to `{type: 'divider'}` in the `items` data API.
|
|
52
|
+
* order. Equivalent to `{type: 'divider'}` in the `items` data API. Host
|
|
53
|
+
* attributes (`id`, `data-*`, `aria-*`) and DOM handlers reach the separator;
|
|
54
|
+
* its role and orientation stay component-owned.
|
|
53
55
|
*
|
|
54
56
|
* @example
|
|
55
57
|
* ```
|
|
@@ -65,9 +67,11 @@ export function DropdownMenuDivider({
|
|
|
65
67
|
className,
|
|
66
68
|
style,
|
|
67
69
|
ref,
|
|
70
|
+
...rest
|
|
68
71
|
}: DropdownMenuDividerProps) {
|
|
69
72
|
return (
|
|
70
73
|
<Divider
|
|
74
|
+
{...rest}
|
|
71
75
|
ref={ref}
|
|
72
76
|
xstyle={[styles.divider, xstyle]}
|
|
73
77
|
className={
|
|
@@ -1494,3 +1494,188 @@ describe('DropdownMenuSubMenu drill-in on a phone', () => {
|
|
|
1494
1494
|
screen.getAllByRole('menu', {name, ...h}).find(isShown);
|
|
1495
1495
|
});
|
|
1496
1496
|
});
|
|
1497
|
+
|
|
1498
|
+
describe('DropdownMenuSubMenu light dismiss of the root menu (#6893)', () => {
|
|
1499
|
+
// The browser closes the root `popover=\"auto\"` element itself on an outside
|
|
1500
|
+
// tap; that path never reaches the submenu, whose flyout is
|
|
1501
|
+
// `popover=\"manual\"`. Simulate it by hiding the root popover element
|
|
1502
|
+
// directly, exactly what light dismiss does to it.
|
|
1503
|
+
function lightDismissRootMenu() {
|
|
1504
|
+
const menu = screen.getByRole('menu', {name: /Actions/, hidden: true});
|
|
1505
|
+
const rootPopover = menu.closest<HTMLElement>('[popover]');
|
|
1506
|
+
expect(rootPopover).not.toBeNull();
|
|
1507
|
+
act(() => {
|
|
1508
|
+
rootPopover!.hidePopover();
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function getFlyoutPopover(name: RegExp) {
|
|
1513
|
+
const flyout = screen.getByRole('menu', {name, hidden: true});
|
|
1514
|
+
return flyout.closest<HTMLElement>('[popover]');
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
it('closes an open flyout when the root menu is light-dismissed', async () => {
|
|
1518
|
+
const user = userEvent.setup();
|
|
1519
|
+
render(<MoveMenu />);
|
|
1520
|
+
await user.click(screen.getByRole('button', {name: /Actions/}));
|
|
1521
|
+
const trigger = screen.getByRole('menuitem', {
|
|
1522
|
+
name: /Move to/,
|
|
1523
|
+
hidden: true,
|
|
1524
|
+
});
|
|
1525
|
+
await user.click(trigger);
|
|
1526
|
+
await waitFor(() => {
|
|
1527
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1528
|
+
});
|
|
1529
|
+
const flyoutPopover = getFlyoutPopover(/Move to/);
|
|
1530
|
+
expect(flyoutPopover).toHaveAttribute('popover-open', '');
|
|
1531
|
+
|
|
1532
|
+
lightDismissRootMenu();
|
|
1533
|
+
|
|
1534
|
+
await waitFor(() => {
|
|
1535
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1536
|
+
});
|
|
1537
|
+
expect(flyoutPopover).not.toHaveAttribute('popover-open');
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it('leaves the submenu collapsed when the menu reopens after a light dismiss', async () => {
|
|
1541
|
+
const user = userEvent.setup();
|
|
1542
|
+
render(<MoveMenu />);
|
|
1543
|
+
await user.click(screen.getByRole('button', {name: /Actions/}));
|
|
1544
|
+
const trigger = screen.getByRole('menuitem', {
|
|
1545
|
+
name: /Move to/,
|
|
1546
|
+
hidden: true,
|
|
1547
|
+
});
|
|
1548
|
+
await user.click(trigger);
|
|
1549
|
+
await waitFor(() => {
|
|
1550
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
lightDismissRootMenu();
|
|
1554
|
+
await user.click(screen.getByRole('button', {name: /Actions/}));
|
|
1555
|
+
|
|
1556
|
+
await waitFor(() => {
|
|
1557
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1558
|
+
});
|
|
1559
|
+
expect(getFlyoutPopover(/Move to/)).not.toHaveAttribute('popover-open');
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
it('closes an open flyout when the root menu closes via controlled isMenuOpen', async () => {
|
|
1563
|
+
const user = userEvent.setup();
|
|
1564
|
+
function ControlledMenu() {
|
|
1565
|
+
const [open, setOpen] = useState(true);
|
|
1566
|
+
return (
|
|
1567
|
+
<>
|
|
1568
|
+
<DropdownMenu
|
|
1569
|
+
button={{label: 'Actions'}}
|
|
1570
|
+
isMenuOpen={open}
|
|
1571
|
+
onOpenChange={setOpen}>
|
|
1572
|
+
<DropdownMenuSubMenu label="Move to">
|
|
1573
|
+
<DropdownMenuItem label="Folder A" onClick={() => {}} />
|
|
1574
|
+
</DropdownMenuSubMenu>
|
|
1575
|
+
</DropdownMenu>
|
|
1576
|
+
<button type="button" onClick={() => setOpen(false)}>
|
|
1577
|
+
close it
|
|
1578
|
+
</button>
|
|
1579
|
+
</>
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
render(<ControlledMenu />);
|
|
1583
|
+
const trigger = screen.getByRole('menuitem', {
|
|
1584
|
+
name: /Move to/,
|
|
1585
|
+
hidden: true,
|
|
1586
|
+
});
|
|
1587
|
+
await user.click(trigger);
|
|
1588
|
+
await waitFor(() => {
|
|
1589
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1590
|
+
});
|
|
1591
|
+
const flyoutPopover = getFlyoutPopover(/Move to/);
|
|
1592
|
+
|
|
1593
|
+
await user.click(screen.getByRole('button', {name: /close it/}));
|
|
1594
|
+
|
|
1595
|
+
await waitFor(() => {
|
|
1596
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1597
|
+
});
|
|
1598
|
+
expect(flyoutPopover).not.toHaveAttribute('popover-open');
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
it('closes flyouts nested two levels deep on light dismiss', async () => {
|
|
1602
|
+
const user = userEvent.setup();
|
|
1603
|
+
render(
|
|
1604
|
+
<DropdownMenu button={{label: 'Actions'}}>
|
|
1605
|
+
<DropdownMenuSubMenu label="Move to">
|
|
1606
|
+
<DropdownMenuSubMenu label="Deeper">
|
|
1607
|
+
<DropdownMenuItem label="Leaf" onClick={() => {}} />
|
|
1608
|
+
</DropdownMenuSubMenu>
|
|
1609
|
+
</DropdownMenuSubMenu>
|
|
1610
|
+
</DropdownMenu>,
|
|
1611
|
+
);
|
|
1612
|
+
await user.click(screen.getByRole('button', {name: /Actions/}));
|
|
1613
|
+
const first = screen.getByRole('menuitem', {
|
|
1614
|
+
name: /Move to/,
|
|
1615
|
+
hidden: true,
|
|
1616
|
+
});
|
|
1617
|
+
await user.click(first);
|
|
1618
|
+
await waitFor(() => {
|
|
1619
|
+
expect(first).toHaveAttribute('aria-expanded', 'true');
|
|
1620
|
+
});
|
|
1621
|
+
const deeper = screen.getByRole('menuitem', {
|
|
1622
|
+
name: /Deeper/,
|
|
1623
|
+
hidden: true,
|
|
1624
|
+
});
|
|
1625
|
+
await user.click(deeper);
|
|
1626
|
+
await waitFor(() => {
|
|
1627
|
+
expect(deeper).toHaveAttribute('aria-expanded', 'true');
|
|
1628
|
+
});
|
|
1629
|
+
const firstPopover = getFlyoutPopover(/Move to/);
|
|
1630
|
+
const deeperPopover = getFlyoutPopover(/Deeper/);
|
|
1631
|
+
|
|
1632
|
+
lightDismissRootMenu();
|
|
1633
|
+
|
|
1634
|
+
await waitFor(() => {
|
|
1635
|
+
expect(first).toHaveAttribute('aria-expanded', 'false');
|
|
1636
|
+
});
|
|
1637
|
+
await waitFor(() => {
|
|
1638
|
+
expect(deeper).toHaveAttribute('aria-expanded', 'false');
|
|
1639
|
+
});
|
|
1640
|
+
expect(firstPopover).not.toHaveAttribute('popover-open');
|
|
1641
|
+
expect(deeperPopover).not.toHaveAttribute('popover-open');
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// Regression for the stale hover timer: a hover over the submenu trigger
|
|
1645
|
+
// schedules an open after the show delay. A press on the same trigger
|
|
1646
|
+
// cancels its own scheduled hover-open (that is the click guard), so the
|
|
1647
|
+
// way a timer is still pending at close time is a close that comes from
|
|
1648
|
+
// elsewhere — here the root menu's light dismiss, the #6893 case. The
|
|
1649
|
+
// close must cancel the scheduled hover-open: once the delay elapses the
|
|
1650
|
+
// flyout stays closed instead of reopening behind the dismissed menu.
|
|
1651
|
+
// Time is driven manually so the dismiss lands inside the show delay;
|
|
1652
|
+
// auto-advancing timers would let the hover-open fire before it.
|
|
1653
|
+
it('cancels a hover-open scheduled before the menu was closed from elsewhere', async () => {
|
|
1654
|
+
vi.useFakeTimers();
|
|
1655
|
+
try {
|
|
1656
|
+
render(<MoveMenu />);
|
|
1657
|
+
fireEvent.click(screen.getByRole('button', {name: /Actions/}));
|
|
1658
|
+
const trigger = screen.getByRole('menuitem', {
|
|
1659
|
+
name: /Move to/,
|
|
1660
|
+
hidden: true,
|
|
1661
|
+
});
|
|
1662
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1663
|
+
|
|
1664
|
+
// Schedules the hover-open (default show delay 150ms).
|
|
1665
|
+
fireEvent.mouseEnter(trigger);
|
|
1666
|
+
|
|
1667
|
+
// Close from elsewhere while the hover-open is still pending.
|
|
1668
|
+
lightDismissRootMenu();
|
|
1669
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1670
|
+
|
|
1671
|
+
// The scheduled delay passes; the flyout must stay closed.
|
|
1672
|
+
await act(async () => {
|
|
1673
|
+
vi.advanceTimersByTime(300);
|
|
1674
|
+
});
|
|
1675
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1676
|
+
expect(getFlyoutPopover(/Move to/)).not.toHaveAttribute('popover-open');
|
|
1677
|
+
} finally {
|
|
1678
|
+
vi.useRealTimers();
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
});
|
|
@@ -97,7 +97,9 @@ import {
|
|
|
97
97
|
} from './menuItemRoles';
|
|
98
98
|
import {
|
|
99
99
|
DropdownMenuContext,
|
|
100
|
+
DropdownMenuRootOpenContext,
|
|
100
101
|
useDropdownMenuContext,
|
|
102
|
+
useDropdownMenuRootOpen,
|
|
101
103
|
type DropdownMenuContextValue,
|
|
102
104
|
} from './DropdownMenuContext';
|
|
103
105
|
import {focusMenuItemOnHover} from './menuItemHover';
|
|
@@ -166,8 +168,8 @@ const flyoutStyles = stylex.create({
|
|
|
166
168
|
flexDirection: 'column',
|
|
167
169
|
gap: spacingVars['--spacing-0-5'],
|
|
168
170
|
maxInlineSize: stylex.firstThatWorks(
|
|
169
|
-
layerViewportInset.maxInlineSize
|
|
170
|
-
layerViewportInset.maxInlineSizeFallback
|
|
171
|
+
`calc(${layerViewportInset.maxInlineSize})`,
|
|
172
|
+
`calc(${layerViewportInset.maxInlineSizeFallback})`,
|
|
171
173
|
),
|
|
172
174
|
maxHeight: stylex.firstThatWorks(
|
|
173
175
|
MENU_MAX_BLOCK_SIZE,
|
|
@@ -451,13 +453,17 @@ export function DropdownMenuSubMenu(
|
|
|
451
453
|
// Hover intent and the shared hover→click guard only: this level owns its own
|
|
452
454
|
// click handling, roving focus and typeahead. popover="manual", so the
|
|
453
455
|
// invoker wiring other consumers need does not apply. A drill-in row has no
|
|
454
|
-
// hover: a finger drives it, and a press opens it.
|
|
456
|
+
// hover: a finger drives it, and a press opens it. Hover is also gated on
|
|
457
|
+
// the root menu being open, so a hover-open scheduled before the menu
|
|
458
|
+
// closed from elsewhere is cancelled instead of firing into the hidden
|
|
459
|
+
// menu (#6893).
|
|
460
|
+
const isRootMenuOpen = useDropdownMenuRootOpen();
|
|
455
461
|
const {triggerProps, contentProps, confirmHoverOpen} =
|
|
456
462
|
useMenuHover<HTMLDivElement>({
|
|
457
463
|
show: showLayer,
|
|
458
464
|
hide: hideLayer,
|
|
459
465
|
isOpen: isFlyoutOpen,
|
|
460
|
-
isEnabled: canOpen && !isDrillIn,
|
|
466
|
+
isEnabled: canOpen && !isDrillIn && isRootMenuOpen,
|
|
461
467
|
// The safe triangle toward the flyout's near edge is built from the
|
|
462
468
|
// flyout this component renders, not the hook's own list ref.
|
|
463
469
|
flyoutRef: menuRef,
|
|
@@ -770,7 +776,7 @@ export function DropdownMenuSubMenu(
|
|
|
770
776
|
: flyoutStyles.popoverCustomWidth(resolvedMenuWidth.value)
|
|
771
777
|
: flyoutStyles.popover;
|
|
772
778
|
|
|
773
|
-
const
|
|
779
|
+
const nestedRows = (
|
|
774
780
|
<DropdownMenuContext value={nestedMenuContext}>
|
|
775
781
|
{isDrillIn && (
|
|
776
782
|
<DropdownMenuItem
|
|
@@ -790,6 +796,14 @@ export function DropdownMenuSubMenu(
|
|
|
790
796
|
{children}
|
|
791
797
|
</DropdownMenuContext>
|
|
792
798
|
);
|
|
799
|
+
// Nested rows live in this level's flyout, a layer of their own where the
|
|
800
|
+
// layer-scoped signal resets; re-provide it so their pending hover intent
|
|
801
|
+
// dies with the root menu too.
|
|
802
|
+
const nestedContent = (
|
|
803
|
+
<DropdownMenuRootOpenContext value={isRootMenuOpen}>
|
|
804
|
+
{nestedRows}
|
|
805
|
+
</DropdownMenuRootOpenContext>
|
|
806
|
+
);
|
|
793
807
|
|
|
794
808
|
return (
|
|
795
809
|
<>
|