@astryxdesign/core 0.7.1 → 0.7.2-canary.020e1fe
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/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +40 -1
- 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 +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +83 -11
- 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/DropdownMenuGroup.d.ts +6 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
- 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/MobileNav/MobileNav.d.ts +9 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +30 -3
- 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/scripts/agent-doc-state.mjs +38 -0
- package/scripts/postinstall.mjs +1 -1
- package/src/AppShell/AppShell.tsx +45 -1
- 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/Card/Card.doc.mjs +1 -1
- 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/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -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 +473 -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.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +79 -18
- package/src/DropdownMenu/DropdownMenu.tsx +83 -12
- 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/DropdownMenuGroup.doc.mjs +169 -10
- package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- 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__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -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/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -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/Field/FieldLabel.doc.mjs +133 -26
- package/src/Field/FieldLabel.spec.md +172 -0
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
- 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/Icon/Icon.doc.mjs +36 -0
- 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/LayoutHeader.doc.mjs +5 -0
- 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 +3 -3
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MobileNav/MobileNav.tsx +30 -3
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
- package/src/MoreMenu/MoreMenu.test.tsx +5 -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/ProgressBar/ProgressBar.doc.mjs +1 -0
- 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/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/Selector/hooks.ts +6 -2
- package/src/Selector/useCombobox.doc.mjs +237 -0
- package/src/SideNav/SideNav.spec.md +227 -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/Spinner/Spinner.doc.mjs +1 -1
- 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/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/Tokenizer/Tokenizer.test.tsx +54 -1
- package/src/Toolbar/Toolbar.test.tsx +0 -8
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
- 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/postinstall.test.mjs +46 -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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DrawerHeaderAudit.a11y.chromium.spec.ts
|
|
5
|
+
* @input Exact-head Core/DrawerHeader Storybook fixture in light/dark, neutral/custom themes, LTR/RTL, wide/narrow, rest/focus states
|
|
6
|
+
* @output Bounded screenshots and fail-closed sensor receipts for the DrawerHeader whole-component audit
|
|
7
|
+
* @position Component-owned Night Watch browser evidence; CI runs it only for a DrawerHeader component-audit pull request
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {createHash} from 'node:crypto';
|
|
11
|
+
import {execFileSync} from 'node:child_process';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_STORYBOOK_DIR,
|
|
18
|
+
serveStorybook,
|
|
19
|
+
type StaticServer,
|
|
20
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
21
|
+
|
|
22
|
+
const OUTPUT = path.resolve('test-results/drawer-header-audit-evidence');
|
|
23
|
+
const WIDE = {width: 1024, height: 768};
|
|
24
|
+
const NARROW = {width: 320, height: 720};
|
|
25
|
+
|
|
26
|
+
type Mode = 'light' | 'dark';
|
|
27
|
+
type Direction = 'ltr' | 'rtl';
|
|
28
|
+
type Theme = 'neutral' | 'butter';
|
|
29
|
+
type State = 'rest' | 'focus';
|
|
30
|
+
|
|
31
|
+
type Case = {
|
|
32
|
+
key: string;
|
|
33
|
+
story: 'audit' | 'audit-without-close';
|
|
34
|
+
hasClose: boolean;
|
|
35
|
+
mode: Mode;
|
|
36
|
+
direction: Direction;
|
|
37
|
+
theme: Theme;
|
|
38
|
+
state: State;
|
|
39
|
+
viewport: {width: number; height: number};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const cases: Case[] = [];
|
|
43
|
+
function addPair(
|
|
44
|
+
key: string,
|
|
45
|
+
options: Partial<Omit<Case, 'key' | 'mode'>> = {},
|
|
46
|
+
) {
|
|
47
|
+
for (const mode of ['light', 'dark'] as const) {
|
|
48
|
+
cases.push({
|
|
49
|
+
key: `${key}-${mode}`,
|
|
50
|
+
story: options.story ?? 'audit',
|
|
51
|
+
hasClose: options.hasClose ?? true,
|
|
52
|
+
mode,
|
|
53
|
+
direction: options.direction ?? 'ltr',
|
|
54
|
+
theme: options.theme ?? 'neutral',
|
|
55
|
+
state: options.state ?? 'rest',
|
|
56
|
+
viewport: options.viewport ?? WIDE,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
addPair('rest');
|
|
62
|
+
addPair('rest-without-close', {
|
|
63
|
+
story: 'audit-without-close',
|
|
64
|
+
hasClose: false,
|
|
65
|
+
});
|
|
66
|
+
addPair('rest-custom-theme', {theme: 'butter'});
|
|
67
|
+
addPair('rest-rtl', {direction: 'rtl'});
|
|
68
|
+
addPair('rest-narrow', {viewport: NARROW});
|
|
69
|
+
addPair('focus', {state: 'focus'});
|
|
70
|
+
|
|
71
|
+
test.describe.configure({mode: 'serial', retries: 0});
|
|
72
|
+
let server: StaticServer;
|
|
73
|
+
let checkoutSha: string;
|
|
74
|
+
let storybookSha: string;
|
|
75
|
+
|
|
76
|
+
function storyUrl(item: Case) {
|
|
77
|
+
return `${server.origin}/iframe.html?id=core-drawerheader--${item.story}&viewMode=story&globals=colorMode:${item.mode};astryxTheme:${item.theme};direction:${item.direction}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function settle(page: Page) {
|
|
81
|
+
await holdMotionStill(page);
|
|
82
|
+
await page.evaluate(async () => document.fonts.ready);
|
|
83
|
+
await page.waitForTimeout(40);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test.beforeAll(async () => {
|
|
87
|
+
fs.rmSync(OUTPUT, {recursive: true, force: true});
|
|
88
|
+
fs.mkdirSync(OUTPUT, {recursive: true});
|
|
89
|
+
checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
}).trim();
|
|
92
|
+
const expected = process.env.ASTRYX_HEAD_SHA;
|
|
93
|
+
if (expected && expected !== checkoutSha) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`checkout ${checkoutSha} does not match expected head ${expected}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
server = await serveStorybook(
|
|
99
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
100
|
+
);
|
|
101
|
+
const response = await fetch(`${server.origin}/astryx-build-sha.txt`);
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`Storybook build has no SHA stamp (${response.status})`);
|
|
104
|
+
}
|
|
105
|
+
storybookSha = (await response.text()).trim();
|
|
106
|
+
if (storybookSha !== checkoutSha) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Storybook ${storybookSha} does not match checkout ${checkoutSha}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test.afterAll(async () => {
|
|
114
|
+
await server?.close();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
for (const item of cases) {
|
|
118
|
+
test(item.key, async ({browser}) => {
|
|
119
|
+
const context = await browser.newContext({
|
|
120
|
+
viewport: item.viewport,
|
|
121
|
+
colorScheme: item.mode,
|
|
122
|
+
reducedMotion: 'reduce',
|
|
123
|
+
});
|
|
124
|
+
const page = await context.newPage();
|
|
125
|
+
const pageErrors: string[] = [];
|
|
126
|
+
page.on('pageerror', error => pageErrors.push(String(error)));
|
|
127
|
+
await page.goto(storyUrl(item), {waitUntil: 'load'});
|
|
128
|
+
await page.locator('#storybook-root').waitFor();
|
|
129
|
+
|
|
130
|
+
const drawer = page.locator('dialog.astryx-drawer:visible');
|
|
131
|
+
await expect(drawer).toHaveCount(1);
|
|
132
|
+
await settle(page);
|
|
133
|
+
|
|
134
|
+
const close = page.getByRole('button', {name: 'Close', exact: true});
|
|
135
|
+
if (item.hasClose) {
|
|
136
|
+
await expect(close).toBeVisible();
|
|
137
|
+
} else {
|
|
138
|
+
await expect(close).toHaveCount(0);
|
|
139
|
+
}
|
|
140
|
+
if (item.state === 'focus') {
|
|
141
|
+
await close.focus();
|
|
142
|
+
await page.keyboard.press('Tab');
|
|
143
|
+
await expect(close).toBeFocused();
|
|
144
|
+
expect(
|
|
145
|
+
await close.evaluate(element => element.matches(':focus-visible')),
|
|
146
|
+
).toBe(true);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const header = page.locator('.astryx-drawer-header');
|
|
150
|
+
const startSlot = page.locator('.astryx-drawer-header-start-content');
|
|
151
|
+
const titleBlock = page.locator('.astryx-drawer-header-title-block');
|
|
152
|
+
const endSlot = page.locator('.astryx-drawer-header-end-content');
|
|
153
|
+
await expect(header).toHaveCount(1);
|
|
154
|
+
await expect(startSlot).toHaveCount(1);
|
|
155
|
+
await expect(titleBlock).toHaveCount(1);
|
|
156
|
+
await expect(endSlot).toHaveCount(1);
|
|
157
|
+
await expect(startSlot).toHaveText('0');
|
|
158
|
+
await expect(endSlot).toContainText('0');
|
|
159
|
+
await expect(
|
|
160
|
+
page.getByRole('heading', {
|
|
161
|
+
level: 2,
|
|
162
|
+
name: 'Deployment health for web-prod-04',
|
|
163
|
+
}),
|
|
164
|
+
).toBeVisible();
|
|
165
|
+
await expect(
|
|
166
|
+
page.getByText('us-east-1, deployed 12 minutes ago', {exact: true}),
|
|
167
|
+
).toBeVisible();
|
|
168
|
+
|
|
169
|
+
const [headerBox, startBox, titleBox, endBox, closeBox] = await Promise.all(
|
|
170
|
+
[
|
|
171
|
+
header.boundingBox(),
|
|
172
|
+
startSlot.boundingBox(),
|
|
173
|
+
titleBlock.boundingBox(),
|
|
174
|
+
endSlot.boundingBox(),
|
|
175
|
+
item.hasClose ? close.boundingBox() : Promise.resolve(null),
|
|
176
|
+
],
|
|
177
|
+
);
|
|
178
|
+
if (
|
|
179
|
+
headerBox == null ||
|
|
180
|
+
startBox == null ||
|
|
181
|
+
titleBox == null ||
|
|
182
|
+
endBox == null ||
|
|
183
|
+
(item.hasClose && closeBox == null)
|
|
184
|
+
) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`${item.key}: a visible DrawerHeader part has no geometry`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const observed = await page.evaluate(() => ({
|
|
190
|
+
theme: document
|
|
191
|
+
.querySelector('[data-astryx-theme]')
|
|
192
|
+
?.getAttribute('data-astryx-theme'),
|
|
193
|
+
mode: document.documentElement.getAttribute('data-theme'),
|
|
194
|
+
direction: getComputedStyle(
|
|
195
|
+
document.querySelector('.astryx-drawer-header') as Element,
|
|
196
|
+
).direction,
|
|
197
|
+
viewport: {width: innerWidth, height: innerHeight},
|
|
198
|
+
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
199
|
+
horizontalOverflow: document.documentElement.scrollWidth > innerWidth + 1,
|
|
200
|
+
fonts: document.fonts.status,
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
expect(pageErrors).toEqual([]);
|
|
204
|
+
expect(observed.theme).toBe(item.theme);
|
|
205
|
+
expect(observed.mode).toBe(item.mode);
|
|
206
|
+
expect(observed.direction).toBe(item.direction);
|
|
207
|
+
expect(observed.viewport).toEqual(item.viewport);
|
|
208
|
+
expect(observed.reducedMotion).toBe(true);
|
|
209
|
+
expect(observed.horizontalOverflow).toBe(false);
|
|
210
|
+
expect(observed.fonts).toBe('loaded');
|
|
211
|
+
expect(headerBox.width).toBeGreaterThan(0);
|
|
212
|
+
if (closeBox != null) {
|
|
213
|
+
expect(closeBox.width).toBeGreaterThanOrEqual(24);
|
|
214
|
+
expect(closeBox.height).toBeGreaterThanOrEqual(24);
|
|
215
|
+
}
|
|
216
|
+
if (item.direction === 'ltr') {
|
|
217
|
+
expect(startBox.x).toBeLessThan(titleBox.x);
|
|
218
|
+
expect(titleBox.x).toBeLessThan(endBox.x);
|
|
219
|
+
} else {
|
|
220
|
+
expect(startBox.x).toBeGreaterThan(titleBox.x);
|
|
221
|
+
expect(titleBox.x).toBeGreaterThan(endBox.x);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const png = await page.screenshot({animations: 'disabled'});
|
|
225
|
+
expect(png.length).toBeGreaterThan(100);
|
|
226
|
+
const image = `${item.key}.png`;
|
|
227
|
+
const receipt = {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
expected: {
|
|
230
|
+
build: checkoutSha,
|
|
231
|
+
storyId: `core-drawerheader--${item.story}`,
|
|
232
|
+
mode: item.mode,
|
|
233
|
+
direction: item.direction,
|
|
234
|
+
theme: item.theme,
|
|
235
|
+
state: item.state,
|
|
236
|
+
viewport: item.viewport,
|
|
237
|
+
startContent: '0',
|
|
238
|
+
endContent: '0',
|
|
239
|
+
titleRole: 'heading',
|
|
240
|
+
titleLevel: 2,
|
|
241
|
+
closeName: item.hasClose ? 'Close' : null,
|
|
242
|
+
},
|
|
243
|
+
observed: {
|
|
244
|
+
build: storybookSha,
|
|
245
|
+
...observed,
|
|
246
|
+
geometry: {
|
|
247
|
+
header: headerBox,
|
|
248
|
+
startSlot: startBox,
|
|
249
|
+
titleBlock: titleBox,
|
|
250
|
+
endSlot: endBox,
|
|
251
|
+
close: closeBox,
|
|
252
|
+
},
|
|
253
|
+
closeFocused: item.hasClose
|
|
254
|
+
? await close.evaluate(element => element === document.activeElement)
|
|
255
|
+
: false,
|
|
256
|
+
closeFocusVisible: item.hasClose
|
|
257
|
+
? await close.evaluate(element => element.matches(':focus-visible'))
|
|
258
|
+
: false,
|
|
259
|
+
pageErrors,
|
|
260
|
+
},
|
|
261
|
+
image: {
|
|
262
|
+
file: image,
|
|
263
|
+
sha256: createHash('sha256').update(png).digest('hex'),
|
|
264
|
+
width: png.readUInt32BE(16),
|
|
265
|
+
height: png.readUInt32BE(20),
|
|
266
|
+
},
|
|
267
|
+
passed: true,
|
|
268
|
+
};
|
|
269
|
+
fs.writeFileSync(path.join(OUTPUT, image), png);
|
|
270
|
+
fs.writeFileSync(
|
|
271
|
+
path.join(OUTPUT, `${item.key}.sensors.json`),
|
|
272
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
273
|
+
);
|
|
274
|
+
await context.close();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
test('manifest is complete', async () => {
|
|
279
|
+
const receipts = fs
|
|
280
|
+
.readdirSync(OUTPUT)
|
|
281
|
+
.filter(file => file.endsWith('.sensors.json'));
|
|
282
|
+
expect(receipts).toHaveLength(cases.length);
|
|
283
|
+
const manifest = {
|
|
284
|
+
schemaVersion: 1,
|
|
285
|
+
component: 'core/DrawerHeader',
|
|
286
|
+
build: checkoutSha,
|
|
287
|
+
frames: cases.map(item => ({
|
|
288
|
+
key: item.key,
|
|
289
|
+
image: `${item.key}.png`,
|
|
290
|
+
receipt: `${item.key}.sensors.json`,
|
|
291
|
+
})),
|
|
292
|
+
};
|
|
293
|
+
fs.writeFileSync(
|
|
294
|
+
path.join(OUTPUT, 'manifest.json'),
|
|
295
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file index.ts
|
|
5
|
+
* @input Drawer.tsx, DrawerHeader.tsx
|
|
6
|
+
* @output Re-exports Drawer and DrawerHeader components and their prop types
|
|
7
|
+
* @position Core entry point for the Drawer directory (`@astryxdesign/core/Drawer`)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {Drawer} from './Drawer';
|
|
11
|
+
export type {DrawerProps} from './Drawer';
|
|
12
|
+
export {DrawerHeader} from './DrawerHeader';
|
|
13
|
+
export type {DrawerHeaderProps} from './DrawerHeader';
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useDrawerDialogPresence.ts
|
|
7
|
+
* @input Controlled open state, modal mode, dialog ref, and rendered-state setter
|
|
8
|
+
* @output Coordinates native top-layer presence, exit timing, focus restoration, and unmount cleanup
|
|
9
|
+
* @position Drawer-internal hook; consumed only by Drawer.tsx
|
|
10
|
+
*
|
|
11
|
+
* The drawer has two independent notions of presence:
|
|
12
|
+
* - React's rendered state keeps the panel visible for its CSS exit.
|
|
13
|
+
* - Native `showModal()` or `showPopover()` state keeps it in the browser top
|
|
14
|
+
* layer.
|
|
15
|
+
*
|
|
16
|
+
* Their close ordering is a browser-visible invariant: the panel must finish
|
|
17
|
+
* its exit, then leave the active native host and hide in the same task. If the
|
|
18
|
+
* hide lands a frame later, a transformed ancestor becomes the containing block
|
|
19
|
+
* for the now-non-top-layer `position: fixed` panel and it paints back inside
|
|
20
|
+
* the page for one frame.
|
|
21
|
+
*
|
|
22
|
+
* SYNC: When modified, update:
|
|
23
|
+
* - /packages/core/src/Drawer/Drawer.test.tsx
|
|
24
|
+
* - /.github/scripts/modal-close-visibility.js
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
useEffect,
|
|
29
|
+
useRef,
|
|
30
|
+
type Dispatch,
|
|
31
|
+
type RefObject,
|
|
32
|
+
type SetStateAction,
|
|
33
|
+
} from 'react';
|
|
34
|
+
import {flushSync} from 'react-dom';
|
|
35
|
+
|
|
36
|
+
/** Slack past the computed transition before the backstop gives up waiting. */
|
|
37
|
+
const EXIT_BACKSTOP_BUFFER_MS = 50;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hold used when the transition duration cannot be read — an unresolved
|
|
41
|
+
* `var()` outside a real browser. Picking a fixed number would otherwise make
|
|
42
|
+
* an assumption about the consumer's theme.
|
|
43
|
+
*/
|
|
44
|
+
const EXIT_FALLBACK_MS = 250;
|
|
45
|
+
|
|
46
|
+
type UseDrawerDialogPresenceOptions = {
|
|
47
|
+
dialogRef: RefObject<HTMLDialogElement | null>;
|
|
48
|
+
isOpen: boolean;
|
|
49
|
+
isModal: boolean;
|
|
50
|
+
setIsRendered: Dispatch<SetStateAction<boolean>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function isPopoverOpen(dialog: HTMLDialogElement): boolean {
|
|
54
|
+
try {
|
|
55
|
+
return dialog.matches(':popover-open');
|
|
56
|
+
} catch {
|
|
57
|
+
// jsdom and pre-Popover browsers do not recognize :popover-open.
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isDrawerHostOpen(
|
|
63
|
+
dialog: HTMLDialogElement,
|
|
64
|
+
isModal: boolean,
|
|
65
|
+
): boolean {
|
|
66
|
+
return isModal ? dialog.open : isPopoverOpen(dialog) || dialog.open;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function showDrawerHost(dialog: HTMLDialogElement, isModal: boolean): void {
|
|
70
|
+
if (isModal) {
|
|
71
|
+
dialog.showModal();
|
|
72
|
+
} else if (typeof dialog.showPopover === 'function') {
|
|
73
|
+
dialog.showPopover();
|
|
74
|
+
} else {
|
|
75
|
+
// Reduced fallback for browsers below the Popover API support floor.
|
|
76
|
+
dialog.show();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function dispatchDialogClose(dialog: HTMLDialogElement): void {
|
|
81
|
+
const EventConstructor = dialog.ownerDocument.defaultView?.Event ?? Event;
|
|
82
|
+
dialog.dispatchEvent(new EventConstructor('close'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Leaves the active native host. Returns whether the caller owes a synthetic
|
|
87
|
+
* `close` dispatch: `dialog.close()` used to power the non-modal path, and
|
|
88
|
+
* consumers observe its native `close` event through refs/onClose. Popover
|
|
89
|
+
* dismissal has no equivalent event, so that public DOM contract is preserved
|
|
90
|
+
* explicitly — but by the caller, not here. Native `close` is queued as a
|
|
91
|
+
* task, so a consumer's listener always ran after this hook's own focus
|
|
92
|
+
* restore; the synthetic event must keep that ordering (and fire exactly once
|
|
93
|
+
* per exit, including the unmount path).
|
|
94
|
+
*/
|
|
95
|
+
function hideDrawerHost(dialog: HTMLDialogElement, isModal: boolean): boolean {
|
|
96
|
+
if (!isModal && typeof dialog.hidePopover === 'function') {
|
|
97
|
+
dialog.hidePopover();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (dialog.open) {
|
|
101
|
+
dialog.close();
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Coordinates the native dialog and React-rendered presence for Drawer.
|
|
108
|
+
*
|
|
109
|
+
* Opening captures the trigger, enters the modal-dialog or manual-popover host,
|
|
110
|
+
* and honours the component's `data-autofocus` contract. Closing waits for the
|
|
111
|
+
* actual transform transition (with a computed-duration backstop), then leaves
|
|
112
|
+
* the native host and synchronously hides the panel before the browser can paint
|
|
113
|
+
* it outside the top layer, restores focus to the captured trigger, and only
|
|
114
|
+
* then dispatches the popover host's synthetic `close`. Unmount cleanup closes
|
|
115
|
+
* a host left open by React Activity or a removed subtree.
|
|
116
|
+
*/
|
|
117
|
+
export function useDrawerDialogPresence({
|
|
118
|
+
dialogRef,
|
|
119
|
+
isOpen,
|
|
120
|
+
isModal,
|
|
121
|
+
setIsRendered,
|
|
122
|
+
}: UseDrawerDialogPresenceOptions): void {
|
|
123
|
+
// Element focused when the drawer opened — restored on close.
|
|
124
|
+
const triggerElementRef = useRef<HTMLElement | null>(null);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const dialog = dialogRef.current;
|
|
128
|
+
if (!dialog) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isOpen) {
|
|
133
|
+
if (!isDrawerHostOpen(dialog, isModal)) {
|
|
134
|
+
triggerElementRef.current =
|
|
135
|
+
document.activeElement as HTMLElement | null;
|
|
136
|
+
showDrawerHost(dialog, isModal);
|
|
137
|
+
// React's autoFocus calls .focus() during commit, before the native host
|
|
138
|
+
// is visible, so it silently fails — honour data-autofocus instead (same
|
|
139
|
+
// contract as Dialog).
|
|
140
|
+
dialog.querySelector<HTMLElement>('[data-autofocus]')?.focus();
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!isDrawerHostOpen(dialog, isModal)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return waitForDrawerExit(dialog, () => {
|
|
150
|
+
const owesCloseEvent = hideDrawerHost(dialog, isModal);
|
|
151
|
+
// flushSync, not a plain setState: React's default scheduling can land
|
|
152
|
+
// the commit after the next paint, and that one frame is exactly the
|
|
153
|
+
// bug — the panel paints outside the top layer. Both happen in this
|
|
154
|
+
// task, so the browser never gets to paint between them.
|
|
155
|
+
// eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the panel must stop painting in the task the native host closes
|
|
156
|
+
flushSync(() => {
|
|
157
|
+
setIsRendered(false);
|
|
158
|
+
});
|
|
159
|
+
// Return focus after leaving the native host: a modal dialog makes the
|
|
160
|
+
// rest of the document inert, so focusing earlier silently fails.
|
|
161
|
+
triggerElementRef.current?.focus();
|
|
162
|
+
triggerElementRef.current = null;
|
|
163
|
+
// The synthetic popover close fires after the focus restore above,
|
|
164
|
+
// mirroring the task-queued native event: a consumer close listener
|
|
165
|
+
// that retargets focus (master-detail row switching) gets the last word.
|
|
166
|
+
if (owesCloseEvent) {
|
|
167
|
+
dispatchDialogClose(dialog);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}, [dialogRef, isModal, isOpen, setIsRendered]);
|
|
171
|
+
|
|
172
|
+
// Leave the active native host on unmount. When the drawer is mounted inside
|
|
173
|
+
// an <Activity> that flips to mode="hidden", React runs effect cleanups (with
|
|
174
|
+
// stale isOpen) instead of re-running the effect with isOpen=false. Leaving
|
|
175
|
+
// the host active would strand the top-layer surface and skip the next open.
|
|
176
|
+
// This is deliberately separate from the open/close effect: putting it in
|
|
177
|
+
// that cleanup would cut off every delayed slide-out.
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const dialog = dialogRef.current;
|
|
180
|
+
return () => {
|
|
181
|
+
if (dialog && isDrawerHostOpen(dialog, isModal)) {
|
|
182
|
+
if (hideDrawerHost(dialog, isModal)) {
|
|
183
|
+
// No focus restore on this path — the drawer is being torn down, so
|
|
184
|
+
// the owed synthetic close fires immediately (still exactly once).
|
|
185
|
+
dispatchDialogClose(dialog);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}, [dialogRef, isModal]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wait for the panel's transform transition, with a computed-duration
|
|
194
|
+
* backstop. Mirrors BottomSheetPanel.waitForTransition: the native event is
|
|
195
|
+
* authoritative, and the timeout only prevents a lost event from stranding an
|
|
196
|
+
* open dialog.
|
|
197
|
+
*/
|
|
198
|
+
function waitForDrawerExit(
|
|
199
|
+
element: HTMLDialogElement,
|
|
200
|
+
complete: () => void,
|
|
201
|
+
): () => void {
|
|
202
|
+
let done = false;
|
|
203
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
204
|
+
|
|
205
|
+
const finish = () => {
|
|
206
|
+
if (done) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
done = true;
|
|
210
|
+
if (timer != null) {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
}
|
|
213
|
+
element.removeEventListener('transitionend', handleTransitionEnd);
|
|
214
|
+
element.removeEventListener('transitioncancel', handleTransitionEnd);
|
|
215
|
+
complete();
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleTransitionEnd = (event: TransitionEvent) => {
|
|
219
|
+
if (event.target === element && event.propertyName === 'transform') {
|
|
220
|
+
finish();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
element.addEventListener('transitionend', handleTransitionEnd);
|
|
225
|
+
element.addEventListener('transitioncancel', handleTransitionEnd);
|
|
226
|
+
|
|
227
|
+
const transitionMs = readTransformTransitionMs(element);
|
|
228
|
+
timer = setTimeout(
|
|
229
|
+
finish,
|
|
230
|
+
(transitionMs ?? EXIT_FALLBACK_MS) + EXIT_BACKSTOP_BUFFER_MS,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return () => {
|
|
234
|
+
done = true;
|
|
235
|
+
if (timer != null) {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
238
|
+
element.removeEventListener('transitionend', handleTransitionEnd);
|
|
239
|
+
element.removeEventListener('transitioncancel', handleTransitionEnd);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Read the transform transition's duration + delay from computed CSS. */
|
|
244
|
+
function readTransformTransitionMs(element: HTMLElement): number | null {
|
|
245
|
+
const computed = window.getComputedStyle(element);
|
|
246
|
+
const properties = computed.transitionProperty
|
|
247
|
+
.split(',')
|
|
248
|
+
.map(value => value.trim());
|
|
249
|
+
const durations = parseTimes(computed.transitionDuration);
|
|
250
|
+
const delays = parseTimes(computed.transitionDelay);
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
properties.length === 0 ||
|
|
254
|
+
durations.length === 0 ||
|
|
255
|
+
delays.length === 0 ||
|
|
256
|
+
durations.includes(null) ||
|
|
257
|
+
delays.includes(null)
|
|
258
|
+
) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return properties.reduce((longest, property, index) => {
|
|
263
|
+
if (property !== 'transform' && property !== 'all') {
|
|
264
|
+
return longest;
|
|
265
|
+
}
|
|
266
|
+
const duration = durations[index % durations.length];
|
|
267
|
+
const delay = delays[index % delays.length];
|
|
268
|
+
return Math.max(longest, (duration ?? 0) + (delay ?? 0));
|
|
269
|
+
}, 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseTimes(value: string): (number | null)[] {
|
|
273
|
+
return value.split(',').map(part => {
|
|
274
|
+
const trimmed = part.trim();
|
|
275
|
+
const time = Number.parseFloat(trimmed);
|
|
276
|
+
if (!Number.isFinite(time)) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
if (trimmed.endsWith('ms')) {
|
|
280
|
+
return time;
|
|
281
|
+
}
|
|
282
|
+
return trimmed.endsWith('s') ? time * 1000 : null;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenu.ssr.test.tsx
|
|
5
|
+
* @input DropdownMenu server markup and React hydration
|
|
6
|
+
* @output Proves a bottom-sheet DropdownMenu hydrates without a recoverable
|
|
7
|
+
* error while its on-demand sheet has not loaded (spec:AST-013 FR9)
|
|
8
|
+
* @position SSR acceptance test for DropdownMenu
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {act} from 'react';
|
|
12
|
+
import {afterEach, describe, expect, it, vi} from 'vitest';
|
|
13
|
+
import {hydrateRoot} from 'react-dom/client';
|
|
14
|
+
import {renderToString} from 'react-dom/server';
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
document.body.replaceChildren();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function serverRenderThenHydrate(): Promise<unknown[]> {
|
|
22
|
+
// A fresh module graph is a fresh server process: the sheet has not loaded.
|
|
23
|
+
vi.resetModules();
|
|
24
|
+
const {DropdownMenu} = await import('./DropdownMenu');
|
|
25
|
+
const tree = (
|
|
26
|
+
<DropdownMenu
|
|
27
|
+
button={{label: 'Actions'}}
|
|
28
|
+
presentation="bottom-sheet"
|
|
29
|
+
items={[{label: 'Edit project'}, {label: 'Delete'}]}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
const container = document.createElement('div');
|
|
33
|
+
container.innerHTML = renderToString(tree);
|
|
34
|
+
document.body.appendChild(container);
|
|
35
|
+
|
|
36
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
+
const recoverableErrors: unknown[] = [];
|
|
38
|
+
let root!: ReturnType<typeof hydrateRoot>;
|
|
39
|
+
await act(async () => {
|
|
40
|
+
root = hydrateRoot(container, tree, {
|
|
41
|
+
onRecoverableError: error => recoverableErrors.push(error),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
await act(async () => {
|
|
45
|
+
root.unmount();
|
|
46
|
+
});
|
|
47
|
+
container.remove();
|
|
48
|
+
return recoverableErrors;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('DropdownMenu — SSR', () => {
|
|
52
|
+
it('hydrates a bottom-sheet menu cleanly on the first renders of a server process', async () => {
|
|
53
|
+
expect(await serverRenderThenHydrate()).toEqual([]);
|
|
54
|
+
expect(await serverRenderThenHydrate()).toEqual([]);
|
|
55
|
+
expect(await serverRenderThenHydrate()).toEqual([]);
|
|
56
|
+
}, 30000);
|
|
57
|
+
});
|