@astryxdesign/core 0.1.2 → 0.1.3
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 +165 -0
- package/README.md +12 -12
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +11 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +24 -5
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +2 -4
- package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/ButtonGroup/ButtonGroup.js +0 -1
- package/dist/Calendar/Calendar.d.ts +5 -4
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +82 -43
- package/dist/Calendar/hooks/index.d.ts +0 -2
- package/dist/Calendar/hooks/index.d.ts.map +1 -1
- package/dist/Calendar/hooks/index.js +1 -2
- package/dist/Calendar/index.d.ts +3 -3
- package/dist/Calendar/index.d.ts.map +1 -1
- package/dist/Calendar/index.js +1 -1
- package/dist/Calendar/styles.d.ts +3 -9
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +3 -11
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +17 -3
- package/dist/Center/Center.d.ts +11 -1
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +9 -3
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +0 -1
- package/dist/Chat/ChatMessageList.d.ts +14 -1
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +2 -0
- package/dist/Chat/useTriggerMenu.d.ts +8 -1
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +15 -3
- package/dist/CheckboxInput/CheckboxInput.d.ts +23 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +51 -9
- package/dist/CheckboxList/CheckboxList.d.ts +13 -1
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +39 -9
- package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
- package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +0 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +9 -5
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +51 -37
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +5 -5
- package/dist/ContextMenu/ContextMenu.d.ts +4 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +93 -11
- package/dist/DateInput/DateInput.d.ts +23 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +167 -99
- package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +42 -9
- package/dist/DateTimeInput/DateTimeInput.d.ts +35 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +78 -15
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +12 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -8
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +42 -14
- package/dist/Field/Field.d.ts +19 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +4 -0
- package/dist/Field/FieldLabel.d.ts +21 -2
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +41 -26
- package/dist/FileInput/FileInput.d.ts +22 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +56 -10
- package/dist/Grid/Grid.d.ts +11 -1
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +32 -6
- package/dist/InputGroup/InputGroup.d.ts +4 -3
- package/dist/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/InputGroup/InputGroup.js +16 -6
- package/dist/InputGroup/InputGroupContext.d.ts +4 -0
- package/dist/InputGroup/InputGroupContext.d.ts.map +1 -1
- package/dist/InputGroup/InputGroupContext.js +2 -2
- package/dist/InputGroup/groupStyles.d.ts +1 -1
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +3 -3
- package/dist/InputGroup/index.d.ts +1 -0
- package/dist/InputGroup/index.d.ts.map +1 -1
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +33 -1
- package/dist/Layer/layerAnimations.stylex.d.ts +4 -0
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +8 -4
- package/dist/Layer/useLayer.d.ts +10 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +64 -9
- package/dist/Layout/Layout.d.ts +4 -11
- package/dist/Layout/Layout.d.ts.map +1 -1
- package/dist/Layout/Layout.js +10 -11
- package/dist/Layout/LayoutFooter.d.ts +2 -2
- package/dist/Layout/LayoutFooter.d.ts.map +1 -1
- package/dist/Layout/LayoutHeader.d.ts +2 -2
- package/dist/Layout/LayoutHeader.d.ts.map +1 -1
- package/dist/Layout/LayoutPanel.d.ts +2 -2
- package/dist/Layout/LayoutPanel.d.ts.map +1 -1
- package/dist/Layout/padding.stylex.d.ts +51 -0
- package/dist/Layout/padding.stylex.d.ts.map +1 -1
- package/dist/Layout/padding.stylex.js +63 -0
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +4 -2
- package/dist/Link/Link.d.ts +16 -1
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +18 -7
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +7 -0
- package/dist/MoreMenu/MoreMenu.d.ts +1 -7
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +0 -2
- package/dist/MultiSelector/MultiSelector.d.ts +27 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +141 -42
- package/dist/MultiSelector/hooks.d.ts +12 -1
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +14 -1
- package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.js +34 -3
- package/dist/NumberInput/NumberInput.d.ts +23 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +62 -12
- package/dist/Pagination/Pagination.d.ts +1 -1
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +15 -1
- package/dist/Popover/Popover.d.ts +18 -2
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +5 -2
- package/dist/Popover/usePopover.d.ts +38 -3
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +24 -9
- package/dist/PowerSearch/PowerSearch.d.ts +11 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +6 -1
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +3 -5
- package/dist/RadioList/RadioList.d.ts +20 -2
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +105 -9
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -2
- package/dist/SegmentedControl/SegmentedControl.d.ts +13 -2
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +78 -44
- package/dist/SegmentedControl/SegmentedControlContext.d.ts +7 -0
- package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +13 -2
- package/dist/Selector/Selector.d.ts +24 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +122 -43
- package/dist/Selector/hooks.d.ts +8 -1
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +14 -1
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +2 -0
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +1 -1
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +8 -2
- package/dist/Slider/Slider.d.ts +21 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +36 -9
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +1 -1
- package/dist/Stack/Stack.d.ts +38 -23
- package/dist/Stack/Stack.d.ts.map +1 -1
- package/dist/Stack/Stack.js +25 -1
- package/dist/Stack/StackItem.d.ts +12 -23
- package/dist/Stack/StackItem.d.ts.map +1 -1
- package/dist/Stack/StackItem.js +8 -1
- package/dist/Switch/Switch.d.ts +23 -2
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +41 -11
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +6 -0
- package/dist/TabList/TabList.d.ts +16 -4
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +87 -10
- package/dist/TabList/TabListContext.d.ts +5 -0
- package/dist/TabList/TabListContext.d.ts.map +1 -1
- package/dist/TabList/TabListContext.js +5 -0
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +6 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +11 -8
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +10 -1
- package/dist/Table/TableCell.d.ts +8 -1
- package/dist/Table/TableCell.d.ts.map +1 -1
- package/dist/Table/TableCell.js +10 -10
- package/dist/Table/TableHeaderCell.d.ts +8 -1
- package/dist/Table/TableHeaderCell.d.ts.map +1 -1
- package/dist/Table/TableHeaderCell.js +16 -16
- package/dist/Table/index.d.ts +2 -1
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +2 -2
- package/dist/Table/plugins/pagination/paginateData.d.ts +2 -1
- package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/paginateData.js +8 -3
- package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/useTablePagination.js +6 -1
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +59 -3
- package/dist/Table/tableContextMenu.d.ts +33 -0
- package/dist/Table/tableContextMenu.d.ts.map +1 -0
- package/dist/Table/tableContextMenu.js +133 -0
- package/dist/Table/types.d.ts +66 -0
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/TextArea/TextArea.d.ts +22 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +41 -9
- package/dist/TextInput/TextInput.d.ts +23 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +55 -10
- package/dist/TimeInput/TimeInput.d.ts +23 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +147 -76
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +14 -0
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +131 -3
- package/dist/Tokenizer/Tokenizer.d.ts +12 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +26 -4
- package/dist/Toolbar/Toolbar.d.ts +3 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +51 -7
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +56 -9
- package/dist/TopNav/TopNavHeading.d.ts +7 -1
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +8 -0
- package/dist/TreeList/TreeList.d.ts +1 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +72 -2
- package/dist/TreeList/TreeListItem.d.ts +11 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +51 -12
- package/dist/TreeList/treeListItem.markers.stylex.d.ts +16 -0
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +1 -0
- package/dist/TreeList/treeListItem.markers.stylex.js +22 -0
- package/dist/Typeahead/BaseTypeahead.d.ts +13 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +69 -6
- package/dist/Typeahead/Typeahead.d.ts +26 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +65 -29
- package/dist/VisuallyHidden/VisuallyHidden.d.ts +61 -0
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden/VisuallyHidden.js +65 -0
- package/dist/VisuallyHidden/index.d.ts +11 -0
- package/dist/VisuallyHidden/index.d.ts.map +1 -0
- package/dist/VisuallyHidden/index.js +13 -0
- package/dist/astryx.css +18 -0
- package/dist/astryx.umd.js +55 -55
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/focusableSelector.d.ts +22 -0
- package/dist/hooks/focusableSelector.d.ts.map +1 -0
- package/dist/hooks/focusableSelector.js +24 -0
- package/dist/hooks/index.d.ts +12 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +7 -2
- package/dist/hooks/useAnnounce.d.ts +40 -0
- package/dist/hooks/useAnnounce.d.ts.map +1 -0
- package/dist/hooks/useAnnounce.js +142 -0
- package/dist/hooks/useClickableContainer.d.ts +16 -1
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +16 -4
- package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
- package/dist/hooks/useEntryAnimation.js +4 -4
- package/dist/hooks/useFocusTrap.d.ts +18 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +87 -3
- package/dist/hooks/useGridFocus.d.ts +88 -1
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +261 -62
- package/dist/hooks/useKeyboardHint.d.ts +82 -0
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -0
- package/dist/hooks/useKeyboardHint.js +220 -0
- package/dist/hooks/useListFocus.d.ts +78 -7
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +305 -52
- package/dist/hooks/useLongPress.d.ts +21 -0
- package/dist/hooks/useLongPress.d.ts.map +1 -0
- package/dist/hooks/useLongPress.js +89 -0
- package/dist/hooks/useTreeFocus.d.ts +157 -0
- package/dist/hooks/useTreeFocus.d.ts.map +1 -0
- package/dist/hooks/useTreeFocus.js +337 -0
- package/dist/hooks/useTypeahead.d.ts +63 -0
- package/dist/hooks/useTypeahead.d.ts.map +1 -0
- package/dist/hooks/useTypeahead.js +117 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/utils/dateTypes.d.ts +12 -0
- package/dist/utils/dateTypes.d.ts.map +1 -1
- package/dist/utils/dateTypes.js +41 -1
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/inputAria.d.ts +29 -0
- package/dist/utils/inputAria.d.ts.map +1 -0
- package/dist/utils/inputAria.js +30 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +5 -2
- package/docs.mjs +2 -2
- package/package.json +12 -8
- package/src/Avatar/Avatar.test.tsx +37 -0
- package/src/Avatar/Avatar.tsx +10 -5
- package/src/Breadcrumbs/BreadcrumbItem.tsx +25 -7
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -10
- package/src/Button/Button.tsx +3 -16
- package/src/ButtonGroup/ButtonGroup.test.tsx +9 -9
- package/src/ButtonGroup/ButtonGroup.tsx +0 -1
- package/src/Calendar/Calendar.doc.mjs +4 -4
- package/src/Calendar/Calendar.test.tsx +168 -30
- package/src/Calendar/Calendar.tsx +121 -66
- package/src/Calendar/hooks/index.ts +0 -6
- package/src/Calendar/index.ts +1 -3
- package/src/Calendar/styles.ts +4 -9
- package/src/Carousel/Carousel.doc.mjs +7 -1
- package/src/Carousel/Carousel.test.tsx +20 -0
- package/src/Carousel/Carousel.tsx +16 -1
- package/src/Center/Center.doc.mjs +12 -2
- package/src/Center/Center.tsx +28 -3
- package/src/Chat/ChatComposerInput.test.tsx +26 -7
- package/src/Chat/ChatComposerInput.tsx +0 -1
- package/src/Chat/ChatDictationButton.doc.mjs +1 -1
- package/src/Chat/ChatMessageList.doc.mjs +8 -0
- package/src/Chat/ChatMessageList.test.tsx +18 -0
- package/src/Chat/ChatMessageList.tsx +16 -0
- package/src/Chat/useTriggerMenu.tsx +22 -4
- package/src/CheckboxInput/CheckboxInput.doc.mjs +10 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +172 -27
- package/src/CheckboxInput/CheckboxInput.tsx +78 -6
- package/src/CheckboxList/CheckboxList.doc.mjs +9 -0
- package/src/CheckboxList/CheckboxList.test.tsx +192 -26
- package/src/CheckboxList/CheckboxList.tsx +56 -3
- package/src/CheckboxList/CheckboxListContext.tsx +7 -0
- package/src/CheckboxList/CheckboxListItem.tsx +0 -3
- package/src/Citation/Citation.doc.mjs +10 -0
- package/src/Citation/Citation.test.tsx +121 -0
- package/src/Citation/Citation.tsx +15 -3
- package/src/CodeBlock/CodeBlock.test.tsx +115 -0
- package/src/CodeBlock/CodeBlock.tsx +63 -33
- package/src/CommandPalette/CommandPalette.test.tsx +47 -1
- package/src/CommandPalette/CommandPalette.tsx +9 -21
- package/src/ContextMenu/ContextMenu.doc.mjs +4 -4
- package/src/ContextMenu/ContextMenu.test.tsx +155 -1
- package/src/ContextMenu/ContextMenu.tsx +113 -16
- package/src/DateInput/DateInput.doc.mjs +286 -56
- package/src/DateInput/DateInput.test.tsx +248 -2
- package/src/DateInput/DateInput.tsx +196 -100
- package/src/DateRangeInput/DateRangeInput.doc.mjs +251 -46
- package/src/DateRangeInput/DateRangeInput.test.tsx +182 -20
- package/src/DateRangeInput/DateRangeInput.tsx +63 -5
- package/src/DateTimeInput/DateTimeInput.doc.mjs +368 -67
- package/src/DateTimeInput/DateTimeInput.test.tsx +279 -11
- package/src/DateTimeInput/DateTimeInput.tsx +125 -17
- package/src/Dialog/Dialog.doc.mjs +2 -2
- package/src/Dialog/Dialog.tsx +12 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -8
- package/src/DropdownMenu/DropdownMenu.test.tsx +56 -42
- package/src/DropdownMenu/DropdownMenu.tsx +52 -18
- package/src/Field/Field.doc.mjs +59 -18
- package/src/Field/Field.test.tsx +32 -0
- package/src/Field/Field.tsx +23 -5
- package/src/Field/FieldLabel.tsx +55 -17
- package/src/FileInput/FileInput.doc.mjs +17 -0
- package/src/FileInput/FileInput.test.tsx +283 -49
- package/src/FileInput/FileInput.tsx +76 -4
- package/src/Grid/Grid.doc.mjs +17 -4
- package/src/Grid/Grid.test.tsx +43 -30
- package/src/Grid/Grid.tsx +38 -3
- package/src/Icon/Icon.doc.mjs +3 -3
- package/src/InputGroup/InputGroup.doc.mjs +111 -23
- package/src/InputGroup/InputGroup.test.tsx +257 -60
- package/src/InputGroup/InputGroup.tsx +23 -5
- package/src/InputGroup/InputGroupContext.ts +9 -4
- package/src/InputGroup/groupStyles.ts +7 -1
- package/src/InputGroup/index.ts +1 -0
- package/src/Kbd/Kbd.test.tsx +22 -4
- package/src/Kbd/Kbd.tsx +35 -3
- package/src/Layer/layerAnimations.stylex.ts +20 -4
- package/src/Layer/useLayer.doc.mjs +5 -0
- package/src/Layer/useLayer.test.tsx +111 -0
- package/src/Layer/useLayer.tsx +90 -9
- package/src/Layout/Layout.tsx +8 -16
- package/src/Layout/LayoutFooter.doc.mjs +4 -4
- package/src/Layout/LayoutFooter.tsx +3 -3
- package/src/Layout/LayoutHeader.doc.mjs +4 -4
- package/src/Layout/LayoutHeader.tsx +3 -3
- package/src/Layout/LayoutPanel.tsx +3 -3
- package/src/Layout/padding.stylex.ts +52 -0
- package/src/Lightbox/Lightbox.doc.mjs +9 -0
- package/src/Lightbox/Lightbox.test.tsx +53 -5
- package/src/Lightbox/Lightbox.tsx +9 -4
- package/src/Link/Link.doc.mjs +14 -0
- package/src/Link/Link.test.tsx +50 -0
- package/src/Link/Link.tsx +39 -4
- package/src/Markdown/Markdown.test.tsx +11 -0
- package/src/Markdown/Markdown.tsx +7 -0
- package/src/MetadataList/MetadataListItem.doc.mjs +4 -0
- package/src/MoreMenu/MoreMenu.doc.mjs +2 -17
- package/src/MoreMenu/MoreMenu.tsx +0 -9
- package/src/MultiSelector/MultiSelector.doc.mjs +30 -0
- package/src/MultiSelector/MultiSelector.test.tsx +375 -8
- package/src/MultiSelector/MultiSelector.tsx +194 -48
- package/src/MultiSelector/hooks.ts +27 -0
- package/src/NavMenu/NavHeadingMenu.test.tsx +77 -1
- package/src/NavMenu/NavHeadingMenu.tsx +50 -3
- package/src/NumberInput/NumberInput.doc.mjs +17 -0
- package/src/NumberInput/NumberInput.test.tsx +242 -40
- package/src/NumberInput/NumberInput.tsx +89 -11
- package/src/Pagination/Pagination.doc.mjs +3 -3
- package/src/Pagination/Pagination.test.tsx +117 -5
- package/src/Pagination/Pagination.tsx +21 -1
- package/src/Popover/Popover.doc.mjs +26 -0
- package/src/Popover/Popover.test.tsx +121 -0
- package/src/Popover/Popover.tsx +23 -2
- package/src/Popover/usePopover.doc.mjs +9 -3
- package/src/Popover/usePopover.tsx +70 -12
- package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
- package/src/PowerSearch/PowerSearch.test.tsx +97 -1
- package/src/PowerSearch/PowerSearch.tsx +16 -3
- package/src/ProgressBar/ProgressBar.test.tsx +34 -26
- package/src/ProgressBar/ProgressBar.tsx +3 -4
- package/src/RadioList/RadioList.doc.mjs +9 -0
- package/src/RadioList/RadioList.test.tsx +252 -16
- package/src/RadioList/RadioList.tsx +166 -10
- package/src/RadioList/RadioListItem.tsx +14 -2
- package/src/SegmentedControl/SegmentedControl.doc.mjs +9 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +214 -42
- package/src/SegmentedControl/SegmentedControl.tsx +102 -54
- package/src/SegmentedControl/SegmentedControlContext.ts +7 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +14 -1
- package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
- package/src/Selector/Selector.doc.mjs +26 -0
- package/src/Selector/Selector.test.tsx +387 -15
- package/src/Selector/Selector.tsx +175 -67
- package/src/Selector/hooks.ts +23 -0
- package/src/SideNav/SideNav.test.tsx +48 -0
- package/src/SideNav/SideNavHeading.tsx +8 -2
- package/src/SideNav/SideNavItem.tsx +2 -4
- package/src/Skeleton/Skeleton.test.tsx +25 -0
- package/src/Skeleton/Skeleton.tsx +11 -1
- package/src/Slider/Slider.doc.mjs +15 -0
- package/src/Slider/Slider.test.tsx +155 -2
- package/src/Slider/Slider.tsx +53 -5
- package/src/Spinner/Spinner.tsx +7 -1
- package/src/Stack/Stack.doc.mjs +77 -0
- package/src/Stack/Stack.test.tsx +55 -5
- package/src/Stack/Stack.tsx +73 -24
- package/src/Stack/StackItem.test.tsx +23 -0
- package/src/Stack/StackItem.tsx +20 -23
- package/src/Switch/Switch.doc.mjs +15 -0
- package/src/Switch/Switch.test.tsx +162 -27
- package/src/Switch/Switch.tsx +61 -21
- package/src/TabList/Tab.tsx +6 -0
- package/src/TabList/TabList.doc.mjs +6 -0
- package/src/TabList/TabList.test.tsx +251 -0
- package/src/TabList/TabList.tsx +103 -12
- package/src/TabList/TabListContext.ts +7 -3
- package/src/TabList/TabMenu.tsx +6 -3
- package/src/Table/BaseTable.tsx +21 -17
- package/src/Table/Table.doc.mjs +2 -0
- package/src/Table/Table.test.tsx +10 -0
- package/src/Table/Table.tsx +8 -0
- package/src/Table/TableCell.tsx +25 -23
- package/src/Table/TableHeaderCell.tsx +28 -29
- package/src/Table/index.ts +3 -2
- package/src/Table/plugins/pagination/paginateData.ts +10 -3
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +16 -0
- package/src/Table/plugins/pagination/useTablePagination.tsx +8 -1
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +91 -2
- package/src/Table/plugins/sortable/useTableSortable.tsx +53 -1
- package/src/Table/tableContextMenu.test.tsx +157 -0
- package/src/Table/tableContextMenu.tsx +142 -0
- package/src/Table/types.ts +74 -0
- package/src/Table/useTableColumnResize.doc.mjs +3 -3
- package/src/Table/useTableStickyColumns.doc.mjs +2 -2
- package/src/Text/Text.doc.mjs +18 -18
- package/src/TextArea/TextArea.doc.mjs +17 -0
- package/src/TextArea/TextArea.test.tsx +167 -35
- package/src/TextArea/TextArea.tsx +62 -15
- package/src/TextInput/TextInput.doc.mjs +17 -0
- package/src/TextInput/TextInput.test.tsx +168 -34
- package/src/TextInput/TextInput.tsx +77 -9
- package/src/Thumbnail/Thumbnail.doc.mjs +4 -4
- package/src/TimeInput/TimeInput.doc.mjs +340 -60
- package/src/TimeInput/TimeInput.test.tsx +255 -7
- package/src/TimeInput/TimeInput.tsx +176 -70
- package/src/Timestamp/Timestamp.doc.mjs +19 -19
- package/src/Toast/Toast.tsx +14 -0
- package/src/Toast/ToastViewport.test.tsx +214 -0
- package/src/Toast/ToastViewport.tsx +167 -21
- package/src/Tokenizer/Tokenizer.doc.mjs +29 -14
- package/src/Tokenizer/Tokenizer.test.tsx +105 -1
- package/src/Tokenizer/Tokenizer.tsx +37 -1
- package/src/Toolbar/Toolbar.test.tsx +109 -6
- package/src/Toolbar/Toolbar.tsx +59 -5
- package/src/Tooltip/Tooltip.test.tsx +68 -0
- package/src/Tooltip/useTooltip.tsx +55 -8
- package/src/TopNav/TopNav.test.tsx +50 -0
- package/src/TopNav/TopNavHeading.tsx +17 -1
- package/src/TreeList/TreeList.test.tsx +295 -6
- package/src/TreeList/TreeList.tsx +73 -1
- package/src/TreeList/TreeListItem.tsx +51 -5
- package/src/TreeList/treeListItem.markers.stylex.ts +20 -0
- package/src/Typeahead/BaseTypeahead.tsx +93 -9
- package/src/Typeahead/Typeahead.doc.mjs +141 -30
- package/src/Typeahead/Typeahead.test.tsx +331 -1
- package/src/Typeahead/Typeahead.tsx +105 -33
- package/src/VisuallyHidden/VisuallyHidden.doc.mjs +80 -0
- package/src/VisuallyHidden/VisuallyHidden.test.tsx +76 -0
- package/src/VisuallyHidden/VisuallyHidden.tsx +113 -0
- package/src/VisuallyHidden/index.ts +15 -0
- package/src/docs-script.test.mjs +50 -0
- package/src/hooks/focusableSelector.ts +25 -0
- package/src/hooks/index.ts +28 -2
- package/src/hooks/useAnnounce.test.tsx +101 -0
- package/src/hooks/useAnnounce.ts +168 -0
- package/src/hooks/useClickableContainer.ts +16 -4
- package/src/hooks/useEntryAnimation.ts +16 -4
- package/src/hooks/useFocusTrap.test.tsx +132 -0
- package/src/hooks/useFocusTrap.ts +98 -4
- package/src/hooks/useGridFocus.doc.mjs +40 -2
- package/src/hooks/useGridFocus.test.tsx +132 -0
- package/src/hooks/useGridFocus.ts +343 -67
- package/src/hooks/useKeyboardHint.doc.mjs +101 -0
- package/src/hooks/useKeyboardHint.test.tsx +70 -0
- package/src/hooks/useKeyboardHint.tsx +332 -0
- package/src/hooks/useListFocus.doc.mjs +50 -10
- package/src/hooks/useListFocus.test.tsx +375 -0
- package/src/hooks/useListFocus.ts +395 -57
- package/src/hooks/useLongPress.test.tsx +134 -0
- package/src/hooks/useLongPress.ts +119 -0
- package/src/hooks/useTreeFocus.doc.mjs +140 -0
- package/src/hooks/useTreeFocus.test.tsx +214 -0
- package/src/hooks/useTreeFocus.ts +559 -0
- package/src/hooks/useTypeahead.test.tsx +109 -0
- package/src/hooks/useTypeahead.ts +183 -0
- package/src/index.ts +1 -0
- package/src/theme/derivedVarRegistry.test.ts +10 -5
- package/src/utils/dateTypes.test.ts +40 -0
- package/src/utils/dateTypes.ts +40 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/inputAria.test.ts +39 -0
- package/src/utils/inputAria.ts +58 -0
- package/src/utils/timeParser.test.ts +10 -0
- package/src/utils/timeParser.ts +5 -2
- package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts +0 -57
- package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts.map +0 -1
- package/dist/Calendar/hooks/useCalendarRovingTabindex.js +0 -96
- package/src/Calendar/hooks/useCalendarRovingTabindex.ts +0 -118
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useFocusTrap.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, useFocusTrap hook
|
|
6
|
+
* @output Unit tests for useFocusTrap tabbable-element model + Escape/IME guard
|
|
7
|
+
* @position Testing; validates useFocusTrap.ts focusable detection + dismissal coordination
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When useFocusTrap.ts changes, update tests to match new behavior
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
|
+
import {useFocusTrap} from './useFocusTrap';
|
|
15
|
+
|
|
16
|
+
function Trap({children}: {children: React.ReactNode}) {
|
|
17
|
+
const {containerRef, focusFirst} = useFocusTrap<HTMLDivElement>({
|
|
18
|
+
isActive: true,
|
|
19
|
+
});
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<button type="button" data-testid="outside">
|
|
23
|
+
Outside
|
|
24
|
+
</button>
|
|
25
|
+
<div ref={containerRef} data-testid="trap">
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
<button type="button" onClick={focusFirst} data-testid="focus-first">
|
|
29
|
+
Focus first
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function EscapeTrap({
|
|
36
|
+
isActive,
|
|
37
|
+
onEscape,
|
|
38
|
+
label,
|
|
39
|
+
}: {
|
|
40
|
+
isActive: boolean;
|
|
41
|
+
onEscape: () => void;
|
|
42
|
+
label: string;
|
|
43
|
+
}) {
|
|
44
|
+
const {containerRef} = useFocusTrap<HTMLDivElement>({isActive, onEscape});
|
|
45
|
+
return (
|
|
46
|
+
<div ref={containerRef} data-testid={label}>
|
|
47
|
+
<button type="button">{label}-btn</button>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('useFocusTrap tabbable model (infra-8)', () => {
|
|
53
|
+
it('treats a contenteditable as focusable (focusFirst lands on it)', () => {
|
|
54
|
+
render(
|
|
55
|
+
<Trap>
|
|
56
|
+
<div
|
|
57
|
+
contentEditable
|
|
58
|
+
data-testid="editor"
|
|
59
|
+
suppressContentEditableWarning>
|
|
60
|
+
Type here
|
|
61
|
+
</div>
|
|
62
|
+
</Trap>,
|
|
63
|
+
);
|
|
64
|
+
fireEvent.click(screen.getByTestId('focus-first'));
|
|
65
|
+
expect(screen.getByTestId('editor')).toHaveFocus();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('ignores an inert subtree when finding focusables', () => {
|
|
69
|
+
render(
|
|
70
|
+
<Trap>
|
|
71
|
+
<div inert>
|
|
72
|
+
<button type="button" data-testid="inert-btn">
|
|
73
|
+
Inert
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<button type="button" data-testid="real-btn">
|
|
77
|
+
Real
|
|
78
|
+
</button>
|
|
79
|
+
</Trap>,
|
|
80
|
+
);
|
|
81
|
+
fireEvent.click(screen.getByTestId('focus-first'));
|
|
82
|
+
// Focus skips the inert button and lands on the real one.
|
|
83
|
+
expect(screen.getByTestId('real-btn')).toHaveFocus();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('useFocusTrap Escape coordination', () => {
|
|
88
|
+
it('calls onEscape for a single active trap', () => {
|
|
89
|
+
const onEscape = vi.fn();
|
|
90
|
+
render(<EscapeTrap isActive onEscape={onEscape} label="only" />);
|
|
91
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
92
|
+
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('only the top-most trap responds to Escape when nested', () => {
|
|
96
|
+
const outer = vi.fn();
|
|
97
|
+
const inner = vi.fn();
|
|
98
|
+
render(
|
|
99
|
+
<>
|
|
100
|
+
<EscapeTrap isActive onEscape={outer} label="outer" />
|
|
101
|
+
<EscapeTrap isActive onEscape={inner} label="inner" />
|
|
102
|
+
</>,
|
|
103
|
+
);
|
|
104
|
+
// The most recently activated trap (inner) is on top.
|
|
105
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
106
|
+
expect(inner).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(outer).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('ignores Escape during IME composition', () => {
|
|
111
|
+
const onEscape = vi.fn();
|
|
112
|
+
render(<EscapeTrap isActive onEscape={onEscape} label="ime" />);
|
|
113
|
+
fireEvent.keyDown(document, {key: 'Escape', isComposing: true});
|
|
114
|
+
expect(onEscape).not.toHaveBeenCalled();
|
|
115
|
+
// keyCode 229 (composition) is also ignored
|
|
116
|
+
fireEvent.keyDown(document, {key: 'Escape', keyCode: 229});
|
|
117
|
+
expect(onEscape).not.toHaveBeenCalled();
|
|
118
|
+
// a normal Escape still works
|
|
119
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
120
|
+
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not respond after the trap is deactivated', () => {
|
|
124
|
+
const onEscape = vi.fn();
|
|
125
|
+
const {rerender} = render(
|
|
126
|
+
<EscapeTrap isActive onEscape={onEscape} label="toggle" />,
|
|
127
|
+
);
|
|
128
|
+
rerender(<EscapeTrap isActive={false} onEscape={onEscape} label="toggle" />);
|
|
129
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
130
|
+
expect(onEscape).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -17,11 +17,80 @@
|
|
|
17
17
|
|
|
18
18
|
import {useCallback, useEffect, useRef} from 'react';
|
|
19
19
|
|
|
20
|
+
import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Module-level stack of active focus-trap Escape handlers.
|
|
24
|
+
*
|
|
25
|
+
* Every active `useFocusTrap` used to attach its own document-level `keydown`
|
|
26
|
+
* listener with no coordination, so a single Escape press closed *every* open
|
|
27
|
+
* layer at once (e.g. a popover nested inside a Dialog closed both). Tracking
|
|
28
|
+
* traps in a shared stack lets only the most recently activated (top-most)
|
|
29
|
+
* trap respond to Escape.
|
|
30
|
+
*/
|
|
31
|
+
const escapeStack: (() => void)[] = [];
|
|
32
|
+
|
|
33
|
+
function pushEscapeHandler(handler: () => void): void {
|
|
34
|
+
escapeStack.push(handler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeEscapeHandler(handler: () => void): void {
|
|
38
|
+
const index = escapeStack.lastIndexOf(handler);
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
escapeStack.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isTopEscapeHandler(handler: () => void): boolean {
|
|
45
|
+
return escapeStack[escapeStack.length - 1] === handler;
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
/**
|
|
21
|
-
*
|
|
49
|
+
* Whether any focus-trap Escape handler is currently active (i.e. a popover
|
|
50
|
+
* layer is open). Other overlay primitives that manage their own Escape (e.g.
|
|
51
|
+
* Dialog) can consult this to defer to a popover layered on top of them,
|
|
52
|
+
* giving topmost-only dismissal until a full layer stack exists.
|
|
22
53
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
export function hasActiveFocusTrapEscape(): boolean {
|
|
55
|
+
return escapeStack.length > 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether an Escape keydown should be ignored because it is cancelling an
|
|
60
|
+
* in-progress IME composition. CJK/IME users press Escape to cancel
|
|
61
|
+
* composition; that must not close the surrounding overlay. `keyCode === 229`
|
|
62
|
+
* covers browsers that fire keydown before `isComposing` is set. Exported so
|
|
63
|
+
* other overlays (Dialog, Drawer, CommandPalette) share one definition.
|
|
64
|
+
*/
|
|
65
|
+
export function isImeKeyEvent(event: {
|
|
66
|
+
isComposing?: boolean;
|
|
67
|
+
keyCode?: number;
|
|
68
|
+
}): boolean {
|
|
69
|
+
return event.isComposing === true || event.keyCode === 229;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether an element is currently perceivable/focusable — excludes ones hidden
|
|
74
|
+
* via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
|
|
75
|
+
* which the browser skips for Tab.
|
|
76
|
+
*/
|
|
77
|
+
function isVisiblyFocusable(el: HTMLElement): boolean {
|
|
78
|
+
if (el.hasAttribute('inert') || el.closest('[inert]')) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (el.hidden || el.closest('[hidden]')) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// offsetParent is null for display:none (and fixed elements); pair with a
|
|
85
|
+
// visibility check via getComputedStyle when available.
|
|
86
|
+
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
87
|
+
const style = window.getComputedStyle(el);
|
|
88
|
+
if (style.visibility === 'hidden' || style.display === 'none') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
25
94
|
|
|
26
95
|
/**
|
|
27
96
|
* Get all focusable elements within a container.
|
|
@@ -29,7 +98,7 @@ const FOCUSABLE_SELECTOR =
|
|
|
29
98
|
function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|
30
99
|
return Array.from(
|
|
31
100
|
container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
|
|
32
|
-
);
|
|
101
|
+
).filter(isVisiblyFocusable);
|
|
33
102
|
}
|
|
34
103
|
|
|
35
104
|
/**
|
|
@@ -203,6 +272,16 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
203
272
|
return;
|
|
204
273
|
}
|
|
205
274
|
|
|
275
|
+
// Register this trap on the shared Escape stack so only the top-most
|
|
276
|
+
// active trap responds to Escape. A stable identity per active period is
|
|
277
|
+
// enough — we push on activate and remove on cleanup.
|
|
278
|
+
const escapeHandler = () => {
|
|
279
|
+
onEscape?.();
|
|
280
|
+
};
|
|
281
|
+
if (onEscape) {
|
|
282
|
+
pushEscapeHandler(escapeHandler);
|
|
283
|
+
}
|
|
284
|
+
|
|
206
285
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
207
286
|
const container = containerRef.current;
|
|
208
287
|
if (!container) {
|
|
@@ -210,7 +289,19 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
210
289
|
}
|
|
211
290
|
|
|
212
291
|
if (event.key === 'Escape' && onEscape) {
|
|
292
|
+
// Ignore Escape that is cancelling an IME composition, already handled
|
|
293
|
+
// by a nested handler, or not targeting the top-most trap.
|
|
294
|
+
if (
|
|
295
|
+
event.defaultPrevented ||
|
|
296
|
+
isImeKeyEvent(event) ||
|
|
297
|
+
!isTopEscapeHandler(escapeHandler)
|
|
298
|
+
) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Mark handled and stop propagation so an outer layer (e.g. a Dialog
|
|
302
|
+
// hosting this popover) does not also dismiss on the same press.
|
|
213
303
|
event.preventDefault();
|
|
304
|
+
event.stopPropagation();
|
|
214
305
|
onEscape();
|
|
215
306
|
return;
|
|
216
307
|
}
|
|
@@ -247,6 +338,9 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
247
338
|
|
|
248
339
|
return () => {
|
|
249
340
|
document.removeEventListener('keydown', handleKeyDown);
|
|
341
|
+
if (onEscape) {
|
|
342
|
+
removeEscapeHandler(escapeHandler);
|
|
343
|
+
}
|
|
250
344
|
};
|
|
251
345
|
}, [isActive, onEscape]);
|
|
252
346
|
|
|
@@ -21,10 +21,22 @@ export const docs = {
|
|
|
21
21
|
{
|
|
22
22
|
name: 'options.cellSelector',
|
|
23
23
|
type: 'string',
|
|
24
|
-
description: 'Selector for
|
|
24
|
+
description: 'Selector for cells within the grid. Should match ALL cell positions in DOM order (including disabled/empty) so grid geometry is preserved.',
|
|
25
25
|
default: "'button:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'",
|
|
26
26
|
required: false,
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
name: 'options.isCellFocusable',
|
|
30
|
+
type: '(cell: HTMLElement) => boolean',
|
|
31
|
+
description: 'Predicate for whether a matched cell can receive focus. Omit to treat every matched cell as focusable.',
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'options.getFocusTarget',
|
|
36
|
+
type: '(cell: HTMLElement) => HTMLElement | null',
|
|
37
|
+
description: 'Resolves the element to focus for a cell, e.g. a button inside a role="gridcell" wrapper. Omit to focus the cell itself.',
|
|
38
|
+
required: false,
|
|
39
|
+
},
|
|
28
40
|
{
|
|
29
41
|
name: 'options.onNavigateBefore',
|
|
30
42
|
type: '(column: number, offset: number) => void',
|
|
@@ -49,6 +61,20 @@ export const docs = {
|
|
|
49
61
|
description: 'Callback for Page Down key (e.g., navigate to next month in calendars).',
|
|
50
62
|
required: false,
|
|
51
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'options.isRtl',
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts.',
|
|
68
|
+
default: 'false',
|
|
69
|
+
required: false,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'options.hasRovingTabIndex',
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'Own a single roving tab stop across the grid: one focusable cell (its resolved focus target) carries tabindex="0", the rest -1. Stamped/repaired on render and moved with arrow navigation. Attach the returned handleFocus to the container onFocus.',
|
|
75
|
+
default: 'false',
|
|
76
|
+
required: false,
|
|
77
|
+
},
|
|
52
78
|
],
|
|
53
79
|
returns: [
|
|
54
80
|
{
|
|
@@ -61,6 +87,11 @@ export const docs = {
|
|
|
61
87
|
type: '(e: React.KeyboardEvent) => void',
|
|
62
88
|
description: 'Key down handler to attach to the grid container.',
|
|
63
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: 'handleFocus',
|
|
92
|
+
type: '(e: React.FocusEvent) => void',
|
|
93
|
+
description: 'Focus handler for the grid container. Keeps the roving tab stop in sync when hasRovingTabIndex is enabled; a no-op otherwise, so always safe to attach.',
|
|
94
|
+
},
|
|
64
95
|
{
|
|
65
96
|
name: 'focusCell',
|
|
66
97
|
type: '(index: number) => void',
|
|
@@ -83,6 +114,7 @@ export const docs = {
|
|
|
83
114
|
bestPractices: [
|
|
84
115
|
{ guidance: true, description: 'Use for calendar date grids: wire onPageUp/onPageDown to month navigation and onNavigateBefore/onNavigateAfter for cross-month arrow key navigation.' },
|
|
85
116
|
{ guidance: true, description: 'Attach both gridRef and handleKeyDown to the grid container element.' },
|
|
117
|
+
{ guidance: true, description: 'For roving-tabindex grids (e.g. Calendar), set hasRovingTabIndex: true and attach handleFocus to the container onFocus; seed one focus target with tabindex=0 and the hook repairs and moves it.' },
|
|
86
118
|
{ guidance: false, description: 'Use for simple linear lists; prefer useListFocus for 1D navigation.' },
|
|
87
119
|
],
|
|
88
120
|
},
|
|
@@ -99,15 +131,20 @@ export const docsDense = {
|
|
|
99
131
|
paramDescriptions: {
|
|
100
132
|
options: 'config for grid focus behavior.',
|
|
101
133
|
'options.columns': '# columns in grid. Used for up/down navigation (moves by this many cells).',
|
|
102
|
-
'options.cellSelector': 'selector for
|
|
134
|
+
'options.cellSelector': 'selector for grid cells in DOM order (incl. disabled/empty) so geometry is preserved.',
|
|
135
|
+
'options.isCellFocusable': 'predicate for whether a matched cell can take focus. Omit = all focusable.',
|
|
136
|
+
'options.getFocusTarget': 'resolve element to focus for a cell (e.g. button inside gridcell wrapper). Omit = focus the cell.',
|
|
103
137
|
'options.onNavigateBefore': 'callback when navigation would go before first cell. Receives column index + offset (1 for horizontal, columns for vertical).',
|
|
104
138
|
'options.onNavigateAfter': 'callback when navigation would go after last cell. Receives column index + offset.',
|
|
105
139
|
'options.onPageUp': 'callback for Page Up key (e.g. navigate to previous month in calendars).',
|
|
106
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 false.',
|
|
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.',
|
|
107
143
|
},
|
|
108
144
|
returnDescriptions: {
|
|
109
145
|
gridRef: 'ref to attach to grid container element.',
|
|
110
146
|
handleKeyDown: 'key down handler for grid container.',
|
|
147
|
+
handleFocus: 'focus handler for grid container; syncs roving tab stop when hasRovingTabIndex on, else no-op.',
|
|
111
148
|
focusCell: 'focus specific cell by index (clamped to valid range).',
|
|
112
149
|
focusFirst: 'focus first focusable cell in grid.',
|
|
113
150
|
focusLast: 'focus last focusable cell in grid.',
|
|
@@ -118,6 +155,7 @@ export const docsDense = {
|
|
|
118
155
|
bestPractices: [
|
|
119
156
|
{ guidance: true, description: 'Use for calendar date grids: wire onPageUp/onPageDown to month navigation + onNavigateBefore/onNavigateAfter for cross-month arrow key navigation.' },
|
|
120
157
|
{ guidance: true, description: 'Attach both gridRef + handleKeyDown to grid container element.' },
|
|
158
|
+
{ guidance: true, description: 'For roving-tabindex grids (e.g. Calendar): hasRovingTabIndex: true + attach handleFocus to onFocus; seed one focus target tabindex=0, hook repairs + moves it.' },
|
|
121
159
|
{ guidance: false, description: 'Use for simple linear lists; prefer useListFocus for 1D navigation.' },
|
|
122
160
|
],
|
|
123
161
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useGridFocus.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, useGridFocus hook
|
|
6
|
+
* @output Unit tests for useGridFocus roving tabindex + RTL navigation
|
|
7
|
+
* @position Testing; validates useGridFocus.ts roving-tabindex ownership
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When useGridFocus.ts changes, update tests to match new behavior
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect} from 'vitest';
|
|
13
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
|
+
import {useGridFocus} from './useGridFocus';
|
|
15
|
+
|
|
16
|
+
const NO_DISABLED: number[] = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A 3x3 grid of cells shaped like Calendar: each `role="gridcell"` div wraps a
|
|
20
|
+
* focusable `<button>` (the focus target). `seed` marks which button starts
|
|
21
|
+
* tabbable (mirrors Calendar seeding the selected/today/first-enabled day).
|
|
22
|
+
*/
|
|
23
|
+
function Grid({
|
|
24
|
+
disabled = NO_DISABLED,
|
|
25
|
+
seed = 0,
|
|
26
|
+
...opts
|
|
27
|
+
}: {
|
|
28
|
+
disabled?: number[];
|
|
29
|
+
seed?: number;
|
|
30
|
+
} & Partial<Parameters<typeof useGridFocus>[0]>) {
|
|
31
|
+
const {gridRef, handleKeyDown, handleFocus} = useGridFocus<HTMLDivElement>({
|
|
32
|
+
columns: 3,
|
|
33
|
+
cellSelector: '[role="gridcell"]',
|
|
34
|
+
isCellFocusable: cell =>
|
|
35
|
+
cell.querySelector('button:not([disabled])') !== null,
|
|
36
|
+
getFocusTarget: cell => cell.querySelector<HTMLElement>('button'),
|
|
37
|
+
hasRovingTabIndex: true,
|
|
38
|
+
...opts,
|
|
39
|
+
});
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={gridRef}
|
|
43
|
+
role="grid"
|
|
44
|
+
onKeyDown={handleKeyDown}
|
|
45
|
+
onFocus={handleFocus}>
|
|
46
|
+
{Array.from({length: 9}, (_, i) => (
|
|
47
|
+
<div role="gridcell" key={i}>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
disabled={disabled.includes(i)}
|
|
51
|
+
tabIndex={i === seed ? 0 : -1}
|
|
52
|
+
data-testid={`cell-${i}`}>
|
|
53
|
+
{i}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('useGridFocus roving tabindex (hasRovingTabIndex)', () => {
|
|
62
|
+
it('honors the seeded tab stop and stamps -1 on the rest', () => {
|
|
63
|
+
render(<Grid seed={4} />);
|
|
64
|
+
expect(screen.getByTestId('cell-4')).toHaveAttribute('tabindex', '0');
|
|
65
|
+
expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
|
|
66
|
+
expect(screen.getByTestId('cell-8')).toHaveAttribute('tabindex', '-1');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('repairs to the first focusable cell when no cell is seeded', () => {
|
|
70
|
+
render(<Grid seed={-1} />);
|
|
71
|
+
expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '0');
|
|
72
|
+
expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '-1');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('promotes the first ENABLED cell when the seed is disabled', () => {
|
|
76
|
+
render(<Grid seed={-1} disabled={[0, 1]} />);
|
|
77
|
+
expect(screen.getByTestId('cell-2')).toHaveAttribute('tabindex', '0');
|
|
78
|
+
expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('ArrowRight moves the tab stop to the next cell', () => {
|
|
82
|
+
render(<Grid seed={0} />);
|
|
83
|
+
const grid = screen.getByRole('grid');
|
|
84
|
+
screen.getByTestId('cell-0').focus();
|
|
85
|
+
fireEvent.keyDown(grid, {key: 'ArrowRight'});
|
|
86
|
+
expect(screen.getByTestId('cell-1')).toHaveFocus();
|
|
87
|
+
expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '0');
|
|
88
|
+
expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('ArrowDown moves the tab stop one row down', () => {
|
|
92
|
+
render(<Grid seed={0} />);
|
|
93
|
+
const grid = screen.getByRole('grid');
|
|
94
|
+
screen.getByTestId('cell-0').focus();
|
|
95
|
+
fireEvent.keyDown(grid, {key: 'ArrowDown'});
|
|
96
|
+
expect(screen.getByTestId('cell-3')).toHaveFocus();
|
|
97
|
+
expect(screen.getByTestId('cell-3')).toHaveAttribute('tabindex', '0');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handleFocus repairs the stop when the tabbable cell became disabled', () => {
|
|
101
|
+
// Seed cell 0 as tabbable, then disable it and fire the container onFocus.
|
|
102
|
+
// syncTabStops should promote the first still-focusable cell (cell 1).
|
|
103
|
+
const {rerender} = render(<Grid seed={0} />);
|
|
104
|
+
const grid = screen.getByRole('grid');
|
|
105
|
+
expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '0');
|
|
106
|
+
rerender(<Grid seed={-1} disabled={[0]} />);
|
|
107
|
+
fireEvent.focus(grid);
|
|
108
|
+
expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '0');
|
|
109
|
+
expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('flips ArrowLeft/ArrowRight under RTL', () => {
|
|
113
|
+
render(<Grid seed={1} isRtl />);
|
|
114
|
+
const grid = screen.getByRole('grid');
|
|
115
|
+
screen.getByTestId('cell-1').focus();
|
|
116
|
+
// In RTL, ArrowLeft is "forward" (moves to the next cell in DOM order).
|
|
117
|
+
fireEvent.keyDown(grid, {key: 'ArrowLeft'});
|
|
118
|
+
expect(screen.getByTestId('cell-2')).toHaveFocus();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('does not manage tabindex when hasRovingTabIndex is off', () => {
|
|
122
|
+
render(<Grid seed={0} hasRovingTabIndex={false} />);
|
|
123
|
+
// The seeded -1/0 values are left untouched (caller owns them), and
|
|
124
|
+
// navigation still works without stamping.
|
|
125
|
+
const grid = screen.getByRole('grid');
|
|
126
|
+
screen.getByTestId('cell-0').focus();
|
|
127
|
+
fireEvent.keyDown(grid, {key: 'ArrowRight'});
|
|
128
|
+
expect(screen.getByTestId('cell-1')).toHaveFocus();
|
|
129
|
+
// cell-1 kept its seeded -1; the hook did not promote it.
|
|
130
|
+
expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '-1');
|
|
131
|
+
});
|
|
132
|
+
});
|