@astryxdesign/core 0.1.9-canary.d2d84f8 → 0.1.9-canary.e00a241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Avatar/Avatar.d.ts +6 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +46 -12
- package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
- package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
- 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/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +50 -9
- package/dist/Calendar/dayCellUtils.d.ts +4 -4
- package/dist/Calendar/dayCellUtils.js +2 -2
- package/dist/Calendar/styles.d.ts +30 -31
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +30 -31
- package/dist/Card/Card.js +1 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +22 -12
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +3 -1
- 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/CheckboxInput/CheckboxInput.js +4 -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 +13 -7
- 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/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +14 -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 +1 -0
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +31 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts +1 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +20 -19
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +23 -18
- package/dist/Divider/Divider.d.ts +2 -2
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +13 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +22 -5
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +4 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +4 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +427 -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 +23 -0
- package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemHover.js +33 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
- package/dist/DropdownMenu/menuItemRoles.js +10 -1
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
- package/dist/DropdownMenu/renderDropdownItems.js +21 -6
- package/dist/Field/Field.d.ts +3 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +4 -1
- package/dist/Field/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 +4 -1
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +4 -2
- package/dist/FileInput/FileInput.d.ts +1 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +91 -65
- 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/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +15 -9
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +6 -3
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +7 -0
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +65 -40
- package/dist/NumberInput/NumberInput.d.ts +1 -0
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +20 -17
- package/dist/Outline/Outline.js +2 -2
- package/dist/RadioList/RadioList.d.ts +2 -2
- package/dist/RadioList/RadioListItem.js +4 -4
- 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 +2 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +58 -16
- 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/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 +160 -91
- 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/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +30 -4
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +36 -26
- 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 +14 -6
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
- package/dist/Table/types.d.ts +32 -0
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/TextArea/TextArea.d.ts +1 -0
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +16 -19
- package/dist/TextInput/TextInput.d.ts +1 -0
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +18 -17
- package/dist/TimeInput/TimeInput.d.ts +1 -0
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +15 -17
- package/dist/Timestamp/Timestamp.d.ts +31 -3
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +47 -76
- 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/Token/Token.d.ts +30 -3
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +17 -2
- package/dist/Token/index.d.ts +1 -1
- package/dist/Token/index.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +2 -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/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.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +19 -10
- package/dist/Typeahead/Typeahead.js +1 -1
- 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 +38 -14
- package/dist/astryx.umd.js +52 -52
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- 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/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 +158 -0
- 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/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +13 -0
- 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 +76 -0
- package/locales/pseudo.json +57 -0
- package/package.json +6 -1
- package/src/Avatar/Avatar.doc.mjs +18 -4
- package/src/Avatar/Avatar.test.tsx +106 -0
- package/src/Avatar/Avatar.tsx +62 -10
- package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
- package/src/Avatar/AvatarStatusDot.tsx +6 -4
- package/src/AvatarGroup/AvatarGroup.test.tsx +16 -1
- package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +51 -0
- package/src/Breadcrumbs/index.ts +4 -0
- package/src/Button/Button.doc.mjs +0 -4
- package/src/Calendar/Calendar.test.tsx +170 -20
- package/src/Calendar/Calendar.tsx +73 -16
- package/src/Calendar/dayCellUtils.test.ts +18 -18
- package/src/Calendar/dayCellUtils.ts +2 -2
- package/src/Calendar/styles.ts +33 -39
- package/src/Card/Card.tsx +1 -1
- package/src/Carousel/Carousel.tsx +25 -6
- package/src/Chat/ChatComposer.doc.mjs +2 -2
- package/src/Chat/ChatComposerInput.doc.mjs +1 -1
- package/src/Chat/ChatDictationButton.tsx +7 -1
- package/src/Chat/ChatMessageBubble.tsx +15 -10
- package/src/Chat/ChatSystemMessage.test.tsx +5 -0
- package/src/Chat/ChatToolCalls.tsx +9 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
- package/src/CheckboxInput/CheckboxInput.tsx +4 -4
- package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
- 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.tsx +35 -8
- 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/ContextMenu/ContextMenu.test.tsx +42 -0
- package/src/ContextMenu/ContextMenu.tsx +13 -11
- package/src/ContextMenu/index.ts +5 -0
- package/src/DateInput/DateInput.doc.mjs +15 -5
- package/src/DateInput/DateInput.test.tsx +210 -3
- package/src/DateInput/DateInput.tsx +41 -29
- package/src/DateRangeInput/DateRangeInput.doc.mjs +11 -3
- package/src/DateRangeInput/DateRangeInput.test.tsx +120 -3
- package/src/DateRangeInput/DateRangeInput.tsx +29 -27
- package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
- package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
- package/src/DateTimeInput/DateTimeInput.tsx +44 -39
- package/src/Divider/Divider.test.tsx +32 -8
- package/src/Divider/Divider.tsx +14 -1
- package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
- package/src/DropdownMenu/DropdownMenu.tsx +31 -12
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
- package/src/DropdownMenu/DropdownMenuItem.tsx +8 -2
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +8 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +635 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +553 -0
- package/src/DropdownMenu/index.ts +16 -0
- package/src/DropdownMenu/menuItemHover.ts +39 -0
- package/src/DropdownMenu/menuItemRoles.ts +9 -0
- package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
- package/src/Field/Field.tsx +14 -9
- package/src/Field/inputStyles.stylex.ts +2 -0
- package/src/FieldStatus/FieldStatus.tsx +10 -4
- package/src/FileInput/FileInput.doc.mjs +13 -7
- package/src/FileInput/FileInput.test.tsx +84 -16
- package/src/FileInput/FileInput.tsx +96 -79
- 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/Lightbox/Lightbox.tsx +11 -3
- package/src/List/List.test.tsx +17 -29
- package/src/List/List.tsx +6 -1
- package/src/Markdown/Markdown.test.tsx +21 -0
- package/src/Markdown/Markdown.tsx +4 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +11 -0
- package/src/MultiSelector/MultiSelector.test.tsx +306 -3
- package/src/MultiSelector/MultiSelector.tsx +88 -57
- package/src/NumberInput/NumberInput.doc.mjs +11 -5
- package/src/NumberInput/NumberInput.test.tsx +48 -0
- package/src/NumberInput/NumberInput.tsx +25 -26
- package/src/Outline/Outline.test.tsx +12 -12
- package/src/Outline/Outline.tsx +2 -2
- package/src/RadioList/RadioList.doc.mjs +6 -0
- package/src/RadioList/RadioList.tsx +2 -2
- package/src/RadioList/RadioListItem.tsx +4 -4
- package/src/Resizable/ResizeHandle.test.tsx +94 -0
- package/src/Resizable/ResizeHandle.tsx +70 -20
- package/src/SegmentedControl/SegmentedControlItem.tsx +2 -0
- package/src/Selector/Selector.doc.mjs +8 -0
- package/src/Selector/Selector.test.tsx +328 -3
- package/src/Selector/Selector.tsx +70 -19
- package/src/SideNav/SideNav.tsx +1 -8
- package/src/SideNav/SideNavCollapseButton.tsx +9 -7
- package/src/SideNav/SideNavItem.tsx +5 -3
- 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 +22 -0
- package/src/Switch/Switch.tsx +90 -33
- 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/Table/BaseTable.tsx +34 -2
- package/src/Table/Table.doc.mjs +11 -0
- package/src/Table/Table.test.tsx +85 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +24 -17
- 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 +37 -8
- package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
- package/src/Table/types.ts +33 -0
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/TextArea/TextArea.doc.mjs +11 -5
- package/src/TextArea/TextArea.tsx +17 -26
- package/src/TextInput/TextInput.doc.mjs +11 -5
- package/src/TextInput/TextInput.test.tsx +130 -3
- package/src/TextInput/TextInput.tsx +18 -25
- package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +11 -5
- package/src/TimeInput/TimeInput.tsx +14 -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/Token/Token.test.tsx +21 -15
- package/src/Token/Token.tsx +31 -14
- package/src/Token/index.ts +1 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +8 -1
- package/src/Tokenizer/Tokenizer.tsx +4 -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/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 +29 -14
- package/src/Typeahead/Typeahead.doc.mjs +6 -0
- package/src/Typeahead/Typeahead.test.tsx +27 -0
- package/src/Typeahead/Typeahead.tsx +1 -1
- package/src/Typeahead/TypeaheadItem.tsx +2 -1
- package/src/__tests__/inputWidthContract.test.tsx +62 -0
- package/src/doc-mjs.d.ts +19 -0
- package/src/hooks/index.ts +6 -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/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 +162 -0
- package/src/hooks/useInputStatusIcon.tsx +224 -0
- 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/reset.css +0 -26
- package/src/reset.test.ts +60 -0
- package/src/theme/derivedVarRegistry.test.ts +1 -0
- package/src/theme/derivedVarRegistry.ts +6 -0
- package/src/theme/generateThemeRules.test.ts +42 -0
- package/src/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
|
@@ -201,6 +201,100 @@ describe('ResizeHandle', () => {
|
|
|
201
201
|
expect(document.body.style.userSelect).toBe('');
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
// --- RTL pointer-drag direction ---
|
|
205
|
+
|
|
206
|
+
it('drives the region with the raw pointer delta under LTR', () => {
|
|
207
|
+
const resizable: ResizableProps = {
|
|
208
|
+
_size: 200,
|
|
209
|
+
_isCollapsed: false,
|
|
210
|
+
_onResizeStart: vi.fn(),
|
|
211
|
+
_onResizeMove: vi.fn(),
|
|
212
|
+
_onResizeEnd: vi.fn(),
|
|
213
|
+
_minSizePx: 100,
|
|
214
|
+
_maxSizePx: 400,
|
|
215
|
+
_snaps: [],
|
|
216
|
+
_collapsedSize: 40,
|
|
217
|
+
_collapsible: false,
|
|
218
|
+
_isResizableProps: true,
|
|
219
|
+
};
|
|
220
|
+
render(<ResizeHandle resizable={resizable} label="Resize" />);
|
|
221
|
+
const hitArea = screen.getByRole('separator')
|
|
222
|
+
.firstElementChild as HTMLElement;
|
|
223
|
+
|
|
224
|
+
fireEvent.pointerDown(hitArea, {clientX: 0, clientY: 0});
|
|
225
|
+
// Pointer moves +40px to the right → panel grows by +40 under LTR.
|
|
226
|
+
fireEvent.pointerMove(window, {clientX: 40, clientY: 0});
|
|
227
|
+
expect(resizable._onResizeMove).toHaveBeenLastCalledWith(40);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('inverts the pointer delta under RTL so dragging resizes intuitively', () => {
|
|
231
|
+
const resizable: ResizableProps = {
|
|
232
|
+
_size: 200,
|
|
233
|
+
_isCollapsed: false,
|
|
234
|
+
_onResizeStart: vi.fn(),
|
|
235
|
+
_onResizeMove: vi.fn(),
|
|
236
|
+
_onResizeEnd: vi.fn(),
|
|
237
|
+
_minSizePx: 100,
|
|
238
|
+
_maxSizePx: 400,
|
|
239
|
+
_snaps: [],
|
|
240
|
+
_collapsedSize: 40,
|
|
241
|
+
_collapsible: false,
|
|
242
|
+
_isResizableProps: true,
|
|
243
|
+
};
|
|
244
|
+
// Under RTL the start panel sits on the RIGHT, so a pointer move to the
|
|
245
|
+
// right (+clientX) must SHRINK it — the delta is inverted. The handle reads
|
|
246
|
+
// its own computed `direction` via getRTLMultiplier(); jsdom doesn't resolve
|
|
247
|
+
// inherited `direction`, so force it on the separator (the handle element),
|
|
248
|
+
// mirroring the Slider RTL pointer-mapping test precedent.
|
|
249
|
+
render(<ResizeHandle resizable={resizable} label="Resize" />);
|
|
250
|
+
const separator = screen.getByRole('separator');
|
|
251
|
+
const hitArea = separator.firstElementChild as HTMLElement;
|
|
252
|
+
|
|
253
|
+
const realGetComputedStyle = window.getComputedStyle;
|
|
254
|
+
const gcsSpy = vi
|
|
255
|
+
.spyOn(window, 'getComputedStyle')
|
|
256
|
+
.mockImplementation((el: Element, pseudo?: string | null) => {
|
|
257
|
+
if (el === separator) {
|
|
258
|
+
return {direction: 'rtl'} as CSSStyleDeclaration;
|
|
259
|
+
}
|
|
260
|
+
return realGetComputedStyle(el, pseudo ?? undefined);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
fireEvent.pointerDown(hitArea, {clientX: 0, clientY: 0});
|
|
264
|
+
// Same +40px physical move as LTR, but mirrored → −40 delta under RTL.
|
|
265
|
+
fireEvent.pointerMove(window, {clientX: 40, clientY: 0});
|
|
266
|
+
expect(resizable._onResizeMove).toHaveBeenLastCalledWith(-40);
|
|
267
|
+
|
|
268
|
+
gcsSpy.mockRestore();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// --- Hit-area geometry (grab zone tracks the visible pill) ---
|
|
272
|
+
|
|
273
|
+
it('anchors the biased grab zone with the pill offset and a dir-flipped centering shift', () => {
|
|
274
|
+
// For an off-center pill the hit area must reuse the pill's physical offset
|
|
275
|
+
// construction (anchored at insetInlineStart:0) rather than a divider-
|
|
276
|
+
// relative 50% anchor + percentage bias, so the two stay aligned in LTR and
|
|
277
|
+
// RTL. The half-width-difference centering shift (6.5px) is inline, so it
|
|
278
|
+
// flips physical sign under RTL: `- 6.5px` (LTR) vs `+ 6.5px` (RTL). See the
|
|
279
|
+
// Playwright measurement (hitArea.center === pill.center, 0px offset in both
|
|
280
|
+
// directions) for the geometric proof; here we lock the transform shape.
|
|
281
|
+
render(<Harness handleProps={{pillPlacement: 'start'}} />);
|
|
282
|
+
const hitArea = getSeparator().firstElementChild as HTMLElement;
|
|
283
|
+
expect(hitArea.className).toContain('hitAreaOffsetX');
|
|
284
|
+
const style = hitArea.getAttribute('style') ?? '';
|
|
285
|
+
// LTR (default) branch subtracts the centering shift; RTL branch adds it.
|
|
286
|
+
expect(style).toContain('- 6.5px');
|
|
287
|
+
expect(style).toContain('+ 6.5px');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('centers the grab zone on the divider when the pill is centered (no bias)', () => {
|
|
291
|
+
render(<Harness handleProps={{pillPlacement: 'center'}} />);
|
|
292
|
+
const hitArea = getSeparator().firstElementChild as HTMLElement;
|
|
293
|
+
// No physical offset — a plain logical centering class, no biased transform.
|
|
294
|
+
expect(hitArea.className).toContain('hitAreaCenteredX');
|
|
295
|
+
expect(hitArea.className).not.toContain('hitAreaOffsetX');
|
|
296
|
+
});
|
|
297
|
+
|
|
204
298
|
// --- Prop composition (ordering choice: handler sits after {...props}) ---
|
|
205
299
|
|
|
206
300
|
it('runs a consumer onKeyDown alongside keyboard resizing', () => {
|
|
@@ -56,14 +56,27 @@ function resolveEffectiveSide(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Hit
|
|
60
|
-
*
|
|
59
|
+
* Hit-area inline bias, expressed as a fraction of the pill's per-side offset.
|
|
60
|
+
*
|
|
61
|
+
* The visible pill is placed by `pillOffsetX`/`pillOffsetY`: anchored at the
|
|
62
|
+
* divider's inline-start edge and pushed one grip-width-plus-gap toward the
|
|
63
|
+
* panel side via a PHYSICAL translate (dir −1 = start, +1 = end). Because that
|
|
64
|
+
* offset is physical, the pill sits on the same physical side of the divider in
|
|
65
|
+
* both LTR and RTL. The grab zone must sit OVER that pill, so it has to use the
|
|
66
|
+
* exact same anchor + physical-offset construction as the pill (just wider) —
|
|
67
|
+
* anchoring the hit area at the divider centre (`insetInlineStart: 50%`) and
|
|
68
|
+
* biasing with a percentage translate mixes a logical anchor (which flips under
|
|
69
|
+
* RTL) with a physical translate (which doesn't), leaving the grab zone stranded
|
|
70
|
+
* to one side under RTL. Returning the pill's `dir` here lets the hit area reuse
|
|
71
|
+
* `pillOffsetX`/`pillOffsetY` so the two elements are positioned identically.
|
|
61
72
|
*/
|
|
62
|
-
function
|
|
73
|
+
function hitAreaBiasDir(
|
|
74
|
+
effectiveSide: 'start' | 'end' | 'center',
|
|
75
|
+
): number | null {
|
|
63
76
|
if (effectiveSide === 'center') {
|
|
64
|
-
return
|
|
77
|
+
return null;
|
|
65
78
|
}
|
|
66
|
-
return effectiveSide === 'start' ?
|
|
79
|
+
return effectiveSide === 'start' ? -1 : 1;
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
const styles = stylex.create({
|
|
@@ -102,8 +115,8 @@ const styles = stylex.create({
|
|
|
102
115
|
},
|
|
103
116
|
overlayVertical: {
|
|
104
117
|
insetBlockEnd: 0,
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
insetInlineStart: 0,
|
|
119
|
+
insetInlineEnd: 0,
|
|
107
120
|
height: 'var(--resize-handle-hit-area, 16px)',
|
|
108
121
|
},
|
|
109
122
|
horizontal: {
|
|
@@ -145,16 +158,24 @@ const styles = stylex.create({
|
|
|
145
158
|
width: spacingVars['--spacing-4'],
|
|
146
159
|
top: 0,
|
|
147
160
|
bottom: 0,
|
|
148
|
-
left: '50%',
|
|
149
161
|
cursor: 'col-resize',
|
|
150
162
|
},
|
|
151
163
|
hitAreaVertical: {
|
|
152
164
|
height: spacingVars['--spacing-4'],
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
top: '50%',
|
|
165
|
+
insetInlineStart: 0,
|
|
166
|
+
insetInlineEnd: 0,
|
|
156
167
|
cursor: 'row-resize',
|
|
157
168
|
},
|
|
169
|
+
// Centered grab zone (pillPlacement 'center' / no bias): sit the hit area on
|
|
170
|
+
// the divider itself. Anchored logically so it centers in both directions.
|
|
171
|
+
hitAreaCenteredX: {
|
|
172
|
+
insetInlineStart: '50%',
|
|
173
|
+
transform: 'translateX(-50%)',
|
|
174
|
+
},
|
|
175
|
+
hitAreaCenteredY: {
|
|
176
|
+
insetBlockStart: '50%',
|
|
177
|
+
transform: 'translateY(-50%)',
|
|
178
|
+
},
|
|
158
179
|
|
|
159
180
|
// Pill base — themes target .astryx-resize-handle-pill for size/shape.
|
|
160
181
|
pill: {
|
|
@@ -167,7 +188,7 @@ const styles = stylex.create({
|
|
|
167
188
|
transitionDuration: durationVars['--duration-fast'],
|
|
168
189
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
169
190
|
top: '50%',
|
|
170
|
-
|
|
191
|
+
insetInlineStart: '50%',
|
|
171
192
|
transform: 'translate(-50%, -50%)',
|
|
172
193
|
},
|
|
173
194
|
pillHorizontal: {
|
|
@@ -193,14 +214,32 @@ const styles = stylex.create({
|
|
|
193
214
|
// Dynamic styles — avoids inline style overrides.
|
|
194
215
|
// Each axis gets its own function since StyleX requires static structure.
|
|
195
216
|
const dynamicStyles = stylex.create({
|
|
196
|
-
|
|
197
|
-
|
|
217
|
+
// Hit-area inline offset — mirrors the pill's `pillOffsetX`/`pillOffsetY`
|
|
218
|
+
// construction so the grab zone sits centred on the visible pill in BOTH
|
|
219
|
+
// directions. Both anchor at the divider's inline-start edge
|
|
220
|
+
// (`insetInlineStart: 0`) and travel toward the panel side with the SAME
|
|
221
|
+
// physical translate `dir * (gripWidth + gap)`, so they share the pill's
|
|
222
|
+
// near edge. The hit area is wider, so it also shifts by half the width
|
|
223
|
+
// difference `(16px − 3px) / 2 = 6.5px` to align the two CENTRES. That
|
|
224
|
+
// centring shift is along the inline axis (a box grows from its inline-start
|
|
225
|
+
// edge toward inline-end), so it must flip physical sign under RTL — unlike
|
|
226
|
+
// the `dir * (...)` travel, which is physical and identical in both
|
|
227
|
+
// directions because the pill's own offset is physical. Mixing the previous
|
|
228
|
+
// divider-relative `50%` anchor with a percentage translate stranded the grab
|
|
229
|
+
// zone to one side under RTL; this construction keeps it on the pill.
|
|
230
|
+
hitAreaOffsetX: (dir: number) => ({
|
|
231
|
+
insetInlineStart: 0,
|
|
232
|
+
transform: {
|
|
233
|
+
default: `translate(calc(${dir} * (3px + ${spacingVars['--spacing-1']}) - 6.5px), -50%)`,
|
|
234
|
+
':is([dir="rtl"] *)': `translate(calc(${dir} * (3px + ${spacingVars['--spacing-1']}) + 6.5px), -50%)`,
|
|
235
|
+
},
|
|
198
236
|
}),
|
|
199
|
-
|
|
200
|
-
|
|
237
|
+
hitAreaOffsetY: (dir: number) => ({
|
|
238
|
+
insetBlockStart: 0,
|
|
239
|
+
transform: `translate(-50%, calc(${dir} * (3px + ${spacingVars['--spacing-1']}) - 6.5px))`,
|
|
201
240
|
}),
|
|
202
241
|
pillOffsetX: (dir: number) => ({
|
|
203
|
-
|
|
242
|
+
insetInlineStart: 0,
|
|
204
243
|
transform: `translate(calc(${dir} * (100% + ${spacingVars['--spacing-1']})), -50%)`,
|
|
205
244
|
}),
|
|
206
245
|
// Vertical offset: no rotation — use explicit landscape dimensions.
|
|
@@ -333,6 +372,9 @@ export function ResizeHandle({
|
|
|
333
372
|
isReversed,
|
|
334
373
|
resizable?._isCollapsed ?? false,
|
|
335
374
|
);
|
|
375
|
+
// Physical offset direction shared by the pill and the grab zone (null =
|
|
376
|
+
// centered / no bias). Keeps the two positioned identically in LTR and RTL.
|
|
377
|
+
const hitBiasDir = hitAreaBiasDir(effectiveSide);
|
|
336
378
|
|
|
337
379
|
const getRTLMultiplier = useCallback((): number => {
|
|
338
380
|
const el = handleRef.current;
|
|
@@ -538,9 +580,17 @@ export function ResizeHandle({
|
|
|
538
580
|
{...stylex.props(
|
|
539
581
|
styles.hitArea,
|
|
540
582
|
isHorizontal ? styles.hitAreaHorizontal : styles.hitAreaVertical,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
583
|
+
// Bias the grab zone to sit over the visible pill. When the pill is
|
|
584
|
+
// centered (no bias) just center the hit area on the divider; when
|
|
585
|
+
// it's offset to a panel side, reuse the pill's physical-offset
|
|
586
|
+
// construction so the two stay aligned in LTR and RTL alike.
|
|
587
|
+
hitBiasDir == null
|
|
588
|
+
? isHorizontal
|
|
589
|
+
? styles.hitAreaCenteredX
|
|
590
|
+
: styles.hitAreaCenteredY
|
|
591
|
+
: isHorizontal
|
|
592
|
+
? dynamicStyles.hitAreaOffsetX(hitBiasDir)
|
|
593
|
+
: dynamicStyles.hitAreaOffsetY(hitBiasDir),
|
|
544
594
|
isDisabled && styles.disabled,
|
|
545
595
|
)}
|
|
546
596
|
onPointerDown={handlePointerDown}
|
|
@@ -169,6 +169,7 @@ export function SegmentedControlItem({
|
|
|
169
169
|
icon,
|
|
170
170
|
isDisabled = false,
|
|
171
171
|
onClick: onClickProp,
|
|
172
|
+
xstyle,
|
|
172
173
|
...rest
|
|
173
174
|
}: SegmentedControlItemProps) {
|
|
174
175
|
const ctx = useSegmentedControlContext();
|
|
@@ -228,6 +229,7 @@ export function SegmentedControlItem({
|
|
|
228
229
|
isSelected && styles.selected,
|
|
229
230
|
!isSelected && !isItemDisabled && styles.hover,
|
|
230
231
|
isItemDisabled && styles.disabled,
|
|
232
|
+
xstyle,
|
|
231
233
|
),
|
|
232
234
|
)}>
|
|
233
235
|
{iconElement}
|
|
@@ -23,6 +23,8 @@ export const docs = {
|
|
|
23
23
|
targets: [
|
|
24
24
|
{className: 'astryx-selector', visualProps: ['size', 'status']},
|
|
25
25
|
{className: 'astryx-selector-option'},
|
|
26
|
+
{className: 'astryx-selector-clear-icon'},
|
|
27
|
+
{className: 'astryx-selector-indicator-icon', states: ['state']},
|
|
26
28
|
],
|
|
27
29
|
},
|
|
28
30
|
description: 'Dropdown selector for choosing from a list of options.',
|
|
@@ -141,6 +143,12 @@ export const docs = {
|
|
|
141
143
|
description:
|
|
142
144
|
'Custom render function for each selectable option in the dropdown. Use this instead of JSX children; dividers and sections are rendered by the selector.',
|
|
143
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: 'width',
|
|
148
|
+
type: 'SizeValue',
|
|
149
|
+
description:
|
|
150
|
+
'Width of the field (number = pixels, string used as-is, e.g. "100%"). Sizes the whole field (label, control, and status) so they stay aligned.',
|
|
151
|
+
},
|
|
144
152
|
{
|
|
145
153
|
name: 'xstyle',
|
|
146
154
|
type: 'StyleXStyles',
|
|
@@ -20,8 +20,11 @@ import {
|
|
|
20
20
|
import userEvent from '@testing-library/user-event';
|
|
21
21
|
import {Selector} from './Selector';
|
|
22
22
|
import {SelectorOption} from './SelectorOption';
|
|
23
|
+
import {Icon} from '../Icon';
|
|
23
24
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
24
25
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
26
|
+
import {defineTheme} from '../theme/defineTheme';
|
|
27
|
+
import {generateThemeCSSFlat} from '../theme/generateThemeRules';
|
|
25
28
|
|
|
26
29
|
function politeRegion(): HTMLElement | null {
|
|
27
30
|
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
@@ -746,6 +749,119 @@ describe('Selector', () => {
|
|
|
746
749
|
expect(politeRegion()?.textContent ?? '').toBe('');
|
|
747
750
|
});
|
|
748
751
|
});
|
|
752
|
+
|
|
753
|
+
describe('grouped search', () => {
|
|
754
|
+
const GROUPED = [
|
|
755
|
+
{
|
|
756
|
+
type: 'section' as const,
|
|
757
|
+
title: 'Citrus',
|
|
758
|
+
options: [
|
|
759
|
+
{value: 'orange', label: 'Orange'},
|
|
760
|
+
{value: 'lemon', label: 'Lemon'},
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
type: 'section' as const,
|
|
765
|
+
title: 'Berries',
|
|
766
|
+
options: [
|
|
767
|
+
{value: 'strawberry', label: 'Strawberry'},
|
|
768
|
+
{value: 'blueberry', label: 'Blueberry'},
|
|
769
|
+
],
|
|
770
|
+
},
|
|
771
|
+
];
|
|
772
|
+
|
|
773
|
+
it('keeps the group header above matching items while searching', async () => {
|
|
774
|
+
const user = userEvent.setup();
|
|
775
|
+
render(
|
|
776
|
+
<Selector
|
|
777
|
+
label="Fruit"
|
|
778
|
+
options={GROUPED}
|
|
779
|
+
onChange={() => {}}
|
|
780
|
+
hasSearch
|
|
781
|
+
/>,
|
|
782
|
+
);
|
|
783
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
784
|
+
// "orange" only matches within the Citrus group.
|
|
785
|
+
await user.type(screen.getByRole('combobox', h), 'orange');
|
|
786
|
+
|
|
787
|
+
expect(
|
|
788
|
+
screen.getByRole('group', {name: 'Citrus', ...h}),
|
|
789
|
+
).toBeInTheDocument();
|
|
790
|
+
const options = screen.getAllByRole('option', h);
|
|
791
|
+
expect(options).toHaveLength(1);
|
|
792
|
+
expect(options[0]).toHaveTextContent('Orange');
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('hides a group whose items have no match', async () => {
|
|
796
|
+
const user = userEvent.setup();
|
|
797
|
+
render(
|
|
798
|
+
<Selector
|
|
799
|
+
label="Fruit"
|
|
800
|
+
options={GROUPED}
|
|
801
|
+
onChange={() => {}}
|
|
802
|
+
hasSearch
|
|
803
|
+
/>,
|
|
804
|
+
);
|
|
805
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
806
|
+
// "berry" only matches items inside the Berries group.
|
|
807
|
+
await user.type(screen.getByRole('combobox', h), 'berry');
|
|
808
|
+
|
|
809
|
+
expect(
|
|
810
|
+
screen.getByRole('group', {name: 'Berries', ...h}),
|
|
811
|
+
).toBeInTheDocument();
|
|
812
|
+
expect(
|
|
813
|
+
screen.queryByRole('group', {name: 'Citrus', ...h}),
|
|
814
|
+
).not.toBeInTheDocument();
|
|
815
|
+
expect(screen.getAllByRole('option', h)).toHaveLength(2);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('lands keyboard focus on the correct option after filtering', async () => {
|
|
819
|
+
const user = userEvent.setup();
|
|
820
|
+
render(
|
|
821
|
+
<Selector
|
|
822
|
+
label="Fruit"
|
|
823
|
+
options={GROUPED}
|
|
824
|
+
onChange={() => {}}
|
|
825
|
+
hasSearch
|
|
826
|
+
/>,
|
|
827
|
+
);
|
|
828
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
829
|
+
const search = screen.getByRole('combobox', h);
|
|
830
|
+
// "berry" leaves Strawberry, Blueberry (in that document order).
|
|
831
|
+
await user.type(search, 'berry');
|
|
832
|
+
const options = screen.getAllByRole('option', h);
|
|
833
|
+
expect(options.map(o => o.textContent)).toEqual([
|
|
834
|
+
'Strawberry',
|
|
835
|
+
'Blueberry',
|
|
836
|
+
]);
|
|
837
|
+
await user.keyboard('{ArrowDown}');
|
|
838
|
+
expect(search).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
839
|
+
await user.keyboard('{ArrowDown}');
|
|
840
|
+
expect(search).toHaveAttribute('aria-activedescendant', options[1].id);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('shows the empty state when no group has a match', async () => {
|
|
844
|
+
const user = userEvent.setup();
|
|
845
|
+
render(
|
|
846
|
+
<Selector
|
|
847
|
+
label="Fruit"
|
|
848
|
+
options={GROUPED}
|
|
849
|
+
onChange={() => {}}
|
|
850
|
+
hasSearch
|
|
851
|
+
/>,
|
|
852
|
+
);
|
|
853
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
854
|
+
await user.type(screen.getByRole('combobox', h), 'zzz');
|
|
855
|
+
expect(screen.queryAllByRole('option', h)).toHaveLength(0);
|
|
856
|
+
expect(
|
|
857
|
+
screen.queryByRole('group', {name: 'Citrus', ...h}),
|
|
858
|
+
).not.toBeInTheDocument();
|
|
859
|
+
const listbox = screen.getByRole('listbox', h);
|
|
860
|
+
expect(
|
|
861
|
+
within(listbox).getByText('No results found'),
|
|
862
|
+
).toBeInTheDocument();
|
|
863
|
+
});
|
|
864
|
+
});
|
|
749
865
|
});
|
|
750
866
|
|
|
751
867
|
describe('keyboard accessibility', () => {
|
|
@@ -1074,11 +1190,14 @@ describe('Selector', () => {
|
|
|
1074
1190
|
});
|
|
1075
1191
|
});
|
|
1076
1192
|
|
|
1077
|
-
|
|
1078
1193
|
describe('Selector statusVariant forwarding', () => {
|
|
1079
1194
|
it('defaults to attached (status renders with data-variant="attached")', () => {
|
|
1080
1195
|
const {container} = render(
|
|
1081
|
-
<Selector
|
|
1196
|
+
<Selector
|
|
1197
|
+
label="Fruit"
|
|
1198
|
+
options={['Apple', 'Banana']}
|
|
1199
|
+
status={{type: 'error', message: 'Required'}}
|
|
1200
|
+
/>,
|
|
1082
1201
|
);
|
|
1083
1202
|
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
1084
1203
|
'data-variant',
|
|
@@ -1088,7 +1207,12 @@ describe('Selector statusVariant forwarding', () => {
|
|
|
1088
1207
|
|
|
1089
1208
|
it('forwards statusVariant="detached" to the underlying Field status', () => {
|
|
1090
1209
|
const {container} = render(
|
|
1091
|
-
<Selector
|
|
1210
|
+
<Selector
|
|
1211
|
+
label="Fruit"
|
|
1212
|
+
options={['Apple', 'Banana']}
|
|
1213
|
+
status={{type: 'error', message: 'Required'}}
|
|
1214
|
+
statusVariant="detached"
|
|
1215
|
+
/>,
|
|
1092
1216
|
);
|
|
1093
1217
|
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
1094
1218
|
'data-variant',
|
|
@@ -1096,3 +1220,204 @@ describe('Selector statusVariant forwarding', () => {
|
|
|
1096
1220
|
);
|
|
1097
1221
|
});
|
|
1098
1222
|
});
|
|
1223
|
+
|
|
1224
|
+
describe('Selector clear icon theme target', () => {
|
|
1225
|
+
// Resolve the clear glyph span (the astryx-icon element inside the clear
|
|
1226
|
+
// button), independent of the theme target class.
|
|
1227
|
+
const getClearIcon = (): HTMLElement => {
|
|
1228
|
+
const button = screen.getByRole('button', {name: 'Clear Fruit'});
|
|
1229
|
+
const icon = button.querySelector('.astryx-icon');
|
|
1230
|
+
if (icon == null) {
|
|
1231
|
+
throw new Error('clear icon not found');
|
|
1232
|
+
}
|
|
1233
|
+
return icon as HTMLElement;
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
it('renders the astryx-selector-clear-icon target on the clear glyph', () => {
|
|
1237
|
+
render(
|
|
1238
|
+
<Selector
|
|
1239
|
+
label="Fruit"
|
|
1240
|
+
options={OPTIONS}
|
|
1241
|
+
value="Banana"
|
|
1242
|
+
onChange={() => {}}
|
|
1243
|
+
hasClear
|
|
1244
|
+
/>,
|
|
1245
|
+
);
|
|
1246
|
+
// The stable theme target lands on the icon element itself (not the
|
|
1247
|
+
// button), so a theme can restyle just this glyph (color, size, hover)
|
|
1248
|
+
// via `defineTheme` — a button-level target could not reach the icon's
|
|
1249
|
+
// own color/size.
|
|
1250
|
+
const icon = getClearIcon();
|
|
1251
|
+
expect(icon).toHaveClass('astryx-selector-clear-icon');
|
|
1252
|
+
expect(icon).toHaveClass('astryx-icon');
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it('keeps the clear button functional alongside the target', () => {
|
|
1256
|
+
const onChange = vi.fn();
|
|
1257
|
+
render(
|
|
1258
|
+
<Selector
|
|
1259
|
+
label="Fruit"
|
|
1260
|
+
options={OPTIONS}
|
|
1261
|
+
value="Banana"
|
|
1262
|
+
onChange={onChange}
|
|
1263
|
+
hasClear
|
|
1264
|
+
/>,
|
|
1265
|
+
);
|
|
1266
|
+
const clear = screen.getByRole('button', {name: 'Clear Fruit'});
|
|
1267
|
+
expect(clear.tagName).toBe('BUTTON');
|
|
1268
|
+
fireEvent.click(clear);
|
|
1269
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
it('renders the default icon (secondary color, sm size) byte-identically', () => {
|
|
1273
|
+
// Pixel-identical default guard: the clear glyph must carry the exact same
|
|
1274
|
+
// StyleX color/size classes as a standalone secondary/sm icon. The added
|
|
1275
|
+
// target class is purely additive — it changes nothing until a theme
|
|
1276
|
+
// targets it.
|
|
1277
|
+
render(
|
|
1278
|
+
<Selector
|
|
1279
|
+
label="Fruit"
|
|
1280
|
+
options={OPTIONS}
|
|
1281
|
+
value="Banana"
|
|
1282
|
+
onChange={() => {}}
|
|
1283
|
+
hasClear
|
|
1284
|
+
/>,
|
|
1285
|
+
);
|
|
1286
|
+
const icon = getClearIcon();
|
|
1287
|
+
|
|
1288
|
+
const {container: refContainer} = render(
|
|
1289
|
+
<Icon icon="close" size="sm" color="secondary" />,
|
|
1290
|
+
);
|
|
1291
|
+
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
1292
|
+
|
|
1293
|
+
const styleClasses = (el: HTMLElement) =>
|
|
1294
|
+
el.className
|
|
1295
|
+
.split(' ')
|
|
1296
|
+
.filter(c => c !== 'astryx-selector-clear-icon')
|
|
1297
|
+
.sort();
|
|
1298
|
+
|
|
1299
|
+
expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it('exposes selector-clear-icon so a theme reaches the icon color, size, and hover', () => {
|
|
1303
|
+
// jsdom cannot resolve the @layer cascade, so the DOM-class assertion above
|
|
1304
|
+
// (target lands on the icon element) plus this generation assertion (the
|
|
1305
|
+
// theme emits same-element icon rules in @layer astryx-theme) together
|
|
1306
|
+
// prove the seam: a same-element theme rule wins over the icon's own
|
|
1307
|
+
// base-layer color/size.
|
|
1308
|
+
const theme = defineTheme({
|
|
1309
|
+
name: 'selector-clear-icon-test',
|
|
1310
|
+
components: {
|
|
1311
|
+
'selector-clear-icon': {
|
|
1312
|
+
base: {
|
|
1313
|
+
width: '12px',
|
|
1314
|
+
height: '12px',
|
|
1315
|
+
fontSize: '12px',
|
|
1316
|
+
color: 'var(--color-icon-secondary)',
|
|
1317
|
+
':hover': {color: 'var(--color-icon-primary)'},
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
const css = generateThemeCSSFlat(theme);
|
|
1323
|
+
expect(css).toContain('.astryx-selector-clear-icon {');
|
|
1324
|
+
expect(css).toContain('width: 12px');
|
|
1325
|
+
expect(css).toContain('height: 12px');
|
|
1326
|
+
expect(css).toContain('.astryx-selector-clear-icon:hover {');
|
|
1327
|
+
expect(css).toContain('color: var(--color-icon-primary)');
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
describe('Selector indicator (chevron) icon theme target', () => {
|
|
1332
|
+
const getIndicatorIcon = (container: HTMLElement): HTMLElement => {
|
|
1333
|
+
// The chevron is the only glyph carrying the indicator target class.
|
|
1334
|
+
const icon = container.querySelector('.astryx-selector-indicator-icon');
|
|
1335
|
+
if (icon == null) {
|
|
1336
|
+
throw new Error('indicator icon not found');
|
|
1337
|
+
}
|
|
1338
|
+
return icon as HTMLElement;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
it('renders the astryx-selector-indicator-icon target on the chevron glyph', () => {
|
|
1342
|
+
const {container} = render(
|
|
1343
|
+
<Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
|
|
1344
|
+
);
|
|
1345
|
+
// The stable theme target lands on the icon element itself (not the trigger
|
|
1346
|
+
// button), so a theme can restyle just this glyph (color, size, hover) —
|
|
1347
|
+
// and each open/closed state — via `defineTheme`. A button-level target
|
|
1348
|
+
// could not reach the icon's own color/size.
|
|
1349
|
+
const icon = getIndicatorIcon(container);
|
|
1350
|
+
expect(icon).toHaveClass('astryx-selector-indicator-icon');
|
|
1351
|
+
expect(icon).toHaveClass('astryx-icon');
|
|
1352
|
+
// Open/closed state is reflected so a theme can target each state alone.
|
|
1353
|
+
expect(icon).toHaveAttribute('data-state', 'collapsed');
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it('reflects the expanded state on the chevron when the popover is open', async () => {
|
|
1357
|
+
const user = userEvent.setup();
|
|
1358
|
+
const {container} = render(
|
|
1359
|
+
<Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
|
|
1360
|
+
);
|
|
1361
|
+
await user.click(screen.getByRole('combobox'));
|
|
1362
|
+
await waitFor(() => {
|
|
1363
|
+
expect(getIndicatorIcon(container)).toHaveAttribute(
|
|
1364
|
+
'data-state',
|
|
1365
|
+
'expanded',
|
|
1366
|
+
);
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
it('renders the default icon (inherit color, sm size) byte-identically', () => {
|
|
1371
|
+
// Pixel-identical default guard: the chevron glyph must carry the exact
|
|
1372
|
+
// same StyleX color/size classes as a standalone inherit/sm icon. The added
|
|
1373
|
+
// target class + data-state are purely additive — they change nothing until
|
|
1374
|
+
// a theme targets them.
|
|
1375
|
+
const {container} = render(
|
|
1376
|
+
<Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
|
|
1377
|
+
);
|
|
1378
|
+
const icon = getIndicatorIcon(container);
|
|
1379
|
+
|
|
1380
|
+
const {container: refContainer} = render(
|
|
1381
|
+
<Icon icon="chevronDown" size="sm" color="inherit" />,
|
|
1382
|
+
);
|
|
1383
|
+
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
1384
|
+
|
|
1385
|
+
// Exclude the additive theme-target classes (the stable target + its
|
|
1386
|
+
// reflected state class) so only the StyleX color/size classes remain.
|
|
1387
|
+
const themeTargetClasses = new Set([
|
|
1388
|
+
'astryx-selector-indicator-icon',
|
|
1389
|
+
'collapsed',
|
|
1390
|
+
'expanded',
|
|
1391
|
+
]);
|
|
1392
|
+
const styleClasses = (el: HTMLElement) =>
|
|
1393
|
+
el.className
|
|
1394
|
+
.split(' ')
|
|
1395
|
+
.filter(c => !themeTargetClasses.has(c))
|
|
1396
|
+
.sort();
|
|
1397
|
+
|
|
1398
|
+
expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
it('exposes selector-indicator-icon so a theme reaches the icon size and per-state color', () => {
|
|
1402
|
+
// jsdom cannot resolve the @layer cascade, so the DOM-class assertions
|
|
1403
|
+
// above (target lands on the icon element) plus this generation assertion
|
|
1404
|
+
// (the theme emits same-element icon rules in @layer astryx-theme) together
|
|
1405
|
+
// prove the seam: a same-element theme rule wins over the icon's own
|
|
1406
|
+
// base-layer color/size.
|
|
1407
|
+
const theme = defineTheme({
|
|
1408
|
+
name: 'selector-indicator-icon-test',
|
|
1409
|
+
components: {
|
|
1410
|
+
'selector-indicator-icon': {
|
|
1411
|
+
base: {width: '14px', height: '14px', fontSize: '14px'},
|
|
1412
|
+
'state:expanded': {color: 'var(--color-icon-primary)'},
|
|
1413
|
+
},
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
const css = generateThemeCSSFlat(theme);
|
|
1417
|
+
expect(css).toContain('.astryx-selector-indicator-icon {');
|
|
1418
|
+
expect(css).toContain('width: 14px');
|
|
1419
|
+
expect(css).toContain('height: 14px');
|
|
1420
|
+
expect(css).toContain('.astryx-selector-indicator-icon.expanded');
|
|
1421
|
+
expect(css).toContain('color: var(--color-icon-primary)');
|
|
1422
|
+
});
|
|
1423
|
+
});
|