@astryxdesign/core 0.1.9 → 0.2.0-canary.0e01213
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/Center/Center.d.ts +20 -3
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +10 -2
- 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.d.ts +7 -0
- package/dist/Chat/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayout.js +16 -5
- 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 +18 -12
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +42 -64
- 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 +10 -3
- 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 +27 -10
- 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 +87 -45
- 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 +72 -19
- 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 +18 -3
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +58 -30
- 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 +11 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +65 -25
- 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 +115 -35
- package/dist/astryx.umd.js +51 -51
- 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/defineTheme.d.ts +6 -0
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +13 -0
- package/dist/theme/expandColorScale.d.ts +19 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +38 -10
- 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 +8 -3
- 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/Center/Center.doc.mjs +38 -0
- package/src/Center/Center.test.tsx +75 -0
- package/src/Center/Center.tsx +37 -2
- 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.test.tsx +57 -0
- package/src/Chat/ChatLayout.tsx +26 -5
- 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.doc.mjs +5 -2
- package/src/DropdownMenu/DropdownMenu.test.tsx +95 -0
- package/src/DropdownMenu/DropdownMenu.tsx +31 -12
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +49 -66
- 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 +19 -4
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +84 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +680 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +569 -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 +40 -14
- 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 +436 -0
- package/src/MultiSelector/MultiSelector.tsx +133 -67
- 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 +400 -0
- package/src/Selector/Selector.tsx +95 -23
- 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 +48 -21
- 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.test.tsx +161 -2
- package/src/Table/plugins/tree/useTableTreeData.tsx +71 -14
- package/src/Table/types.ts +33 -0
- package/src/Table/useTableRowExpansion.doc.mjs +55 -2
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/Table/useTableTreeData.doc.mjs +9 -0
- 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/defineTheme.ts +6 -0
- package/src/theme/derivedVarRegistry.test.ts +1 -0
- package/src/theme/derivedVarRegistry.ts +6 -0
- package/src/theme/expandColorScale.test.ts +144 -0
- package/src/theme/expandColorScale.ts +53 -15
- 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
|
@@ -19,6 +19,13 @@ export const docs = {
|
|
|
19
19
|
default: "'[role=\"menuitem\"]'",
|
|
20
20
|
required: false,
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
name: 'options.boundarySelector',
|
|
24
|
+
type: 'string',
|
|
25
|
+
description:
|
|
26
|
+
"Selector identifying a list boundary, for lists that contain nested lists of the same kind (e.g. a menu with submenu flyouts). When set, item collection and key handling are scoped to this level's own container. Typically '[role=\"menu\"]'.",
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
22
29
|
{
|
|
23
30
|
name: 'options.wrap',
|
|
24
31
|
type: 'boolean',
|
|
@@ -49,8 +56,8 @@ export const docs = {
|
|
|
49
56
|
{
|
|
50
57
|
name: 'options.isRtl',
|
|
51
58
|
type: 'boolean',
|
|
52
|
-
description: 'Whether the list is in a right-to-left context. When true, ArrowLeft/ArrowRight are swapped for horizontal navigation so it follows visual direction.',
|
|
53
|
-
default: '
|
|
59
|
+
description: 'Whether the list is in a right-to-left context. When true, ArrowLeft/ArrowRight are swapped for horizontal navigation so it follows visual direction. When omitted, auto-detected from the container computed direction on keydown.',
|
|
60
|
+
default: "undefined (auto-detect from the container's computed direction)",
|
|
54
61
|
required: false,
|
|
55
62
|
},
|
|
56
63
|
{
|
|
@@ -99,6 +106,18 @@ export const docs = {
|
|
|
99
106
|
type: '() => boolean',
|
|
100
107
|
description: 'Focus the last enabled item. Returns true when an item was focused.',
|
|
101
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: 'ownsEvent',
|
|
111
|
+
type: '(e: React.KeyboardEvent) => boolean',
|
|
112
|
+
description:
|
|
113
|
+
"Whether a key event belongs to this list level rather than a nested list sharing the same boundarySelector. Always true when no boundarySelector is set. Use to guard consumer-added key handling (Enter/Space, typeahead).",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'getItems',
|
|
117
|
+
type: '() => HTMLElement[]',
|
|
118
|
+
description:
|
|
119
|
+
"This level's focusable items in DOM order (already scoped by boundarySelector). Build typeahead targets from this instead of re-querying.",
|
|
120
|
+
},
|
|
102
121
|
],
|
|
103
122
|
usage: {
|
|
104
123
|
description:
|
|
@@ -123,11 +142,12 @@ export const docsDense = {
|
|
|
123
142
|
paramDescriptions: {
|
|
124
143
|
options: 'config for list focus behavior. All fields optional.',
|
|
125
144
|
'options.itemSelector': 'selector for focusable items in list.',
|
|
145
|
+
'options.boundarySelector': "boundary selector for lists that contain nested lists (e.g. submenu flyouts); scopes items + key handling to this level.",
|
|
126
146
|
'options.wrap': 'whether arrow navigation wraps around at ends.',
|
|
127
147
|
'options.onEscape': 'callback when Escape key pressed (e.g. close menu).',
|
|
128
148
|
'options.orientation': "navigation orientation. 'horizontal' uses ArrowLeft/ArrowRight, 'vertical' uses ArrowUp/ArrowDown, 'both' accepts all four arrows.",
|
|
129
149
|
'options.hasHomeEnd': 'whether Home/End jump to first/last enabled item.',
|
|
130
|
-
'options.isRtl': '
|
|
150
|
+
'options.isRtl': 'ArrowLeft/ArrowRight swap for horizontal nav (RTL). default: auto-detect from container computed direction; explicit boolean wins.',
|
|
131
151
|
'options.hasRovingTabIndex': 'opt into roving-tabindex ownership: hook stamps + repairs a single tab stop across items.',
|
|
132
152
|
'options.hasCaretGuard': "when true, don't steal arrow keys from a nested text input/textarea mid-line or from a contenteditable.",
|
|
133
153
|
},
|
|
@@ -138,6 +158,8 @@ export const docsDense = {
|
|
|
138
158
|
focusItem: 'focus specific item by index (clamped to valid range).',
|
|
139
159
|
focusFirst: 'focus first enabled item; returns true when focused.',
|
|
140
160
|
focusLast: 'focus last enabled item; returns true when focused.',
|
|
161
|
+
ownsEvent: 'whether a key event is this level\'s vs a nested list\'s (boundarySelector); always true without one.',
|
|
162
|
+
getItems: "this level's focusable items in DOM order, scoped by boundarySelector.",
|
|
141
163
|
},
|
|
142
164
|
usage: {
|
|
143
165
|
description:
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file useListFocus.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, useListFocus hook
|
|
6
|
-
* @output Unit tests for useListFocus disabled-item skipping
|
|
6
|
+
* @output Unit tests for useListFocus disabled-item skipping, navigation, and
|
|
7
|
+
* RTL auto-detection
|
|
7
8
|
* @position Testing; validates useListFocus.ts keyboard navigation
|
|
8
9
|
*
|
|
9
10
|
* SYNC: When useListFocus.ts changes, update tests to match new behavior
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import {describe, it, expect} from 'vitest';
|
|
14
|
+
import type {KeyboardEvent as ReactKeyboardEvent} from 'react';
|
|
13
15
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
16
|
import {useListFocus} from './useListFocus';
|
|
15
17
|
|
|
@@ -373,3 +375,153 @@ describe('useListFocus shortcut passthrough', () => {
|
|
|
373
375
|
expect(screen.getByTestId('A')).toHaveFocus();
|
|
374
376
|
});
|
|
375
377
|
});
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A horizontal list (menubar-like) for RTL direction tests. jsdom reflects the
|
|
381
|
+
* `dir` attribute into computed style only on the element that carries it, so
|
|
382
|
+
* `dir` is set on the list container itself — the element the hook reads via
|
|
383
|
+
* listRef.
|
|
384
|
+
*/
|
|
385
|
+
function HorizontalMenu({dir, isRtl}: {dir?: 'ltr' | 'rtl'; isRtl?: boolean}) {
|
|
386
|
+
const {listRef, handleKeyDown} = useListFocus<HTMLDivElement>({
|
|
387
|
+
orientation: 'horizontal',
|
|
388
|
+
isRtl,
|
|
389
|
+
});
|
|
390
|
+
const items = ['One', 'Two', 'Three'];
|
|
391
|
+
return (
|
|
392
|
+
<div ref={listRef} role="menu" dir={dir} onKeyDown={handleKeyDown}>
|
|
393
|
+
{items.map(label => (
|
|
394
|
+
<div key={label} role="menuitem" tabIndex={-1} data-testid={label}>
|
|
395
|
+
{label}
|
|
396
|
+
</div>
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
describe('useListFocus RTL auto-detection (WCAG 1.3.2)', () => {
|
|
403
|
+
it('auto-detects dir="rtl": ArrowLeft moves to the next item', () => {
|
|
404
|
+
render(<HorizontalMenu dir="rtl" />);
|
|
405
|
+
const menu = screen.getByRole('menu');
|
|
406
|
+
screen.getByTestId('One').focus();
|
|
407
|
+
fireEvent.keyDown(menu, {key: 'ArrowLeft'});
|
|
408
|
+
expect(screen.getByTestId('Two')).toHaveFocus();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('auto-detects dir="rtl": ArrowRight moves to the previous item', () => {
|
|
412
|
+
render(<HorizontalMenu dir="rtl" />);
|
|
413
|
+
const menu = screen.getByRole('menu');
|
|
414
|
+
screen.getByTestId('Two').focus();
|
|
415
|
+
fireEvent.keyDown(menu, {key: 'ArrowRight'});
|
|
416
|
+
expect(screen.getByTestId('One')).toHaveFocus();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('stays LTR without a direction: ArrowRight moves to the next item', () => {
|
|
420
|
+
render(<HorizontalMenu />);
|
|
421
|
+
const menu = screen.getByRole('menu');
|
|
422
|
+
screen.getByTestId('One').focus();
|
|
423
|
+
fireEvent.keyDown(menu, {key: 'ArrowRight'});
|
|
424
|
+
expect(screen.getByTestId('Two')).toHaveFocus();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('explicit isRtl={false} overrides a dir="rtl" container', () => {
|
|
428
|
+
render(<HorizontalMenu dir="rtl" isRtl={false} />);
|
|
429
|
+
const menu = screen.getByRole('menu');
|
|
430
|
+
screen.getByTestId('One').focus();
|
|
431
|
+
fireEvent.keyDown(menu, {key: 'ArrowRight'});
|
|
432
|
+
expect(screen.getByTestId('Two')).toHaveFocus();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('explicit isRtl={true} flips arrows without a dir attribute', () => {
|
|
436
|
+
render(<HorizontalMenu isRtl />);
|
|
437
|
+
const menu = screen.getByRole('menu');
|
|
438
|
+
screen.getByTestId('One').focus();
|
|
439
|
+
fireEvent.keyDown(menu, {key: 'ArrowLeft'});
|
|
440
|
+
expect(screen.getByTestId('Two')).toHaveFocus();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// A menu whose second item contains a NESTED role="menu" (mirrors an inline
|
|
445
|
+
// submenu flyout). Exercises boundarySelector: item scoping + event ownership.
|
|
446
|
+
function NestedMenu() {
|
|
447
|
+
const {listRef, handleKeyDown, ownsEvent} = useListFocus<HTMLDivElement>({
|
|
448
|
+
boundarySelector: '[role="menu"]',
|
|
449
|
+
wrap: false,
|
|
450
|
+
});
|
|
451
|
+
return (
|
|
452
|
+
<div ref={listRef} role="menu" onKeyDown={handleKeyDown}>
|
|
453
|
+
<div role="menuitem" tabIndex={-1} data-testid="Outer1">
|
|
454
|
+
Outer1
|
|
455
|
+
</div>
|
|
456
|
+
<div role="menuitem" tabIndex={-1} data-testid="Outer2">
|
|
457
|
+
Outer2
|
|
458
|
+
{/* Nested submenu rendered inline (like a popover flyout). */}
|
|
459
|
+
<div role="menu" data-testid="inner-menu">
|
|
460
|
+
<div role="menuitem" tabIndex={-1} data-testid="Inner1">
|
|
461
|
+
Inner1
|
|
462
|
+
</div>
|
|
463
|
+
<div role="menuitem" tabIndex={-1} data-testid="Inner2">
|
|
464
|
+
Inner2
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
<div role="menuitem" tabIndex={-1} data-testid="Outer3">
|
|
469
|
+
Outer3
|
|
470
|
+
</div>
|
|
471
|
+
{/* Surface ownership for assertions. */}
|
|
472
|
+
<input
|
|
473
|
+
data-testid="probe-owns"
|
|
474
|
+
onKeyDown={e => {
|
|
475
|
+
e.currentTarget.setAttribute('data-owns', String(ownsEvent(e)));
|
|
476
|
+
}}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Variant with the ownsEvent probe placed INSIDE the nested menu, so a key
|
|
483
|
+
// event from it reports as not-owned by the outer list.
|
|
484
|
+
function NestedMenuWithInnerProbe() {
|
|
485
|
+
const {listRef, handleKeyDown, ownsEvent} = useListFocus<HTMLDivElement>({
|
|
486
|
+
boundarySelector: '[role="menu"]',
|
|
487
|
+
wrap: false,
|
|
488
|
+
});
|
|
489
|
+
const report = (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
490
|
+
e.currentTarget.setAttribute('data-owns', String(ownsEvent(e)));
|
|
491
|
+
};
|
|
492
|
+
return (
|
|
493
|
+
<div ref={listRef} role="menu" onKeyDown={handleKeyDown}>
|
|
494
|
+
<div role="menuitem" tabIndex={-1} data-testid="Outer1">
|
|
495
|
+
Outer1
|
|
496
|
+
</div>
|
|
497
|
+
<div role="menu" data-testid="inner-menu">
|
|
498
|
+
<input data-testid="inner-probe" onKeyDown={report} />
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
describe('useListFocus boundarySelector (nested lists)', () => {
|
|
505
|
+
it("ArrowDown skips over a nested list's items to the next own item", () => {
|
|
506
|
+
render(<NestedMenu />);
|
|
507
|
+
const menu = screen.getAllByRole('menu')[0];
|
|
508
|
+
screen.getByTestId('Outer2').focus();
|
|
509
|
+
fireEvent.keyDown(menu, {key: 'ArrowDown'});
|
|
510
|
+
// Lands on Outer3, not Inner1 (which is inside the nested menu).
|
|
511
|
+
expect(screen.getByTestId('Outer3')).toHaveFocus();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('ownsEvent is true for an event originating at this level', () => {
|
|
515
|
+
render(<NestedMenu />);
|
|
516
|
+
const ownProbe = screen.getByTestId('probe-owns');
|
|
517
|
+
fireEvent.keyDown(ownProbe, {key: 'ArrowDown'});
|
|
518
|
+
expect(ownProbe).toHaveAttribute('data-owns', 'true');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('ownsEvent is false for an event from inside a nested list', () => {
|
|
522
|
+
render(<NestedMenuWithInnerProbe />);
|
|
523
|
+
const innerProbe = screen.getByTestId('inner-probe');
|
|
524
|
+
fireEvent.keyDown(innerProbe, {key: 'ArrowDown'});
|
|
525
|
+
expect(innerProbe).toHaveAttribute('data-owns', 'false');
|
|
526
|
+
});
|
|
527
|
+
});
|
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useListFocus.ts
|
|
7
|
-
* @input Uses React useCallback, useRef, useIsomorphicLayoutEffect
|
|
7
|
+
* @input Uses React useCallback, useRef, useIsomorphicLayoutEffect,
|
|
8
|
+
* isRtlElement
|
|
8
9
|
* @output Exports useListFocus hook for linear list keyboard navigation
|
|
9
10
|
* @position Core hook; used by TabMenu for dropdown menu navigation, Toolbar
|
|
10
11
|
* for roving tabindex, ButtonGroup, ContextMenu, NavHeadingMenu, and more.
|
|
12
|
+
* Auto-detects RTL from the container's computed direction so horizontal
|
|
13
|
+
* arrow keys follow visual direction (WCAG 1.3.2).
|
|
11
14
|
*
|
|
12
15
|
* SYNC: When modified, update:
|
|
13
16
|
* - /packages/core/src/hooks/index.ts
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import {useCallback, useRef} from 'react';
|
|
20
|
+
import {isRtlElement} from './isRtlElement';
|
|
17
21
|
import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -34,6 +38,25 @@ export interface UseListFocusOptions {
|
|
|
34
38
|
*/
|
|
35
39
|
itemSelector?: string;
|
|
36
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Selector identifying a list boundary — used to scope a list that contains
|
|
43
|
+
* *nested* lists of the same kind (e.g. a menu with submenu flyouts).
|
|
44
|
+
*
|
|
45
|
+
* Overlays like submenu flyouts render inline (native popover, not a
|
|
46
|
+
* portal), so a nested list's items are DOM descendants of the parent list.
|
|
47
|
+
* Without scoping, the parent's `querySelectorAll` sweeps the nested items
|
|
48
|
+
* into its roving order (leaving hidden items `.focus()` can't land on), and
|
|
49
|
+
* key events from the nested list bubble into the parent's handler (moving
|
|
50
|
+
* focus twice). When set, `useListFocus`:
|
|
51
|
+
* - counts an item as its own only when the item's nearest
|
|
52
|
+
* `boundarySelector` ancestor is this list's container, and
|
|
53
|
+
* - ignores key events whose nearest `boundarySelector` ancestor is not
|
|
54
|
+
* this list's container (i.e. they originated in a nested list).
|
|
55
|
+
*
|
|
56
|
+
* Typically `'[role="menu"]'` for menus. Omit for flat lists.
|
|
57
|
+
*/
|
|
58
|
+
boundarySelector?: string;
|
|
59
|
+
|
|
37
60
|
/**
|
|
38
61
|
* Whether arrow navigation wraps around at the ends.
|
|
39
62
|
* @default true
|
|
@@ -62,7 +85,12 @@ export interface UseListFocusOptions {
|
|
|
62
85
|
* Whether the list is in a right-to-left context. When true, ArrowLeft and
|
|
63
86
|
* ArrowRight are swapped so horizontal navigation follows visual direction.
|
|
64
87
|
* Only affects horizontal (`'horizontal'`/`'both'`) navigation.
|
|
65
|
-
*
|
|
88
|
+
*
|
|
89
|
+
* When omitted, the direction is auto-detected from the container's computed
|
|
90
|
+
* `direction` (read lazily on keydown, only for horizontal arrow keys), so
|
|
91
|
+
* lists inside `dir="rtl"` subtrees flip automatically. Pass an explicit
|
|
92
|
+
* boolean to override detection.
|
|
93
|
+
* @default undefined (auto-detect from the container)
|
|
66
94
|
*/
|
|
67
95
|
isRtl?: boolean;
|
|
68
96
|
|
|
@@ -128,6 +156,23 @@ export interface UseListFocusReturn<T extends HTMLElement = HTMLElement> {
|
|
|
128
156
|
* Focus the last enabled item. Returns true when an item was focused.
|
|
129
157
|
*/
|
|
130
158
|
focusLast: () => boolean;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether a key event belongs to this list level (vs. a nested list that
|
|
162
|
+
* shares the same `boundarySelector`). Useful when a consumer wraps
|
|
163
|
+
* `handleKeyDown` with extra behavior (Enter/Space activation, typeahead)
|
|
164
|
+
* and must apply it only to events this level owns. Always true when no
|
|
165
|
+
* `boundarySelector` is configured.
|
|
166
|
+
*/
|
|
167
|
+
ownsEvent: (e: React.KeyboardEvent) => boolean;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The current, in-DOM-order list of this level's focusable items (already
|
|
171
|
+
* scoped by `boundarySelector`). Exposed so consumers can build typeahead
|
|
172
|
+
* targets from the same source of truth as roving focus, rather than
|
|
173
|
+
* re-querying and re-filtering.
|
|
174
|
+
*/
|
|
175
|
+
getItems: () => HTMLElement[];
|
|
131
176
|
}
|
|
132
177
|
|
|
133
178
|
// Text-editing inputs whose caret must not be hijacked by arrow navigation.
|
|
@@ -271,11 +316,12 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
271
316
|
): UseListFocusReturn<T> {
|
|
272
317
|
const {
|
|
273
318
|
itemSelector = '[role="menuitem"]',
|
|
319
|
+
boundarySelector,
|
|
274
320
|
wrap = true,
|
|
275
321
|
onEscape,
|
|
276
322
|
orientation = 'vertical',
|
|
277
323
|
hasHomeEnd = true,
|
|
278
|
-
isRtl
|
|
324
|
+
isRtl,
|
|
279
325
|
hasRovingTabIndex = false,
|
|
280
326
|
hasCaretGuard = false,
|
|
281
327
|
} = options;
|
|
@@ -299,13 +345,43 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
299
345
|
* Get all focusable items in the list.
|
|
300
346
|
*/
|
|
301
347
|
const getItems = useCallback((): HTMLElement[] => {
|
|
302
|
-
|
|
348
|
+
const listEl = listRef.current;
|
|
349
|
+
if (!listEl) {
|
|
303
350
|
return [];
|
|
304
351
|
}
|
|
305
|
-
|
|
306
|
-
|
|
352
|
+
const matched = Array.from(
|
|
353
|
+
listEl.querySelectorAll<HTMLElement>(itemSelector),
|
|
307
354
|
);
|
|
308
|
-
|
|
355
|
+
// When a boundary is set, keep only items that belong to THIS list level —
|
|
356
|
+
// i.e. whose nearest boundary ancestor is our own container. This excludes
|
|
357
|
+
// items inside nested lists (e.g. inline submenu flyouts) that would
|
|
358
|
+
// otherwise be swept in by querySelectorAll.
|
|
359
|
+
if (!boundarySelector) {
|
|
360
|
+
return matched;
|
|
361
|
+
}
|
|
362
|
+
return matched.filter(el => el.closest(boundarySelector) === listEl);
|
|
363
|
+
}, [itemSelector, boundarySelector]);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Whether a key event belongs to this list level rather than a nested list.
|
|
367
|
+
* With a boundary set, an event that originated inside a nested boundary
|
|
368
|
+
* (e.g. a submenu flyout) has bubbled up to us and must be ignored so we
|
|
369
|
+
* don't double-handle navigation. Without a boundary, every event is ours.
|
|
370
|
+
*/
|
|
371
|
+
const ownsEvent = useCallback(
|
|
372
|
+
(e: React.KeyboardEvent): boolean => {
|
|
373
|
+
const listEl = listRef.current;
|
|
374
|
+
if (!listEl || !boundarySelector) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const target = e.target as HTMLElement | null;
|
|
378
|
+
if (!target) {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return target.closest(boundarySelector) === listEl;
|
|
382
|
+
},
|
|
383
|
+
[boundarySelector],
|
|
384
|
+
);
|
|
309
385
|
|
|
310
386
|
/**
|
|
311
387
|
* Find the next enabled item index from `start`, moving by `step`, optionally
|
|
@@ -473,6 +549,13 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
473
549
|
return;
|
|
474
550
|
}
|
|
475
551
|
|
|
552
|
+
// Ignore events that bubbled up from a nested list (e.g. a submenu
|
|
553
|
+
// flyout, which renders inline inside this container). That level owns
|
|
554
|
+
// and already handled them; re-handling here would move focus twice.
|
|
555
|
+
if (!ownsEvent(e)) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
476
559
|
// Escape is handled regardless of orientation. Preserve the historical
|
|
477
560
|
// behavior of always consuming Escape here (preventDefault) so consumers
|
|
478
561
|
// that relied on it are unaffected.
|
|
@@ -486,11 +569,18 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
486
569
|
const vertical = orientation === 'vertical' || orientation === 'both';
|
|
487
570
|
|
|
488
571
|
// Resolve which keys advance vs retreat, honoring RTL for horizontal.
|
|
572
|
+
// Direction is resolved lazily — getComputedStyle runs only when a
|
|
573
|
+
// horizontal arrow key is actually pressed (SSR-safe, no layout thrash
|
|
574
|
+
// on unrelated keys) — and an explicit `isRtl` always wins.
|
|
489
575
|
const nextKeys: string[] = [];
|
|
490
576
|
const prevKeys: string[] = [];
|
|
491
577
|
if (horizontal) {
|
|
492
|
-
|
|
493
|
-
|
|
578
|
+
const rtl =
|
|
579
|
+
e.key === 'ArrowLeft' || e.key === 'ArrowRight'
|
|
580
|
+
? (isRtl ?? isRtlElement(listRef.current))
|
|
581
|
+
: false;
|
|
582
|
+
nextKeys.push(rtl ? 'ArrowLeft' : 'ArrowRight');
|
|
583
|
+
prevKeys.push(rtl ? 'ArrowRight' : 'ArrowLeft');
|
|
494
584
|
}
|
|
495
585
|
if (vertical) {
|
|
496
586
|
nextKeys.push('ArrowDown');
|
|
@@ -553,6 +643,7 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
553
643
|
focusFirst,
|
|
554
644
|
focusLast,
|
|
555
645
|
onEscape,
|
|
646
|
+
ownsEvent,
|
|
556
647
|
],
|
|
557
648
|
);
|
|
558
649
|
|
|
@@ -563,5 +654,7 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
563
654
|
focusItem,
|
|
564
655
|
focusFirst,
|
|
565
656
|
focusLast,
|
|
657
|
+
ownsEvent,
|
|
658
|
+
getItems,
|
|
566
659
|
};
|
|
567
660
|
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* SYNC: When modified, update these files to stay in sync:
|
|
16
16
|
* - /packages/core/src/i18n/InternationalizationProvider.tsx
|
|
17
17
|
* - /packages/core/src/i18n/t.client.ts
|
|
18
|
+
* - /packages/core/src/i18n/useDirection.ts
|
|
19
|
+
* - /packages/core/src/i18n/getLocaleDirection.ts
|
|
18
20
|
* - /packages/core/src/i18n/index.ts
|
|
19
21
|
*/
|
|
20
22
|
|
|
@@ -23,6 +25,7 @@ import type {Locale, MessagesByLocale, Overrides} from './types';
|
|
|
23
25
|
|
|
24
26
|
export interface InternationalizationContextValue {
|
|
25
27
|
locale: Locale;
|
|
28
|
+
direction: 'ltr' | 'rtl';
|
|
26
29
|
messages: MessagesByLocale;
|
|
27
30
|
overrides?: Overrides;
|
|
28
31
|
}
|
|
@@ -34,6 +37,7 @@ export interface InternationalizationContextValue {
|
|
|
34
37
|
export const InternationalizationContext =
|
|
35
38
|
createContext<InternationalizationContextValue>({
|
|
36
39
|
locale: 'en',
|
|
40
|
+
direction: 'ltr',
|
|
37
41
|
messages: {},
|
|
38
42
|
});
|
|
39
43
|
InternationalizationContext.displayName = 'InternationalizationContext';
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import {useMemo, type ReactNode} from 'react';
|
|
23
23
|
import {InternationalizationContext} from './InternationalizationContext';
|
|
24
|
+
import {getLocaleDirection} from './getLocaleDirection';
|
|
24
25
|
import type {Locale, MessagesByLocale, Overrides} from './types';
|
|
25
26
|
|
|
26
27
|
export interface InternationalizationProviderProps {
|
|
@@ -55,6 +56,13 @@ export interface InternationalizationProviderProps {
|
|
|
55
56
|
* ```
|
|
56
57
|
*/
|
|
57
58
|
overrides?: Overrides;
|
|
59
|
+
/**
|
|
60
|
+
* Optional explicit text direction override. When omitted, direction is derived
|
|
61
|
+
* from `locale` via `Intl.Locale.getTextInfo()`. Provide this to force a
|
|
62
|
+
* direction (e.g. RTL layout testing under an English catalog) or to skip the
|
|
63
|
+
* derivation when you already know the direction.
|
|
64
|
+
*/
|
|
65
|
+
dir?: 'ltr' | 'rtl';
|
|
58
66
|
children: ReactNode;
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -66,11 +74,13 @@ export function InternationalizationProvider({
|
|
|
66
74
|
locale,
|
|
67
75
|
messages,
|
|
68
76
|
overrides,
|
|
77
|
+
dir,
|
|
69
78
|
children,
|
|
70
79
|
}: InternationalizationProviderProps) {
|
|
80
|
+
const direction = dir ?? getLocaleDirection(locale);
|
|
71
81
|
const value = useMemo(
|
|
72
|
-
() => ({locale, messages: messages ?? {}, overrides}),
|
|
73
|
-
[locale, messages, overrides],
|
|
82
|
+
() => ({locale, direction, messages: messages ?? {}, overrides}),
|
|
83
|
+
[locale, direction, messages, overrides],
|
|
74
84
|
);
|
|
75
85
|
return (
|
|
76
86
|
<InternationalizationContext value={value}>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file getLocaleDirection.test.ts
|
|
5
|
+
* @input packages/core/src/i18n/getLocaleDirection.ts
|
|
6
|
+
* @output Unit tests for the server-safe locale → direction helper
|
|
7
|
+
* @position Colocated tests; targets LTR/RTL classification and the
|
|
8
|
+
* try/catch fallback for malformed input.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, expect, test} from 'vitest';
|
|
12
|
+
import {getLocaleDirection} from '../getLocaleDirection';
|
|
13
|
+
|
|
14
|
+
describe('getLocaleDirection', () => {
|
|
15
|
+
test('returns ltr for English', () => {
|
|
16
|
+
expect(getLocaleDirection('en')).toBe('ltr');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('returns rtl for RTL languages', () => {
|
|
20
|
+
expect(getLocaleDirection('ar')).toBe('rtl');
|
|
21
|
+
expect(getLocaleDirection('he')).toBe('rtl');
|
|
22
|
+
expect(getLocaleDirection('fa')).toBe('rtl');
|
|
23
|
+
expect(getLocaleDirection('ur')).toBe('rtl');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('returns ltr for regional LTR tags', () => {
|
|
27
|
+
expect(getLocaleDirection('pt-BR')).toBe('ltr');
|
|
28
|
+
expect(getLocaleDirection('zh-CN')).toBe('ltr');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('falls back to ltr for a malformed locale without throwing', () => {
|
|
32
|
+
expect(() => getLocaleDirection('not_a_locale')).not.toThrow();
|
|
33
|
+
expect(getLocaleDirection('not_a_locale')).toBe('ltr');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('falls back to ltr for an empty string without throwing', () => {
|
|
37
|
+
expect(() => getLocaleDirection('')).not.toThrow();
|
|
38
|
+
expect(getLocaleDirection('')).toBe('ltr');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useDirection.test.tsx
|
|
5
|
+
* @input packages/core/src/i18n/useDirection.ts, InternationalizationProvider
|
|
6
|
+
* @output Unit tests for the ambient text-direction hook
|
|
7
|
+
* @position Colocated tests; targets the default (no-provider) fallback,
|
|
8
|
+
* locale-derived direction, and the explicit `dir` override.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {ReactNode} from 'react';
|
|
12
|
+
import {describe, expect, test} from 'vitest';
|
|
13
|
+
import {renderHook} from '@testing-library/react';
|
|
14
|
+
import {useDirection} from '../useDirection';
|
|
15
|
+
import {InternationalizationProvider} from '../InternationalizationProvider';
|
|
16
|
+
|
|
17
|
+
describe('useDirection', () => {
|
|
18
|
+
test('returns ltr when rendered without a provider', () => {
|
|
19
|
+
const {result} = renderHook(() => useDirection());
|
|
20
|
+
expect(result.current).toBe('ltr');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('returns ltr under an English provider', () => {
|
|
24
|
+
const {result} = renderHook(() => useDirection(), {
|
|
25
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
26
|
+
<InternationalizationProvider locale="en">
|
|
27
|
+
{children}
|
|
28
|
+
</InternationalizationProvider>
|
|
29
|
+
),
|
|
30
|
+
});
|
|
31
|
+
expect(result.current).toBe('ltr');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('returns rtl under an Arabic provider', () => {
|
|
35
|
+
const {result} = renderHook(() => useDirection(), {
|
|
36
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
37
|
+
<InternationalizationProvider locale="ar">
|
|
38
|
+
{children}
|
|
39
|
+
</InternationalizationProvider>
|
|
40
|
+
),
|
|
41
|
+
});
|
|
42
|
+
expect(result.current).toBe('rtl');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('explicit dir="rtl" wins over an LTR locale', () => {
|
|
46
|
+
const {result} = renderHook(() => useDirection(), {
|
|
47
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
48
|
+
<InternationalizationProvider locale="en" dir="rtl">
|
|
49
|
+
{children}
|
|
50
|
+
</InternationalizationProvider>
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
expect(result.current).toBe('rtl');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('explicit dir="ltr" wins over an RTL locale', () => {
|
|
57
|
+
const {result} = renderHook(() => useDirection(), {
|
|
58
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
59
|
+
<InternationalizationProvider locale="ar" dir="ltr">
|
|
60
|
+
{children}
|
|
61
|
+
</InternationalizationProvider>
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
expect(result.current).toBe('ltr');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file getLocaleDirection.ts
|
|
5
|
+
* @input BCP 47 locale tag
|
|
6
|
+
* @output 'ltr' | 'rtl'
|
|
7
|
+
* @position Server-safe helper for computing text direction from a locale.
|
|
8
|
+
* Callable in React Server Components / Next.js layouts to set <html dir>.
|
|
9
|
+
*
|
|
10
|
+
* Uses Intl.Locale.getTextInfo() (CLDR-backed) with a try/catch fallback to
|
|
11
|
+
* 'ltr'. React 19 baseline covers all target browsers + Node ≥20; the older
|
|
12
|
+
* accessor form (`.textInfo`) is handled as a fallback for defensive parity.
|
|
13
|
+
*
|
|
14
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
15
|
+
* - /packages/core/src/i18n/InternationalizationProvider.tsx (uses this to derive default)
|
|
16
|
+
* - /packages/core/src/i18n/useDirection.ts (client-side hook counterpart)
|
|
17
|
+
* - /packages/core/src/i18n/index.ts (public export)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export function getLocaleDirection(locale: string): 'ltr' | 'rtl' {
|
|
21
|
+
try {
|
|
22
|
+
const loc = new Intl.Locale(locale);
|
|
23
|
+
// @ts-expect-error — older engines expose accessor form `.textInfo`
|
|
24
|
+
const info = typeof loc.getTextInfo === 'function' ? loc.getTextInfo() : loc.textInfo;
|
|
25
|
+
return info?.direction === 'rtl' ? 'rtl' : 'ltr';
|
|
26
|
+
} catch {
|
|
27
|
+
return 'ltr';
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/i18n/index.ts
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
* The public API is deliberately small:
|
|
11
11
|
* - InternationalizationProvider — provider component
|
|
12
12
|
* - useTranslator — hook returning a translator function
|
|
13
|
+
* - useDirection — hook returning the ambient text direction
|
|
14
|
+
* - getLocaleDirection — server-safe locale → direction helper
|
|
13
15
|
* - Translator — interface for consumer-injected runtimes
|
|
14
16
|
*
|
|
15
17
|
* SYNC: When modified, update these files to stay in sync:
|
|
16
18
|
* - /packages/core/src/index.ts
|
|
19
|
+
* - /packages/core/src/i18n/useDirection.ts
|
|
20
|
+
* - /packages/core/src/i18n/getLocaleDirection.ts
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
export {InternationalizationProvider} from './InternationalizationProvider';
|
|
@@ -22,6 +26,8 @@ export {InternationalizationContext} from './InternationalizationContext';
|
|
|
22
26
|
export type {InternationalizationContextValue} from './InternationalizationContext';
|
|
23
27
|
export {useTranslator} from './useTranslator';
|
|
24
28
|
export type {TranslatorFn} from './useTranslator';
|
|
29
|
+
export {useDirection} from './useDirection';
|
|
30
|
+
export {getLocaleDirection} from './getLocaleDirection';
|
|
25
31
|
export type {Translator} from './translator';
|
|
26
32
|
export type {
|
|
27
33
|
Locale,
|