@astryxdesign/core 0.1.9 → 0.2.0-canary.068bda9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/dist/Avatar/Avatar.d.ts +42 -3
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +190 -59
- package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
- package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts +7 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +58 -6
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
- package/dist/Banner/Banner.js +2 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +11 -4
- package/dist/Breadcrumbs/index.d.ts +1 -0
- package/dist/Breadcrumbs/index.d.ts.map +1 -1
- package/dist/Breadcrumbs/index.js +2 -1
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +6 -4
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +50 -9
- package/dist/Calendar/dayCellUtils.d.ts +4 -4
- package/dist/Calendar/dayCellUtils.js +2 -2
- package/dist/Calendar/styles.d.ts +30 -31
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +30 -31
- package/dist/Card/Card.js +1 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +22 -12
- package/dist/Chat/ChatComposerInput.js +1 -1
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +3 -1
- package/dist/Chat/ChatLayout.js +4 -4
- package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.js +8 -8
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +10 -3
- package/dist/Chat/useTriggerMenu.js +2 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -6
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +19 -4
- package/dist/Citation/Citation.d.ts +22 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +20 -6
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +16 -10
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +3 -1
- package/dist/CommandPalette/CommandPalette.d.ts +1 -1
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +42 -3
- package/dist/CommandPalette/CommandPaletteItem.js +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +23 -5
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +3 -1
- package/dist/DateInput/DateInput.d.ts +10 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +33 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts +10 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +23 -20
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +23 -18
- package/dist/Divider/Divider.d.ts +2 -2
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +13 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +22 -5
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +5 -2
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +8 -11
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +3 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +5 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +435 -0
- package/dist/DropdownMenu/index.d.ts +10 -0
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +13 -0
- package/dist/DropdownMenu/menuItemHover.d.ts +24 -0
- package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemHover.js +34 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
- package/dist/DropdownMenu/menuItemRoles.js +10 -1
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
- package/dist/DropdownMenu/renderDropdownItems.js +21 -6
- package/dist/Field/Field.d.ts +3 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +4 -1
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +9 -5
- package/dist/Field/inputStyles.stylex.d.ts +1 -0
- package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
- package/dist/Field/inputStyles.stylex.js +1 -0
- package/dist/FieldStatus/FieldStatus.d.ts +10 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +51 -5
- package/dist/FileInput/FileInput.d.ts +10 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +93 -65
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +3 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -4
- package/dist/Icon/Icon.d.ts +15 -1
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +19 -3
- package/dist/Item/Item.d.ts +16 -3
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +31 -6
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +10 -2
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +34 -15
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +6 -3
- package/dist/List/ListItem.d.ts +11 -1
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +2 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +7 -0
- package/dist/MetadataList/MetadataList.d.ts +1 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +4 -2
- package/dist/MultiSelector/MultiSelector.d.ts +9 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +81 -43
- package/dist/NavMenu/NavHeadingMenuItem.js +1 -1
- package/dist/NumberInput/NumberInput.d.ts +10 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +22 -17
- package/dist/Outline/Outline.js +2 -2
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +11 -3
- package/dist/PowerSearch/PowerSearch.d.ts +9 -2
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +2 -0
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +6 -1
- package/dist/RadioList/RadioList.d.ts +2 -2
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -5
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +49 -19
- package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -3
- package/dist/Selector/Selector.d.ts +8 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +66 -17
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +5 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +13 -10
- package/dist/SideNav/SideNavItem.d.ts +1 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +12 -10
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +2 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +87 -34
- package/dist/Spinner/Spinner.d.ts +3 -2
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +11 -2
- package/dist/Switch/Switch.d.ts +8 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +171 -91
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts +2 -11
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +5 -9
- package/dist/TabList/TabListContext.d.ts +0 -5
- package/dist/TabList/TabListContext.d.ts.map +1 -1
- package/dist/TabList/TabListContext.js +0 -5
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +30 -4
- package/dist/Table/TableCell.js +3 -3
- package/dist/Table/TableHeaderCell.js +3 -3
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +37 -27
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +3 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +15 -7
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
- package/dist/Table/types.d.ts +32 -0
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Text/Text.d.ts +10 -1
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +25 -2
- package/dist/Text/index.d.ts +2 -2
- package/dist/Text/index.d.ts.map +1 -1
- package/dist/TextArea/TextArea.d.ts +10 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +119 -52
- package/dist/TextInput/TextInput.d.ts +10 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +20 -17
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +51 -45
- package/dist/TimeInput/TimeInput.d.ts +10 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +17 -17
- package/dist/Timestamp/Timestamp.d.ts +31 -3
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +48 -77
- package/dist/Timestamp/formatInstant.d.ts +41 -0
- package/dist/Timestamp/formatInstant.d.ts.map +1 -0
- package/dist/Timestamp/formatInstant.js +177 -0
- package/dist/Timestamp/index.d.ts +1 -0
- package/dist/Timestamp/index.d.ts.map +1 -1
- package/dist/Timestamp/tooltipEntries.d.ts +56 -0
- package/dist/Timestamp/tooltipEntries.d.ts.map +1 -0
- package/dist/Timestamp/tooltipEntries.js +163 -0
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +61 -9
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +3 -1
- package/dist/Token/Token.d.ts +34 -4
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +85 -45
- package/dist/Token/TokenLink.d.ts +62 -0
- package/dist/Token/TokenLink.d.ts.map +1 -0
- package/dist/Token/TokenLink.js +76 -0
- package/dist/Token/index.d.ts +1 -1
- package/dist/Token/index.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.d.ts +9 -2
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +4 -2
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +17 -1
- package/dist/TopNav/TopNavItem.d.ts +3 -1
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +30 -3
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +9 -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 +8 -6
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +32 -14
- package/dist/Typeahead/BaseTypeahead.d.ts +9 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +22 -11
- package/dist/Typeahead/Typeahead.d.ts +9 -2
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +11 -2
- package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/Typeahead/TypeaheadItem.js +20 -10
- package/dist/astryx.css +114 -35
- package/dist/astryx.umd.js +50 -50
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.pool.stylex.d.ts +281 -0
- package/dist/hooks/containerReveal.pool.stylex.d.ts.map +1 -0
- package/dist/hooks/containerReveal.pool.stylex.js +281 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/isRtlElement.d.ts +18 -0
- package/dist/hooks/isRtlElement.d.ts.map +1 -0
- package/dist/hooks/isRtlElement.js +27 -0
- package/dist/hooks/useAnnounce.d.ts +3 -0
- package/dist/hooks/useAnnounce.d.ts.map +1 -1
- package/dist/hooks/useAnnounce.js +39 -1
- package/dist/hooks/useContainerReveal.d.ts +83 -0
- package/dist/hooks/useContainerReveal.d.ts.map +1 -0
- package/dist/hooks/useContainerReveal.js +123 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +52 -11
- package/dist/hooks/useGridFocus.d.ts +6 -1
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +12 -9
- package/dist/hooks/useHotkeys.d.ts +9 -0
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +9 -0
- package/dist/hooks/useInputStatusIcon.d.ts +38 -0
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -0
- package/dist/hooks/useInputStatusIcon.js +169 -0
- package/dist/hooks/useKeyboardHint.d.ts +1 -1
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
- package/dist/hooks/useKeyboardHint.js +4 -2
- package/dist/hooks/useListFocus.d.ts +39 -1
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +54 -9
- package/dist/i18n/InternationalizationContext.d.ts +1 -0
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +3 -0
- package/dist/i18n/InternationalizationProvider.d.ts +8 -1
- package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
- package/dist/i18n/InternationalizationProvider.js +5 -1
- package/dist/i18n/getLocaleDirection.d.ts +18 -0
- package/dist/i18n/getLocaleDirection.d.ts.map +1 -0
- package/dist/i18n/getLocaleDirection.js +29 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +7 -1
- package/dist/i18n/useDirection.d.ts +2 -0
- package/dist/i18n/useDirection.d.ts.map +1 -0
- package/dist/i18n/useDirection.js +27 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +13 -0
- package/dist/theme/index.d.ts +3 -3
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/types.d.ts +39 -2
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/plainDate.d.ts +17 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +23 -4
- package/dist/utils/rtlStyles.d.ts +24 -0
- package/dist/utils/rtlStyles.d.ts.map +1 -0
- package/dist/utils/rtlStyles.js +27 -0
- package/locales/en.json +120 -0
- package/locales/pseudo.json +90 -0
- package/package.json +7 -2
- package/src/Avatar/Avatar.doc.mjs +50 -3
- package/src/Avatar/Avatar.test.tsx +218 -0
- package/src/Avatar/Avatar.tsx +285 -79
- package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
- package/src/Avatar/AvatarStatusDot.tsx +6 -4
- package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -0
- package/src/AvatarGroup/AvatarGroup.test.tsx +162 -2
- package/src/AvatarGroup/AvatarGroup.tsx +58 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +5 -2
- package/src/Banner/Banner.tsx +6 -6
- package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +52 -1
- package/src/Breadcrumbs/index.ts +4 -0
- package/src/Button/Button.doc.mjs +0 -4
- package/src/Button/Button.test.tsx +16 -0
- package/src/Button/Button.tsx +6 -4
- package/src/Calendar/Calendar.test.tsx +170 -20
- package/src/Calendar/Calendar.tsx +73 -16
- package/src/Calendar/dayCellUtils.test.ts +18 -18
- package/src/Calendar/dayCellUtils.ts +2 -2
- package/src/Calendar/styles.ts +33 -39
- package/src/Card/Card.tsx +1 -1
- package/src/Carousel/Carousel.tsx +25 -6
- package/src/Chat/ChatComposer.doc.mjs +2 -2
- package/src/Chat/ChatComposerInput.doc.mjs +1 -1
- package/src/Chat/ChatComposerInput.tsx +2 -2
- package/src/Chat/ChatDictationButton.tsx +7 -1
- package/src/Chat/ChatLayout.tsx +4 -4
- package/src/Chat/ChatMessageBubble.tsx +15 -10
- package/src/Chat/ChatSystemMessage.test.tsx +5 -0
- package/src/Chat/ChatToolCalls.tsx +9 -2
- package/src/Chat/useTriggerMenu.tsx +1 -1
- package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +23 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -6
- package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
- package/src/CheckboxList/CheckboxList.test.tsx +94 -3
- package/src/CheckboxList/CheckboxListItem.tsx +17 -10
- package/src/Citation/Citation.doc.mjs +4 -3
- package/src/Citation/Citation.test.tsx +97 -0
- package/src/Citation/Citation.tsx +43 -11
- package/src/CodeBlock/CodeBlock.test.tsx +17 -0
- package/src/CodeBlock/CodeBlock.tsx +38 -11
- package/src/Collapsible/Collapsible.doc.mjs +1 -0
- package/src/Collapsible/Collapsible.test.tsx +7 -0
- package/src/Collapsible/Collapsible.tsx +16 -6
- package/src/CommandPalette/CommandPalette.test.tsx +95 -1
- package/src/CommandPalette/CommandPalette.tsx +41 -2
- package/src/CommandPalette/CommandPaletteItem.tsx +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +59 -2
- package/src/ContextMenu/ContextMenu.tsx +22 -11
- package/src/ContextMenu/index.ts +5 -0
- package/src/DateInput/DateInput.doc.mjs +25 -0
- package/src/DateInput/DateInput.test.tsx +230 -0
- package/src/DateInput/DateInput.tsx +51 -29
- package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +140 -0
- package/src/DateRangeInput/DateRangeInput.tsx +42 -30
- package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
- package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
- package/src/DateTimeInput/DateTimeInput.tsx +44 -39
- package/src/Divider/Divider.test.tsx +32 -8
- package/src/Divider/Divider.tsx +14 -1
- package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
- package/src/DropdownMenu/DropdownMenu.tsx +31 -12
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
- package/src/DropdownMenu/DropdownMenuItem.tsx +9 -3
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +4 -12
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +9 -10
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +9 -3
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +20 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +663 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +561 -0
- package/src/DropdownMenu/index.ts +16 -0
- package/src/DropdownMenu/menuItemHover.ts +40 -0
- package/src/DropdownMenu/menuItemRoles.ts +9 -0
- package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
- package/src/Field/Field.doc.mjs +4 -0
- package/src/Field/Field.tsx +14 -9
- package/src/Field/FieldLabel.test.tsx +25 -0
- package/src/Field/FieldLabel.tsx +12 -3
- package/src/Field/inputStyles.stylex.ts +2 -0
- package/src/FieldStatus/FieldStatus.doc.mjs +1 -0
- package/src/FieldStatus/FieldStatus.test.tsx +133 -0
- package/src/FieldStatus/FieldStatus.tsx +61 -4
- package/src/FileInput/FileInput.doc.mjs +23 -2
- package/src/FileInput/FileInput.test.tsx +104 -13
- package/src/FileInput/FileInput.tsx +106 -79
- package/src/Heading/Heading.tsx +2 -1
- package/src/HoverCard/useHoverCard.tsx +15 -3
- package/src/Icon/Icon.doc.mjs +13 -0
- package/src/Icon/Icon.test.tsx +89 -0
- package/src/Icon/Icon.tsx +51 -3
- package/src/Item/Item.doc.mjs +5 -0
- package/src/Item/Item.test.tsx +88 -0
- package/src/Item/Item.tsx +57 -11
- package/src/Layer/layerAnimations.stylex.ts +23 -0
- package/src/Lightbox/Lightbox.test.tsx +33 -0
- package/src/Lightbox/Lightbox.tsx +30 -10
- package/src/List/List.test.tsx +17 -29
- package/src/List/List.tsx +6 -1
- package/src/List/ListItem.doc.mjs +12 -0
- package/src/List/ListItem.tsx +13 -0
- package/src/Markdown/Markdown.test.tsx +21 -0
- package/src/Markdown/Markdown.tsx +4 -0
- package/src/MetadataList/MetadataList.test.tsx +24 -0
- package/src/MetadataList/MetadataList.tsx +6 -2
- package/src/MultiSelector/MultiSelector.doc.mjs +20 -0
- package/src/MultiSelector/MultiSelector.test.tsx +402 -0
- package/src/MultiSelector/MultiSelector.tsx +126 -64
- package/src/NavMenu/NavHeadingMenuItem.tsx +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +21 -0
- package/src/NumberInput/NumberInput.test.tsx +71 -0
- package/src/NumberInput/NumberInput.tsx +35 -26
- package/src/Outline/Outline.test.tsx +12 -12
- package/src/Outline/Outline.tsx +2 -2
- package/src/Pagination/Pagination.tsx +11 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
- package/src/PowerSearch/PowerSearch.test.tsx +23 -0
- package/src/PowerSearch/PowerSearch.tsx +10 -1
- package/src/ProgressBar/ProgressBar.test.tsx +34 -0
- package/src/ProgressBar/ProgressBar.tsx +16 -1
- package/src/RadioList/RadioList.doc.mjs +6 -0
- package/src/RadioList/RadioList.test.tsx +26 -4
- package/src/RadioList/RadioList.tsx +2 -2
- package/src/RadioList/RadioListItem.tsx +12 -5
- package/src/Resizable/ResizeHandle.test.tsx +94 -0
- package/src/Resizable/ResizeHandle.tsx +70 -20
- package/src/SegmentedControl/SegmentedControl.test.tsx +28 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +22 -2
- package/src/Selector/Selector.doc.mjs +15 -0
- package/src/Selector/Selector.test.tsx +370 -0
- package/src/Selector/Selector.tsx +88 -20
- package/src/SideNav/SideNav.tsx +1 -8
- package/src/SideNav/SideNavCollapseButton.tsx +9 -7
- package/src/SideNav/SideNavItem.tsx +5 -3
- package/src/Skeleton/Skeleton.test.tsx +16 -0
- package/src/Skeleton/Skeleton.tsx +13 -1
- package/src/Slider/Slider.doc.mjs +6 -0
- package/src/Slider/Slider.test.tsx +240 -12
- package/src/Slider/Slider.tsx +96 -23
- package/src/Spinner/Spinner.test.tsx +10 -8
- package/src/Spinner/Spinner.tsx +15 -5
- package/src/Switch/Switch.doc.mjs +142 -42
- package/src/Switch/Switch.test.tsx +111 -0
- package/src/Switch/Switch.tsx +149 -32
- package/src/TabList/Tab.tsx +2 -2
- package/src/TabList/TabList.doc.mjs +0 -6
- package/src/TabList/TabList.test.tsx +3 -15
- package/src/TabList/TabList.tsx +6 -17
- package/src/TabList/TabListContext.ts +0 -6
- package/src/TabList/TabMenu.tsx +2 -2
- package/src/Table/BaseTable.tsx +34 -2
- package/src/Table/Table.doc.mjs +11 -0
- package/src/Table/Table.test.tsx +85 -0
- package/src/Table/TableCell.tsx +3 -3
- package/src/Table/TableHeaderCell.tsx +3 -3
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +19 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +27 -18
- package/src/Table/plugins/selection/useTableSelection.test.tsx +61 -3
- package/src/Table/plugins/selection/useTableSelection.tsx +3 -0
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +38 -9
- package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
- package/src/Table/types.ts +33 -0
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/Text/Heading.doc.mjs +3 -3
- package/src/Text/Text.doc.mjs +1 -1
- package/src/Text/Text.test.tsx +42 -0
- package/src/Text/Text.tsx +25 -1
- package/src/Text/index.ts +2 -6
- package/src/TextArea/TextArea.doc.mjs +23 -2
- package/src/TextArea/TextArea.test.tsx +132 -4
- package/src/TextArea/TextArea.tsx +161 -67
- package/src/TextInput/TextInput.doc.mjs +21 -0
- package/src/TextInput/TextInput.test.tsx +150 -0
- package/src/TextInput/TextInput.tsx +28 -25
- package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
- package/src/Thumbnail/Thumbnail.test.tsx +24 -1
- package/src/Thumbnail/Thumbnail.tsx +28 -35
- package/src/TimeInput/TimeInput.doc.mjs +21 -0
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +24 -25
- package/src/Timestamp/Timestamp.doc.mjs +32 -6
- package/src/Timestamp/Timestamp.test.tsx +371 -0
- package/src/Timestamp/Timestamp.tsx +96 -93
- package/src/Timestamp/formatInstant.ts +218 -0
- package/src/Timestamp/index.ts +4 -0
- package/src/Timestamp/tooltipEntries.test.ts +552 -0
- package/src/Timestamp/tooltipEntries.ts +210 -0
- package/src/Toast/ToastViewport.test.tsx +218 -8
- package/src/Toast/ToastViewport.tsx +71 -12
- package/src/ToggleButton/ToggleButton.test.tsx +24 -0
- package/src/ToggleButton/ToggleButton.tsx +19 -1
- package/src/Token/Token.test.tsx +166 -15
- package/src/Token/Token.tsx +104 -47
- package/src/Token/TokenLink.tsx +107 -0
- package/src/Token/index.ts +1 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +23 -1
- package/src/Tokenizer/Tokenizer.test.tsx +23 -0
- package/src/Tokenizer/Tokenizer.tsx +14 -2
- package/src/Tooltip/Tooltip.test.tsx +46 -0
- package/src/Tooltip/useTooltip.tsx +20 -0
- package/src/TopNav/TopNav.test.tsx +93 -2
- package/src/TopNav/TopNavItem.doc.mjs +3 -3
- package/src/TopNav/TopNavItem.tsx +35 -6
- package/src/TopNav/TopNavMegaMenu.tsx +26 -2
- package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +1 -1
- package/src/TreeList/TreeList.doc.mjs +6 -0
- package/src/TreeList/TreeList.test.tsx +50 -10
- package/src/TreeList/TreeList.tsx +5 -0
- package/src/TreeList/TreeListBranches.tsx +8 -6
- package/src/TreeList/TreeListItem.tsx +33 -11
- package/src/Typeahead/BaseTypeahead.tsx +42 -15
- package/src/Typeahead/Typeahead.doc.mjs +13 -0
- package/src/Typeahead/Typeahead.test.tsx +112 -0
- package/src/Typeahead/Typeahead.tsx +17 -1
- package/src/Typeahead/TypeaheadItem.tsx +2 -1
- package/src/__tests__/babelPluginAddExtensions.test.ts +190 -0
- package/src/__tests__/forcedColors.ts +60 -0
- package/src/__tests__/inputWidthContract.test.tsx +62 -0
- package/src/doc-mjs.d.ts +19 -0
- package/src/hooks/containerReveal.pool.stylex.ts +464 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/isRtlElement.ts +27 -0
- package/src/hooks/useAnnounce.test.tsx +75 -2
- package/src/hooks/useAnnounce.ts +47 -1
- package/src/hooks/useContainerReveal.test.tsx +116 -0
- package/src/hooks/useContainerReveal.ts +180 -0
- package/src/hooks/useFocusTrap.test.tsx +104 -0
- package/src/hooks/useFocusTrap.ts +61 -12
- package/src/hooks/useGridFocus.doc.mjs +3 -3
- package/src/hooks/useGridFocus.test.tsx +41 -0
- package/src/hooks/useGridFocus.ts +21 -10
- package/src/hooks/useHotkeys.ts +9 -0
- package/src/hooks/useInputStatusIcon.test.tsx +221 -0
- package/src/hooks/useInputStatusIcon.tsx +235 -0
- package/src/hooks/useKeyboardHint.test.tsx +22 -0
- package/src/hooks/useKeyboardHint.tsx +6 -2
- package/src/hooks/useListFocus.doc.mjs +25 -3
- package/src/hooks/useListFocus.test.tsx +153 -1
- package/src/hooks/useListFocus.ts +102 -9
- package/src/i18n/InternationalizationContext.ts +4 -0
- package/src/i18n/InternationalizationProvider.tsx +12 -2
- package/src/i18n/__tests__/getLocaleDirection.test.ts +40 -0
- package/src/i18n/__tests__/useDirection.test.tsx +66 -0
- package/src/i18n/getLocaleDirection.ts +29 -0
- package/src/i18n/index.ts +6 -0
- package/src/i18n/useDirection.ts +29 -0
- package/src/reset.css +0 -26
- package/src/reset.test.ts +60 -0
- package/src/theme/derivedVarRegistry.test.ts +1 -0
- package/src/theme/derivedVarRegistry.ts +6 -0
- package/src/theme/generateThemeRules.test.ts +42 -0
- package/src/theme/index.ts +4 -8
- package/src/theme/types.ts +43 -13
- package/src/utils/index.ts +2 -0
- package/src/utils/plainDate.test.ts +24 -6
- package/src/utils/plainDate.ts +18 -4
- package/src/utils/rtlStyles.ts +27 -0
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +0 -9
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +0 -1
- package/dist/Thumbnail/thumbnail.markers.stylex.js +0 -14
- package/src/Thumbnail/thumbnail.markers.stylex.ts +0 -12
package/src/hooks/useAnnounce.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* @input Uses React useCallback; DOM APIs for a singleton live-region pair
|
|
8
8
|
* @output Exports useAnnounce hook and AnnouncePoliteness type
|
|
9
9
|
* @position Core a11y hook; provides imperative screen-reader announcements
|
|
10
|
-
* via persistently-mounted polite/assertive live regions
|
|
10
|
+
* via persistently-mounted polite/assertive live regions; regions are
|
|
11
|
+
* auto-cleared shortly after announcing so stale status text does not
|
|
12
|
+
* linger in the accessibility tree
|
|
11
13
|
*
|
|
12
14
|
* SYNC: When modified, update:
|
|
13
15
|
* - /packages/core/src/hooks/index.ts
|
|
@@ -47,6 +49,40 @@ interface LiveRegions {
|
|
|
47
49
|
assertive: HTMLElement;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* How long an announcement stays in its region before being auto-cleared.
|
|
54
|
+
* Long enough for screen readers to pick the message up and finish reading
|
|
55
|
+
* it; clearing afterwards keeps stale status text out of the accessibility
|
|
56
|
+
* tree for users who browse the DOM later. Each announce resets the timer.
|
|
57
|
+
*/
|
|
58
|
+
const CLEAR_DELAY_MS = 2000;
|
|
59
|
+
|
|
60
|
+
const clearTimers: {
|
|
61
|
+
[K in AnnouncePoliteness]: ReturnType<typeof setTimeout> | null;
|
|
62
|
+
} = {
|
|
63
|
+
polite: null,
|
|
64
|
+
assertive: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function cancelScheduledClear(politeness: AnnouncePoliteness): void {
|
|
68
|
+
const timer = clearTimers[politeness];
|
|
69
|
+
if (timer != null) {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
clearTimers[politeness] = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scheduleClear(
|
|
76
|
+
politeness: AnnouncePoliteness,
|
|
77
|
+
target: HTMLElement,
|
|
78
|
+
): void {
|
|
79
|
+
cancelScheduledClear(politeness);
|
|
80
|
+
clearTimers[politeness] = setTimeout(() => {
|
|
81
|
+
clearTimers[politeness] = null;
|
|
82
|
+
target.textContent = '';
|
|
83
|
+
}, CLEAR_DELAY_MS);
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
// Singleton: the live regions are created ONCE and stay mounted for the
|
|
51
87
|
// lifetime of the document. This is the crux of reliable announcements — many
|
|
52
88
|
// screen readers will not announce content injected into a live region that is
|
|
@@ -103,6 +139,10 @@ function announceMessage(
|
|
|
103
139
|
requestAnimationFrame(() => {
|
|
104
140
|
target.textContent = message;
|
|
105
141
|
});
|
|
142
|
+
// Auto-clear so the last message does not linger in the accessibility tree
|
|
143
|
+
// indefinitely. Scheduled per announce, so a newer announcement always
|
|
144
|
+
// resets the countdown (the delay comfortably outlasts the rAF re-set).
|
|
145
|
+
scheduleClear(politeness, target);
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
/**
|
|
@@ -111,6 +151,7 @@ function announceMessage(
|
|
|
111
151
|
* so stale status text does not linger in the accessibility tree.
|
|
112
152
|
*/
|
|
113
153
|
function clearRegion(politeness: AnnouncePoliteness): void {
|
|
154
|
+
cancelScheduledClear(politeness);
|
|
114
155
|
if (!regions) {
|
|
115
156
|
return;
|
|
116
157
|
}
|
|
@@ -126,6 +167,9 @@ function clearRegion(politeness: AnnouncePoliteness): void {
|
|
|
126
167
|
* The polite and assertive regions are created once on first use and kept
|
|
127
168
|
* mounted, so announcements are reliable even for messages that appear
|
|
128
169
|
* immediately (unlike a live region rendered together with its content).
|
|
170
|
+
* Each message is automatically cleared a couple of seconds after being
|
|
171
|
+
* announced, so users browsing the DOM later do not encounter stale status
|
|
172
|
+
* text; announcing again before the clear simply resets the countdown.
|
|
129
173
|
*
|
|
130
174
|
* @example
|
|
131
175
|
* ```
|
|
@@ -160,6 +204,8 @@ export function useAnnounce(): AnnounceFn {
|
|
|
160
204
|
* @internal
|
|
161
205
|
*/
|
|
162
206
|
export function __resetLiveRegionsForTest(): void {
|
|
207
|
+
cancelScheduledClear('polite');
|
|
208
|
+
cancelScheduledClear('assertive');
|
|
163
209
|
if (regions) {
|
|
164
210
|
regions.polite.remove();
|
|
165
211
|
regions.assertive.remove();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useContainerReveal.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, useContainerReveal
|
|
6
|
+
* @output Unit tests for the marker-pool free-list, per-instance scoping, the
|
|
7
|
+
* inert (disabled) path, and the content-option → style-block mapping.
|
|
8
|
+
* @position Testing; validates useContainerReveal.ts and its pool.
|
|
9
|
+
*
|
|
10
|
+
* SYNC: When useContainerReveal.ts changes, update these tests.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {describe, it, expect} from 'vitest';
|
|
14
|
+
import {renderHook, render, act} from '@testing-library/react';
|
|
15
|
+
import {useContainerReveal} from './useContainerReveal';
|
|
16
|
+
import {POOL_SIZE} from './containerReveal.pool.stylex';
|
|
17
|
+
|
|
18
|
+
describe('useContainerReveal', () => {
|
|
19
|
+
it('returns spreadable getter props for container and content', () => {
|
|
20
|
+
const {result} = renderHook(() => useContainerReveal());
|
|
21
|
+
const container = result.current.getContainerProps();
|
|
22
|
+
const content = result.current.getContentRevealProps();
|
|
23
|
+
expect(container).toHaveProperty('className');
|
|
24
|
+
expect(content).toHaveProperty('className');
|
|
25
|
+
expect(typeof container.className).toBe('string');
|
|
26
|
+
expect(typeof content.className).toBe('string');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is inert when disabled: no marker class, empty content props', () => {
|
|
30
|
+
const {result} = renderHook(() =>
|
|
31
|
+
useContainerReveal({isEnabled: false}),
|
|
32
|
+
);
|
|
33
|
+
expect(result.current.getContainerProps()).toEqual({});
|
|
34
|
+
expect(result.current.getContentRevealProps()).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('gives two concurrently mounted containers DISTINCT marker classes', () => {
|
|
38
|
+
// The core leak-safety property: nested/concurrent containers must never
|
|
39
|
+
// share a marker, or an ancestor container's :hover would reveal a
|
|
40
|
+
// descendant container's content.
|
|
41
|
+
const a = renderHook(() => useContainerReveal());
|
|
42
|
+
const b = renderHook(() => useContainerReveal());
|
|
43
|
+
const classA = a.result.current.getContainerProps().className;
|
|
44
|
+
const classB = b.result.current.getContainerProps().className;
|
|
45
|
+
expect(classA).toBeTruthy();
|
|
46
|
+
expect(classB).toBeTruthy();
|
|
47
|
+
expect(classA).not.toBe(classB);
|
|
48
|
+
a.unmount();
|
|
49
|
+
b.unmount();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('recycles a slot after unmount (free-list release)', () => {
|
|
53
|
+
const first = renderHook(() => useContainerReveal());
|
|
54
|
+
const firstClass = first.result.current.getContainerProps().className;
|
|
55
|
+
first.unmount();
|
|
56
|
+
// A fresh mount should be able to reclaim the just-released slot.
|
|
57
|
+
const second = renderHook(() => useContainerReveal());
|
|
58
|
+
const secondClass = second.result.current.getContainerProps().className;
|
|
59
|
+
expect(secondClass).toBe(firstClass);
|
|
60
|
+
second.unmount();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reveal and conceal map to different style blocks', () => {
|
|
64
|
+
const {result} = renderHook(() => useContainerReveal());
|
|
65
|
+
const reveal = result.current.getContentRevealProps().className;
|
|
66
|
+
const conceal = result.current.getContentRevealProps({
|
|
67
|
+
isRevealInverted: true,
|
|
68
|
+
}).className;
|
|
69
|
+
expect(reveal).toBeTruthy();
|
|
70
|
+
expect(conceal).toBeTruthy();
|
|
71
|
+
expect(reveal).not.toBe(conceal);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('layout-preserved reveal differs from clipped reveal', () => {
|
|
75
|
+
const {result} = renderHook(() => useContainerReveal());
|
|
76
|
+
const clipped = result.current.getContentRevealProps().className;
|
|
77
|
+
const preserved = result.current.getContentRevealProps({
|
|
78
|
+
isLayoutPreserved: true,
|
|
79
|
+
}).className;
|
|
80
|
+
expect(clipped).not.toBe(preserved);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('assigns distinct markers across a full pool of concurrent containers', () => {
|
|
84
|
+
function Probe({onClass}: {onClass: (c: string) => void}) {
|
|
85
|
+
const {getContainerProps} = useContainerReveal();
|
|
86
|
+
onClass(getContainerProps().className ?? '');
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const classes: string[] = [];
|
|
90
|
+
const {unmount} = render(
|
|
91
|
+
<>
|
|
92
|
+
{Array.from({length: POOL_SIZE}, (_, i) => (
|
|
93
|
+
<Probe key={i} onClass={(c) => classes.push(c)} />
|
|
94
|
+
))}
|
|
95
|
+
</>,
|
|
96
|
+
);
|
|
97
|
+
const unique = new Set(classes.filter(Boolean));
|
|
98
|
+
expect(unique.size).toBe(POOL_SIZE);
|
|
99
|
+
unmount();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('survives repeated mount/unmount without leaking the pool', () => {
|
|
103
|
+
// Mount and unmount a full pool several times; each cycle must still hand
|
|
104
|
+
// out POOL_SIZE distinct markers, proving slots are released.
|
|
105
|
+
for (let cycle = 0; cycle < 3; cycle++) {
|
|
106
|
+
const hooks = Array.from({length: POOL_SIZE}, () =>
|
|
107
|
+
renderHook(() => useContainerReveal()),
|
|
108
|
+
);
|
|
109
|
+
const classes = hooks.map(
|
|
110
|
+
(h) => h.result.current.getContainerProps().className,
|
|
111
|
+
);
|
|
112
|
+
expect(new Set(classes).size).toBe(POOL_SIZE);
|
|
113
|
+
act(() => hooks.forEach((h) => h.unmount()));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useContainerReveal.ts
|
|
7
|
+
* @input Uses React, the generated marker pool, useDevWarning
|
|
8
|
+
* @output Exports useContainerReveal — a headless hover/focus reveal primitive
|
|
9
|
+
* @position Core hook. Consumed by any component that reveals (or conceals)
|
|
10
|
+
* content when its container is hovered or focused — e.g. Thumbnail's remove
|
|
11
|
+
* button, TreeList row actions.
|
|
12
|
+
*
|
|
13
|
+
* Gives a container a scoped hover/focus-within trigger that reveals or
|
|
14
|
+
* conceals content inside it, entirely in CSS (no hover state in JS, no
|
|
15
|
+
* re-render on hover). The caller authors NO StyleX: the hook hands out a
|
|
16
|
+
* marker + matching reveal styles from a pre-compiled pool.
|
|
17
|
+
*
|
|
18
|
+
* ACCESSIBILITY (WCAG 2.2 by construction):
|
|
19
|
+
* - Revealed content is visually hidden at rest via the clip recipe, so it
|
|
20
|
+
* stays in the accessibility tree and tab order — never display:none.
|
|
21
|
+
* - Keyboard: revealed on :focus-within, so tabbing in shows it.
|
|
22
|
+
* - Touch: always visible on coarse pointers; never gated behind hover.
|
|
23
|
+
* - Concealed (inverted) content is a mouse-only visual swap: it ignores
|
|
24
|
+
* :focus-within (a keyboard user must never watch content vanish) and stays
|
|
25
|
+
* visible on touch and in the a11y tree.
|
|
26
|
+
* - Motion: honors prefers-reduced-motion.
|
|
27
|
+
*
|
|
28
|
+
* SYNC: When modified, update:
|
|
29
|
+
* - /packages/core/src/hooks/index.ts (export)
|
|
30
|
+
* - containerReveal.pool.stylex.ts (if the slot shape changes)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {useState, useEffect, type CSSProperties} from 'react';
|
|
34
|
+
import * as stylex from '@stylexjs/stylex';
|
|
35
|
+
import {POOL, POOL_SIZE, type RevealSlot} from './containerReveal.pool.stylex';
|
|
36
|
+
import {useDevWarning} from './useDevWarning';
|
|
37
|
+
|
|
38
|
+
// Module-level free-list. Each mounted, enabled useContainerReveal claims a
|
|
39
|
+
// distinct pool slot so that any two CONCURRENTLY mounted containers — in
|
|
40
|
+
// particular a nested container inside another's revealed content — get
|
|
41
|
+
// different markers and cannot leak hover/focus into one another. Slots are
|
|
42
|
+
// returned on unmount. Sibling containers that would only ever collide when
|
|
43
|
+
// the pool is exhausted are harmless (a sibling is never an ancestor), so the
|
|
44
|
+
// pool only needs to cover the number of concurrently mounted containers, not
|
|
45
|
+
// instances over time.
|
|
46
|
+
const claimed: boolean[] = new Array(POOL_SIZE).fill(false);
|
|
47
|
+
|
|
48
|
+
interface Claim {
|
|
49
|
+
/** Pool index to use. */
|
|
50
|
+
index: number;
|
|
51
|
+
/** True when the pool was full and this claim fell back to a shared slot. */
|
|
52
|
+
isExhausted: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function claimSlot(): Claim {
|
|
56
|
+
for (let i = 0; i < POOL_SIZE; i++) {
|
|
57
|
+
if (!claimed[i]) {
|
|
58
|
+
claimed[i] = true;
|
|
59
|
+
return {index: i, isExhausted: false};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Exhausted: fall back to slot 0. Safe for siblings (a sibling is never an
|
|
63
|
+
// ancestor); only nesting deeper than POOL_SIZE could reintroduce a leak.
|
|
64
|
+
// Surfaced via useDevWarning so the pool can be grown.
|
|
65
|
+
return {index: 0, isExhausted: true};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function releaseSlot(index: number, isExhausted: boolean): void {
|
|
69
|
+
// A fallback claim never owned the slot exclusively; leave it as-is.
|
|
70
|
+
if (!isExhausted) {
|
|
71
|
+
claimed[index] = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UseContainerRevealOptions {
|
|
76
|
+
/**
|
|
77
|
+
* When false the hook is inert: no marker is applied and content getters
|
|
78
|
+
* return no styles, so content is always shown. Lets a component gate on its
|
|
79
|
+
* own prop (e.g. `revealOn === 'hover'`).
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
isEnabled?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ContentRevealOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Conceal-on-hover instead of reveal-on-hover: content is visible at rest
|
|
88
|
+
* and fades out while the container is hovered. Mouse-only and visual —
|
|
89
|
+
* stays in the a11y tree, ignores focus-within, stays visible on touch.
|
|
90
|
+
* @default false
|
|
91
|
+
*/
|
|
92
|
+
isRevealInverted?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Reserve the content's layout box while hidden (opacity-only) instead of
|
|
95
|
+
* collapsing it, to avoid layout shift when it appears.
|
|
96
|
+
* @default false
|
|
97
|
+
*/
|
|
98
|
+
isLayoutPreserved?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface UseContainerRevealReturn {
|
|
102
|
+
/** Spread onto the container whose hover/focus-within drives the reveal. */
|
|
103
|
+
getContainerProps: () => {className?: string; style?: CSSProperties};
|
|
104
|
+
/** Spread onto each revealed / concealed child. */
|
|
105
|
+
getContentRevealProps: (
|
|
106
|
+
options?: ContentRevealOptions,
|
|
107
|
+
) => {className?: string; style?: CSSProperties};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const EMPTY = Object.freeze({});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scoped, CSS-only hover/focus reveal for content inside a container.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* const {getContainerProps, getContentRevealProps} = useContainerReveal({
|
|
118
|
+
* isEnabled: revealOn === 'hover',
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* <div {...mergeProps(getContainerProps(), stylex.props(styles.row))}>
|
|
122
|
+
* {label}
|
|
123
|
+
* <span {...mergeProps(getContentRevealProps(), stylex.props(styles.actions))}>
|
|
124
|
+
* {actions}
|
|
125
|
+
* </span>
|
|
126
|
+
* </div>
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export function useContainerReveal(
|
|
130
|
+
options: UseContainerRevealOptions = {},
|
|
131
|
+
): UseContainerRevealReturn {
|
|
132
|
+
const {isEnabled = true} = options;
|
|
133
|
+
|
|
134
|
+
// Claim a slot for this container's lifetime. Assigned in the initializer
|
|
135
|
+
// (not the render body) and released on unmount so the free-list stays
|
|
136
|
+
// effect-scoped and StrictMode-safe.
|
|
137
|
+
const [claim] = useState<Claim | null>(() =>
|
|
138
|
+
isEnabled ? claimSlot() : null,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (claim == null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
return () => releaseSlot(claim.index, claim.isExhausted);
|
|
146
|
+
}, [claim]);
|
|
147
|
+
|
|
148
|
+
useDevWarning(
|
|
149
|
+
'useContainerReveal',
|
|
150
|
+
`More than ${POOL_SIZE} reveal containers are mounted at once; nested ` +
|
|
151
|
+
`containers beyond the pool may share a marker. Add markers to ` +
|
|
152
|
+
`containerReveal.pool.stylex.ts and raise POOL_SIZE.`,
|
|
153
|
+
claim?.isExhausted ?? false,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (claim == null) {
|
|
157
|
+
return {
|
|
158
|
+
getContainerProps: () => EMPTY,
|
|
159
|
+
getContentRevealProps: () => EMPTY,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const slot: RevealSlot = POOL[claim.index];
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
getContainerProps: () => stylex.props(slot.marker),
|
|
167
|
+
getContentRevealProps: (contentOptions: ContentRevealOptions = {}) => {
|
|
168
|
+
const {isRevealInverted = false, isLayoutPreserved = false} =
|
|
169
|
+
contentOptions;
|
|
170
|
+
const style = isRevealInverted
|
|
171
|
+
? isLayoutPreserved
|
|
172
|
+
? slot.concealLayoutPreserved
|
|
173
|
+
: slot.conceal
|
|
174
|
+
: isLayoutPreserved
|
|
175
|
+
? slot.revealLayoutPreserved
|
|
176
|
+
: slot.reveal;
|
|
177
|
+
return stylex.props(style);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -50,6 +50,39 @@ function EscapeTrap({
|
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function NestedInnerTrap({onEscape}: {onEscape: () => void}) {
|
|
54
|
+
const {containerRef} = useFocusTrap<HTMLDivElement>({
|
|
55
|
+
isActive: true,
|
|
56
|
+
onEscape,
|
|
57
|
+
});
|
|
58
|
+
return (
|
|
59
|
+
<div ref={containerRef} data-testid="nested-inner">
|
|
60
|
+
<button type="button">inner-btn</button>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function NestedTraps({
|
|
66
|
+
onOuterEscape,
|
|
67
|
+
onInnerEscape,
|
|
68
|
+
showInner = true,
|
|
69
|
+
}: {
|
|
70
|
+
onOuterEscape: () => void;
|
|
71
|
+
onInnerEscape: () => void;
|
|
72
|
+
showInner?: boolean;
|
|
73
|
+
}) {
|
|
74
|
+
const {containerRef} = useFocusTrap<HTMLDivElement>({
|
|
75
|
+
isActive: true,
|
|
76
|
+
onEscape: onOuterEscape,
|
|
77
|
+
});
|
|
78
|
+
return (
|
|
79
|
+
<div ref={containerRef} data-testid="nested-outer">
|
|
80
|
+
<button type="button">outer-btn</button>
|
|
81
|
+
{showInner && <NestedInnerTrap onEscape={onInnerEscape} />}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
function RestoreTrap({isActive}: {isActive: boolean}) {
|
|
54
87
|
const {containerRef} = useFocusTrap<HTMLDivElement>({isActive});
|
|
55
88
|
return (
|
|
@@ -118,6 +151,47 @@ describe('useFocusTrap tabbable model (infra-8)', () => {
|
|
|
118
151
|
// Focus skips the inert button and lands on the real one.
|
|
119
152
|
expect(screen.getByTestId('real-btn')).toHaveFocus();
|
|
120
153
|
});
|
|
154
|
+
|
|
155
|
+
it('ignores an aria-hidden subtree when finding focusables', () => {
|
|
156
|
+
render(
|
|
157
|
+
<Trap>
|
|
158
|
+
<div aria-hidden="true">
|
|
159
|
+
<button type="button" data-testid="aria-hidden-btn">
|
|
160
|
+
Hidden from AT
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
<button type="button" data-testid="real-btn">
|
|
164
|
+
Real
|
|
165
|
+
</button>
|
|
166
|
+
</Trap>,
|
|
167
|
+
);
|
|
168
|
+
fireEvent.click(screen.getByTestId('focus-first'));
|
|
169
|
+
// An element AT cannot perceive must not be a trap tab stop (WCAG 4.1.2).
|
|
170
|
+
expect(screen.getByTestId('real-btn')).toHaveFocus();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('excludes an aria-hidden focusable from the Tab wrap boundary', () => {
|
|
174
|
+
render(
|
|
175
|
+
<Trap>
|
|
176
|
+
<button type="button" data-testid="first">
|
|
177
|
+
First
|
|
178
|
+
</button>
|
|
179
|
+
<button type="button" data-testid="visible-last">
|
|
180
|
+
Visible last
|
|
181
|
+
</button>
|
|
182
|
+
<div aria-hidden="true">
|
|
183
|
+
<button type="button" data-testid="aria-hidden-btn">
|
|
184
|
+
Hidden from AT
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</Trap>,
|
|
188
|
+
);
|
|
189
|
+
// The last VISIBLE-to-AT element is the wrap boundary: Tab from it wraps
|
|
190
|
+
// to the first element instead of landing on the aria-hidden button.
|
|
191
|
+
screen.getByTestId('visible-last').focus();
|
|
192
|
+
fireEvent.keyDown(document, {key: 'Tab'});
|
|
193
|
+
expect(screen.getByTestId('first')).toHaveFocus();
|
|
194
|
+
});
|
|
121
195
|
});
|
|
122
196
|
|
|
123
197
|
describe('FOCUSABLE_SELECTOR href matching', () => {
|
|
@@ -198,6 +272,36 @@ describe('useFocusTrap Escape coordination', () => {
|
|
|
198
272
|
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
199
273
|
});
|
|
200
274
|
|
|
275
|
+
it('resolves Escape to the DOM-nested inner trap when both mount in one commit', () => {
|
|
276
|
+
const outer = vi.fn();
|
|
277
|
+
const inner = vi.fn();
|
|
278
|
+
// Outer and inner mount in the SAME React commit. React runs child
|
|
279
|
+
// effects before parent effects, so the inner trap is PUSHED first —
|
|
280
|
+
// DOM containment, not push order, must decide who answers Escape.
|
|
281
|
+
render(<NestedTraps onOuterEscape={outer} onInnerEscape={inner} />);
|
|
282
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
283
|
+
expect(inner).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(outer).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('falls back to the outer trap once the nested inner trap unmounts', () => {
|
|
288
|
+
const outer = vi.fn();
|
|
289
|
+
const inner = vi.fn();
|
|
290
|
+
const {rerender} = render(
|
|
291
|
+
<NestedTraps onOuterEscape={outer} onInnerEscape={inner} />,
|
|
292
|
+
);
|
|
293
|
+
rerender(
|
|
294
|
+
<NestedTraps
|
|
295
|
+
onOuterEscape={outer}
|
|
296
|
+
onInnerEscape={inner}
|
|
297
|
+
showInner={false}
|
|
298
|
+
/>,
|
|
299
|
+
);
|
|
300
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
301
|
+
expect(outer).toHaveBeenCalledTimes(1);
|
|
302
|
+
expect(inner).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
201
305
|
it('does not respond after the trap is deactivated', () => {
|
|
202
306
|
const onEscape = vi.fn();
|
|
203
307
|
const {rerender} = render(
|
|
@@ -26,24 +26,62 @@ import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
|
26
26
|
* Every active `useFocusTrap` used to attach its own document-level `keydown`
|
|
27
27
|
* listener with no coordination, so a single Escape press closed *every* open
|
|
28
28
|
* layer at once (e.g. a popover nested inside a Dialog closed both). Tracking
|
|
29
|
-
* traps in a shared stack lets only the most
|
|
30
|
-
*
|
|
29
|
+
* traps in a shared stack lets only the top-most trap respond to Escape.
|
|
30
|
+
*
|
|
31
|
+
* "Top-most" is resolved by DOM containment first, push order second. Push
|
|
32
|
+
* order alone is not reliable: React runs child effects before parent
|
|
33
|
+
* effects, so when an outer and an inner (DOM-nested) trap mount in the SAME
|
|
34
|
+
* commit, the inner trap pushes first and the outer trap would wrongly win a
|
|
35
|
+
* pure last-pushed comparison.
|
|
31
36
|
*/
|
|
32
|
-
|
|
37
|
+
interface EscapeStackEntry {
|
|
38
|
+
handler: () => void;
|
|
39
|
+
getContainer: () => HTMLElement | null;
|
|
40
|
+
}
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
const escapeStack: EscapeStackEntry[] = [];
|
|
43
|
+
|
|
44
|
+
function pushEscapeHandler(entry: EscapeStackEntry): void {
|
|
45
|
+
escapeStack.push(entry);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function removeEscapeHandler(handler: () => void): void {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
for (let i = escapeStack.length - 1; i >= 0; i--) {
|
|
50
|
+
if (escapeStack[i].handler === handler) {
|
|
51
|
+
escapeStack.splice(i, 1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the top-most trap: walk the stack in push order, keeping the
|
|
59
|
+
* deepest container by DOM containment. When a later entry's container
|
|
60
|
+
* contains the current candidate's container, the candidate is nested inside
|
|
61
|
+
* it and stays on top; otherwise the later push wins (containment for nested
|
|
62
|
+
* traps, push order as the tiebreaker for unrelated ones).
|
|
63
|
+
*/
|
|
45
64
|
function isTopEscapeHandler(handler: () => void): boolean {
|
|
46
|
-
|
|
65
|
+
if (escapeStack.length === 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
let top = escapeStack[0];
|
|
69
|
+
for (let i = 1; i < escapeStack.length; i++) {
|
|
70
|
+
const entry = escapeStack[i];
|
|
71
|
+
const topContainer = top.getContainer();
|
|
72
|
+
const entryContainer = entry.getContainer();
|
|
73
|
+
if (
|
|
74
|
+
topContainer != null &&
|
|
75
|
+
entryContainer != null &&
|
|
76
|
+
entryContainer !== topContainer &&
|
|
77
|
+
entryContainer.contains(topContainer)
|
|
78
|
+
) {
|
|
79
|
+
// The current top is nested inside this entry — it stays on top.
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
top = entry;
|
|
83
|
+
}
|
|
84
|
+
return top.handler === handler;
|
|
47
85
|
}
|
|
48
86
|
|
|
49
87
|
/**
|
|
@@ -73,7 +111,9 @@ export function isImeKeyEvent(event: {
|
|
|
73
111
|
/**
|
|
74
112
|
* Whether an element is currently perceivable/focusable — excludes ones hidden
|
|
75
113
|
* via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
|
|
76
|
-
* which the browser skips for Tab
|
|
114
|
+
* which the browser skips for Tab, and ones inside an `aria-hidden="true"`
|
|
115
|
+
* subtree, which sighted-keyboard users could Tab to while AT skips them
|
|
116
|
+
* (WCAG 4.1.2 — focusable content must be exposed to assistive tech).
|
|
77
117
|
*/
|
|
78
118
|
function isVisiblyFocusable(el: HTMLElement): boolean {
|
|
79
119
|
if (el.hasAttribute('inert') || el.closest('[inert]')) {
|
|
@@ -82,6 +122,10 @@ function isVisiblyFocusable(el: HTMLElement): boolean {
|
|
|
82
122
|
if (el.hidden || el.closest('[hidden]')) {
|
|
83
123
|
return false;
|
|
84
124
|
}
|
|
125
|
+
// closest() matches the element itself as well as any ancestor.
|
|
126
|
+
if (el.closest('[aria-hidden="true"]')) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
85
129
|
// offsetParent is null for display:none (and fixed elements); pair with a
|
|
86
130
|
// visibility check via getComputedStyle when available.
|
|
87
131
|
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
@@ -323,12 +367,17 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
323
367
|
|
|
324
368
|
// Register this trap on the shared Escape stack so only the top-most
|
|
325
369
|
// active trap responds to Escape. A stable identity per active period is
|
|
326
|
-
// enough — we push on activate and remove on cleanup.
|
|
370
|
+
// enough — we push on activate and remove on cleanup. The container is
|
|
371
|
+
// read lazily: the ref may not be attached yet at effect time, and the
|
|
372
|
+
// stack resolves top-most by DOM containment at keydown time.
|
|
327
373
|
const escapeHandler = () => {
|
|
328
374
|
onEscape?.();
|
|
329
375
|
};
|
|
330
376
|
if (onEscape) {
|
|
331
|
-
pushEscapeHandler(
|
|
377
|
+
pushEscapeHandler({
|
|
378
|
+
handler: escapeHandler,
|
|
379
|
+
getContainer: () => containerRef.current,
|
|
380
|
+
});
|
|
332
381
|
}
|
|
333
382
|
|
|
334
383
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
@@ -64,8 +64,8 @@ export const docs = {
|
|
|
64
64
|
{
|
|
65
65
|
name: 'options.isRtl',
|
|
66
66
|
type: 'boolean',
|
|
67
|
-
description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts.',
|
|
68
|
-
default: '
|
|
67
|
+
description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts. When omitted, auto-detected from the container computed direction on keydown.',
|
|
68
|
+
default: "undefined (auto-detect from the container's computed direction)",
|
|
69
69
|
required: false,
|
|
70
70
|
},
|
|
71
71
|
{
|
|
@@ -138,7 +138,7 @@ export const docsDense = {
|
|
|
138
138
|
'options.onNavigateAfter': 'callback when navigation would go after last cell. Receives column index + offset.',
|
|
139
139
|
'options.onPageUp': 'callback for Page Up key (e.g. navigate to previous month in calendars).',
|
|
140
140
|
'options.onPageDown': 'callback for Page Down key (e.g. navigate to next month in calendars).',
|
|
141
|
-
'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default
|
|
141
|
+
'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default: auto-detect from container computed direction; explicit boolean wins.',
|
|
142
142
|
'options.hasRovingTabIndex': 'own a single roving tab stop across the grid (one focus target tabindex=0, rest -1); repaired on render + moved with arrows. Attach handleFocus to onFocus. default false.',
|
|
143
143
|
},
|
|
144
144
|
returnDescriptions: {
|