@astryxdesign/core 0.3.0 → 0.4.0-canary.058f476
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 +156 -0
- package/dist/AlertDialog/AlertDialog.d.ts +10 -2
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +22 -3
- package/dist/AppShell/AppShell.d.ts +2 -2
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +40 -9
- package/dist/AspectRatio/AspectRatio.d.ts +13 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +10 -2
- package/dist/AspectRatio/index.d.ts.map +1 -1
- package/dist/AspectRatio/index.js +1 -2
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +33 -20
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +1 -2
- package/dist/Badge/index.d.ts.map +1 -1
- package/dist/Badge/index.js +1 -2
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +32 -17
- package/dist/Blockquote/Blockquote.d.ts.map +1 -1
- package/dist/Blockquote/Blockquote.js +1 -2
- package/dist/Blockquote/index.d.ts.map +1 -1
- package/dist/Blockquote/index.js +1 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +21 -8
- package/dist/Breadcrumbs/index.d.ts +2 -1
- package/dist/Breadcrumbs/index.d.ts.map +1 -1
- package/dist/Breadcrumbs/index.js +1 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +12 -16
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +2 -1
- package/dist/Calendar/styles.d.ts +0 -2
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +0 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +1 -2
- package/dist/Card/index.d.ts.map +1 -1
- package/dist/Card/index.js +1 -2
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +8 -12
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +1 -2
- package/dist/Center/index.d.ts.map +1 -1
- package/dist/Center/index.js +1 -2
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +2 -1
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +3 -1
- package/dist/Chat/ChatSendButton.d.ts.map +1 -1
- package/dist/Chat/ChatSendButton.js +4 -3
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +17 -17
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +2 -6
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +36 -125
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +3 -11
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +1 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +84 -67
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +35 -15
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +5 -0
- package/dist/CommandPalette/CommandPaletteItem.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +6 -3
- package/dist/CommandPalette/CommandPaletteList.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +32 -31
- package/dist/ContextMenu/ContextMenu.d.ts +2 -2
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +1 -1
- package/dist/ContextMenu/index.d.ts +2 -1
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +1 -0
- package/dist/DateInput/DateInput.d.ts +9 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +47 -33
- package/dist/DateRangeInput/DateRangeInput.d.ts +9 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +55 -32
- package/dist/DateTimeInput/DateTimeInput.d.ts +9 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +54 -30
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +4 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts +33 -8
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +62 -20
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +8 -11
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +32 -31
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts +25 -0
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuDivider.js +66 -0
- package/dist/DropdownMenu/DropdownMenuItem.d.ts +15 -2
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +20 -8
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +5 -5
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +24 -67
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -8
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +4 -0
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
- package/dist/DropdownMenu/renderDropdownItems.js +35 -27
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +9 -5
- package/dist/Field/InputClearButton.d.ts +16 -3
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +15 -4
- package/dist/Field/PanelSearchInput.d.ts +81 -0
- package/dist/Field/PanelSearchInput.d.ts.map +1 -0
- package/dist/Field/PanelSearchInput.js +151 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +21 -10
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +1 -2
- package/dist/Grid/index.d.ts.map +1 -1
- package/dist/Grid/index.js +1 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +3 -19
- package/dist/Icon/globalIconRegistry.d.ts +5 -4
- package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
- package/dist/Icon/globalIconRegistry.js +5 -4
- package/dist/Indicator/CheckIndicator.d.ts +25 -0
- package/dist/Indicator/CheckIndicator.d.ts.map +1 -0
- package/dist/Indicator/CheckIndicator.js +155 -0
- package/dist/Indicator/CheckboxIndicator.d.ts +22 -0
- package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -0
- package/dist/Indicator/CheckboxIndicator.js +173 -0
- package/dist/Indicator/RadioIndicator.d.ts +23 -0
- package/dist/Indicator/RadioIndicator.d.ts.map +1 -0
- package/dist/Indicator/RadioIndicator.js +137 -0
- package/dist/Indicator/index.d.ts +24 -0
- package/dist/Indicator/index.d.ts.map +1 -0
- package/dist/Indicator/index.js +26 -0
- package/dist/Indicator/indicator.markers.stylex.d.ts +16 -0
- package/dist/Indicator/indicator.markers.stylex.d.ts.map +1 -0
- package/dist/Indicator/indicator.markers.stylex.js +21 -0
- package/dist/Indicator/indicatorRegistry.d.ts +71 -0
- package/dist/Indicator/indicatorRegistry.d.ts.map +1 -0
- package/dist/Indicator/indicatorRegistry.js +93 -0
- package/dist/Indicator/types.d.ts +177 -0
- package/dist/Indicator/types.d.ts.map +1 -0
- package/dist/Indicator/types.js +1 -0
- package/dist/Indicator/useIndicator.d.ts +18 -0
- package/dist/Indicator/useIndicator.d.ts.map +1 -0
- package/dist/Indicator/useIndicator.js +30 -0
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +4 -8
- package/dist/Layer/useLayer.d.ts +14 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +25 -2
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +80 -103
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +4 -5
- package/dist/MobileNav/MobileNav.js +8 -8
- package/dist/MoreMenu/MoreMenu.d.ts +14 -1
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +4 -0
- package/dist/MultiSelector/MultiSelector.d.ts +8 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +205 -170
- package/dist/NumberInput/NumberInput.d.ts +27 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +230 -42
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +2 -3
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +61 -63
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +6 -8
- package/dist/Popover/usePopover.d.ts +15 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +14 -2
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +7 -0
- package/dist/ProgressBar/ProgressBar.d.ts +11 -4
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +95 -6
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +29 -84
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +2 -3
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +1 -2
- package/dist/Section/index.d.ts.map +1 -1
- package/dist/Section/index.js +1 -2
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +3 -11
- package/dist/Selector/Selector.d.ts +12 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +293 -183
- package/dist/Selector/hooks.d.ts +18 -6
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +57 -40
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +1 -1
- package/dist/SideNav/SideNavCollapseButton.d.ts +1 -1
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +29 -14
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +56 -20
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +28 -22
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -2
- package/dist/Skeleton/index.d.ts.map +1 -1
- package/dist/Skeleton/index.js +1 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +2 -6
- package/dist/Stack/stack.stylex.d.ts +3 -3
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +14 -13
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +2 -3
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +41 -19
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +8 -0
- package/dist/Table/TableRow.d.ts +1 -1
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +4 -2
- package/dist/Table/index.d.ts +1 -1
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +22 -18
- package/dist/Table/plugins/rowExpansion/index.d.ts +1 -1
- package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/index.js +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +41 -76
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +94 -345
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +23 -4
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +29 -33
- package/dist/Table/tableContextMenu.d.ts.map +1 -1
- package/dist/Table/tableContextMenu.js +2 -1
- package/dist/Table/types.d.ts +11 -0
- package/dist/Table/types.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 +16 -6
- package/dist/TextInput/TextInput.d.ts +10 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +13 -19
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +32 -26
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +14 -17
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +14 -3
- package/dist/Timestamp/TimestampHoverCard.d.ts.map +1 -1
- package/dist/Timestamp/TimestampHoverCard.js +16 -34
- package/dist/Timestamp/formatInstant.d.ts +11 -1
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +3 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +20 -11
- 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 +3 -19
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +6 -5
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +54 -16
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +3 -9
- package/dist/TopNav/TopNavMegaMenu.d.ts +7 -0
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +146 -60
- package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +22 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +83 -38
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +13 -0
- package/dist/TreeList/TreeListBranches.d.ts +1 -1
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +14 -4
- package/dist/TreeList/TreeListItem.d.ts +11 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -30
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +16 -8
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
- package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
- package/dist/VisuallyHidden/index.d.ts.map +1 -1
- package/dist/VisuallyHidden/index.js +1 -2
- package/dist/astryx.css +86 -96
- package/dist/astryx.umd.js +56 -56
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.stylex.d.ts +61 -0
- package/dist/hooks/containerReveal.stylex.d.ts.map +1 -0
- package/dist/hooks/containerReveal.stylex.js +63 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/useClipboard.d.ts +59 -0
- package/dist/hooks/useClipboard.d.ts.map +1 -0
- package/dist/hooks/useClipboard.js +106 -0
- package/dist/hooks/useContainerReveal.d.ts +14 -8
- package/dist/hooks/useContainerReveal.d.ts.map +1 -1
- package/dist/hooks/useContainerReveal.js +28 -69
- package/dist/hooks/useIndicatorFocusRing.d.ts +28 -0
- package/dist/hooks/useIndicatorFocusRing.d.ts.map +1 -0
- package/dist/hooks/useIndicatorFocusRing.js +96 -0
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +17 -4
- package/dist/hooks/useLongPress.d.ts.map +1 -1
- package/dist/hooks/useLongPress.js +12 -2
- package/dist/hooks/useScrollLock.d.ts.map +1 -1
- package/dist/hooks/useScrollLock.js +33 -18
- package/dist/hooks/useStreamingText.d.ts +18 -3
- package/dist/hooks/useStreamingText.d.ts.map +1 -1
- package/dist/hooks/useStreamingText.js +47 -7
- package/dist/hooks/useTreeFocus.d.ts.map +1 -1
- package/dist/hooks/useTreeFocus.js +9 -3
- package/dist/hooks/useTypeahead.d.ts +3 -2
- package/dist/hooks/useTypeahead.d.ts.map +1 -1
- package/dist/hooks/useTypeahead.js +19 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +18 -2
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +10 -1
- package/dist/theme/derivedVarRegistry.d.ts +8 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +14 -13
- package/dist/theme/expandColorScale.d.ts +3 -1
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +17 -7
- package/dist/theme/expandTypeScale.d.ts.map +1 -1
- package/dist/theme/expandTypeScale.js +17 -0
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +11 -0
- package/dist/theme/index.d.ts +2 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.stylex.d.ts +12 -0
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +23 -0
- package/dist/utils/focusOutline.stylex.d.ts +122 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -0
- package/dist/utils/focusOutline.stylex.js +143 -0
- package/dist/utils/focusReturn.d.ts +24 -0
- package/dist/utils/focusReturn.d.ts.map +1 -0
- package/dist/utils/focusReturn.js +29 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +6 -1
- package/dist/utils/interactionModality.d.ts +34 -0
- package/dist/utils/interactionModality.d.ts.map +1 -0
- package/dist/utils/interactionModality.js +70 -0
- package/dist/utils/mergeRefs.d.ts.map +1 -1
- package/dist/utils/mergeRefs.js +7 -5
- package/dist/utils/rtlStyles.d.ts +2 -2
- package/dist/utils/rtlStyles.js +2 -2
- package/dist/utils/themeProps.d.ts +20 -1
- package/dist/utils/themeProps.d.ts.map +1 -1
- package/dist/utils/themeProps.js +8 -2
- package/locales/en.json +52 -0
- package/locales/pseudo.json +39 -0
- package/package.json +8 -3
- package/src/AlertDialog/AlertDialog.doc.mjs +16 -3
- package/src/AlertDialog/AlertDialog.test.tsx +208 -1
- package/src/AlertDialog/AlertDialog.tsx +21 -3
- package/src/AppShell/AppShell.doc.mjs +14 -0
- package/src/AppShell/AppShell.test.tsx +139 -0
- package/src/AppShell/AppShell.tsx +32 -22
- package/src/AspectRatio/AspectRatio.doc.mjs +27 -3
- package/src/AspectRatio/AspectRatio.test.tsx +30 -0
- package/src/AspectRatio/AspectRatio.tsx +13 -2
- package/src/AspectRatio/index.ts +0 -2
- package/src/Avatar/Avatar.doc.mjs +2 -13
- package/src/Avatar/Avatar.test.tsx +33 -8
- package/src/Avatar/Avatar.tsx +52 -50
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -9
- package/src/Badge/Badge.tsx +0 -2
- package/src/Badge/index.ts +0 -2
- package/src/Banner/Banner.test.tsx +58 -1
- package/src/Banner/Banner.tsx +33 -11
- package/src/Blockquote/Blockquote.tsx +0 -2
- package/src/Blockquote/index.ts +0 -2
- package/src/Breadcrumbs/BreadcrumbItem.tsx +16 -6
- package/src/Breadcrumbs/index.ts +5 -1
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.tsx +22 -41
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/Calendar/styles.ts +0 -8
- package/src/Card/Card.tsx +0 -2
- package/src/Card/index.ts +0 -2
- package/src/Carousel/Carousel.doc.mjs +3 -0
- package/src/Carousel/Carousel.tsx +10 -6
- package/src/Center/Center.tsx +0 -2
- package/src/Center/index.ts +0 -2
- package/src/Chat/Chat.doc.mjs +2 -0
- package/src/Chat/ChatDictationButton.tsx +7 -1
- package/src/Chat/ChatLayoutScrollButton.test.tsx +22 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +8 -1
- package/src/Chat/ChatSendButton.tsx +2 -3
- package/src/Chat/ChatToolCalls.test.tsx +35 -0
- package/src/Chat/ChatToolCalls.tsx +36 -16
- package/src/Chat/useTriggerMenu.tsx +2 -5
- package/src/CheckboxInput/CheckboxInput.doc.mjs +4 -2
- package/src/CheckboxInput/CheckboxInput.test.tsx +120 -0
- package/src/CheckboxInput/CheckboxInput.tsx +45 -202
- package/src/ClickableCard/ClickableCard.tsx +6 -11
- package/src/Code/Code.tsx +0 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
- package/src/CodeBlock/CodeBlock.test.tsx +91 -0
- package/src/CodeBlock/CodeBlock.tsx +57 -80
- package/src/Collapsible/Collapsible.tsx +22 -18
- package/src/CommandPalette/CommandPalette.test.tsx +83 -0
- package/src/CommandPalette/CommandPalette.tsx +5 -0
- package/src/CommandPalette/CommandPaletteItem.tsx +7 -4
- package/src/CommandPalette/CommandPaletteList.tsx +2 -2
- package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
- package/src/ComplexSelector/ComplexSelector.test.tsx +73 -0
- package/src/ComplexSelector/ComplexSelector.tsx +41 -28
- package/src/ContextMenu/ContextMenu.doc.mjs +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/ContextMenu/ContextMenu.tsx +3 -3
- package/src/ContextMenu/index.ts +5 -1
- package/src/DateInput/DateInput.doc.mjs +18 -3
- package/src/DateInput/DateInput.test.tsx +112 -11
- package/src/DateInput/DateInput.tsx +44 -25
- package/src/DateRangeInput/DateRangeInput.doc.mjs +10 -2
- package/src/DateRangeInput/DateRangeInput.test.tsx +110 -14
- package/src/DateRangeInput/DateRangeInput.tsx +27 -21
- package/src/DateTimeInput/DateTimeInput.doc.mjs +16 -2
- package/src/DateTimeInput/DateTimeInput.test.tsx +154 -1
- package/src/DateTimeInput/DateTimeInput.tsx +58 -17
- package/src/Dialog/Dialog.tsx +2 -9
- package/src/DropdownMenu/DropdownMenu.doc.mjs +5 -9
- package/src/DropdownMenu/DropdownMenu.test.tsx +534 -4
- package/src/DropdownMenu/DropdownMenu.tsx +113 -52
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +27 -30
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +34 -0
- package/src/DropdownMenu/DropdownMenuDivider.tsx +81 -0
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +39 -4
- package/src/DropdownMenu/DropdownMenuItem.tsx +48 -10
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +30 -76
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +25 -19
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +44 -14
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +1 -5
- package/src/DropdownMenu/index.ts +8 -1
- package/src/DropdownMenu/renderDropdownItems.tsx +33 -34
- package/src/EmptyState/EmptyState.doc.mjs +4 -0
- package/src/EmptyState/EmptyState.test.tsx +62 -0
- package/src/EmptyState/EmptyState.tsx +16 -6
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/InputClearButton.test.tsx +117 -0
- package/src/Field/InputClearButton.tsx +30 -3
- package/src/Field/PanelSearchInput.tsx +271 -0
- package/src/FileInput/FileInput.tsx +21 -8
- package/src/Grid/Grid.tsx +0 -2
- package/src/Grid/index.ts +0 -2
- package/src/HoverCard/useHoverCard.tsx +2 -23
- package/src/Icon/Icon.doc.mjs +2 -1
- package/src/Icon/globalIconRegistry.tsx +5 -4
- package/src/Indicator/CheckIndicator.tsx +174 -0
- package/src/Indicator/CheckboxIndicator.tsx +257 -0
- package/src/Indicator/Indicator.doc.mjs +344 -0
- package/src/Indicator/Indicator.test.tsx +393 -0
- package/src/Indicator/RadioIndicator.tsx +206 -0
- package/src/Indicator/index.ts +46 -0
- package/src/Indicator/indicator.markers.stylex.ts +19 -0
- package/src/Indicator/indicatorRegistry.test.tsx +106 -0
- package/src/Indicator/indicatorRegistry.ts +120 -0
- package/src/Indicator/types.ts +197 -0
- package/src/Indicator/useIndicator.ts +39 -0
- package/src/Item/Item.doc.mjs +1 -1
- package/src/Item/Item.tsx +7 -14
- package/src/Layer/useLayer.doc.mjs +2 -2
- package/src/Layer/useLayer.test.tsx +85 -0
- package/src/Layer/useLayer.tsx +39 -1
- package/src/Lightbox/Lightbox.doc.mjs +1 -1
- package/src/Lightbox/Lightbox.tsx +45 -49
- package/src/Link/Link.tsx +4 -11
- package/src/MobileNav/MobileNav.tsx +3 -3
- package/src/MoreMenu/MoreMenu.doc.mjs +31 -0
- package/src/MoreMenu/MoreMenu.test.tsx +68 -1
- package/src/MoreMenu/MoreMenu.tsx +19 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +18 -1
- package/src/MultiSelector/MultiSelector.test.tsx +387 -18
- package/src/MultiSelector/MultiSelector.tsx +267 -220
- package/src/NumberInput/NumberInput.doc.mjs +265 -50
- package/src/NumberInput/NumberInput.test.tsx +726 -21
- package/src/NumberInput/NumberInput.tsx +353 -59
- package/src/Outline/Outline.tsx +2 -5
- package/src/Pagination/Pagination.doc.mjs +1 -1
- package/src/Pagination/Pagination.test.tsx +22 -7
- package/src/Pagination/Pagination.tsx +38 -35
- package/src/Popover/Popover.doc.mjs +2 -0
- package/src/Popover/Popover.tsx +6 -7
- package/src/Popover/usePopover.tsx +37 -4
- package/src/PowerSearch/PowerSearch.tsx +1 -1
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +42 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +7 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +27 -5
- package/src/ProgressBar/ProgressBar.test.tsx +224 -1
- package/src/ProgressBar/ProgressBar.tsx +126 -30
- package/src/RadioList/RadioList.doc.mjs +4 -2
- package/src/RadioList/RadioListItem.tsx +31 -130
- package/src/Resizable/ResizeHandle.tsx +2 -9
- package/src/Section/Section.tsx +0 -2
- package/src/Section/index.ts +0 -2
- package/src/SegmentedControl/SegmentedControlItem.tsx +2 -9
- package/src/SelectableCard/SelectableCard.tsx +4 -9
- package/src/Selector/Selector.doc.mjs +17 -4
- package/src/Selector/Selector.test.tsx +1394 -100
- package/src/Selector/Selector.tsx +355 -212
- package/src/Selector/hooks.ts +77 -50
- package/src/SideNav/SideNav.test.tsx +46 -0
- package/src/SideNav/SideNav.tsx +1 -0
- package/src/SideNav/SideNavCollapseButton.tsx +15 -10
- package/src/SideNav/SideNavHeading.tsx +48 -10
- package/src/SideNav/SideNavItem.tsx +20 -12
- package/src/Skeleton/Skeleton.tsx +0 -2
- package/src/Skeleton/index.ts +0 -2
- package/src/Slider/Slider.tsx +2 -11
- package/src/Stack/Stack.doc.mjs +110 -0
- package/src/StatusDot/StatusDot.doc.mjs +12 -3
- package/src/Switch/Switch.test.tsx +32 -0
- package/src/Switch/Switch.tsx +18 -10
- package/src/TabList/Tab.tsx +2 -9
- package/src/TabList/TabMenu.tsx +14 -21
- package/src/Table/BaseTable.tsx +11 -0
- package/src/Table/Table.test.tsx +31 -0
- package/src/Table/TableRow.tsx +6 -0
- package/src/Table/index.ts +1 -4
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +30 -14
- package/src/Table/plugins/rowExpansion/index.ts +1 -4
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +125 -175
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +115 -458
- package/src/Table/plugins/sortable/useTableSortable.tsx +2 -6
- package/src/Table/plugins/tree/useTableTreeData.tsx +43 -24
- package/src/Table/tableContextMenu.test.tsx +27 -0
- package/src/Table/tableContextMenu.tsx +1 -0
- package/src/Table/types.ts +11 -0
- package/src/Table/useTableGroupedRows.doc.mjs +1 -1
- package/src/Table/useTableRowExpansion.doc.mjs +43 -66
- package/src/TextArea/TextArea.doc.mjs +42 -2
- package/src/TextArea/TextArea.test.tsx +219 -0
- package/src/TextArea/TextArea.tsx +34 -7
- package/src/TextInput/TextInput.doc.mjs +18 -2
- package/src/TextInput/TextInput.test.tsx +189 -0
- package/src/TextInput/TextInput.tsx +25 -31
- package/src/Thumbnail/Thumbnail.tsx +2 -9
- package/src/TimeInput/TimeInput.doc.mjs +2 -2
- package/src/TimeInput/TimeInput.test.tsx +84 -3
- package/src/TimeInput/TimeInput.tsx +26 -28
- package/src/Timestamp/Timestamp.doc.mjs +4 -3
- package/src/Timestamp/Timestamp.test.tsx +91 -14
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/TimestampHoverCard.tsx +13 -35
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/Token/Token.tsx +5 -28
- package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
- package/src/Tokenizer/Tokenizer.test.tsx +45 -3
- package/src/Tokenizer/Tokenizer.tsx +10 -2
- package/src/Tooltip/useTooltip.tsx +2 -23
- package/src/TopNav/TopNav.tsx +1 -3
- package/src/TopNav/TopNavHeading.tsx +42 -13
- package/src/TopNav/TopNavItem.tsx +3 -21
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +433 -33
- package/src/TopNav/TopNavMegaMenu.tsx +118 -53
- package/src/TopNav/TopNavMegaMenuItem.tsx +2 -9
- package/src/TopNav/TopNavMenu.tsx +34 -37
- package/src/TreeList/TreeList.doc.mjs +5 -0
- package/src/TreeList/TreeList.test.tsx +208 -8
- package/src/TreeList/TreeList.tsx +22 -0
- package/src/TreeList/TreeListBranches.tsx +25 -2
- package/src/TreeList/TreeListItem.tsx +89 -52
- package/src/Typeahead/BaseTypeahead.tsx +19 -6
- package/src/Typeahead/Typeahead.doc.mjs +1 -0
- package/src/Typeahead/Typeahead.test.tsx +76 -3
- package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
- package/src/VisuallyHidden/index.ts +0 -2
- package/src/hooks/containerReveal.stylex.ts +102 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useClipboard.doc.mjs +87 -0
- package/src/hooks/useClipboard.test.tsx +151 -0
- package/src/hooks/useClipboard.ts +135 -0
- package/src/hooks/useContainerReveal.doc.mjs +7 -7
- package/src/hooks/useContainerReveal.test.tsx +42 -62
- package/src/hooks/useContainerReveal.ts +36 -91
- package/src/hooks/useImageMode.test.ts +1 -1
- package/src/hooks/useIndicatorFocusRing.tsx +120 -0
- package/src/hooks/useInputStatusIcon.tsx +8 -8
- package/src/hooks/useLongPress.test.tsx +18 -0
- package/src/hooks/useLongPress.ts +12 -2
- package/src/hooks/useScrollLock.test.ts +72 -0
- package/src/hooks/useScrollLock.ts +51 -19
- package/src/hooks/useStreamingText.test.ts +46 -1
- package/src/hooks/useStreamingText.ts +49 -7
- package/src/hooks/useTreeFocus.test.tsx +50 -0
- package/src/hooks/useTreeFocus.ts +14 -3
- package/src/hooks/useTypeahead.test.tsx +150 -0
- package/src/hooks/useTypeahead.ts +24 -9
- package/src/i18n/InternationalizationProvider.doc.mjs +7 -2
- package/src/index.ts +1 -0
- package/src/serverSafeComponents.test.ts +665 -0
- package/src/theme/defineTheme.ts +27 -0
- package/src/theme/derivedVarRegistry.test.ts +21 -1
- package/src/theme/derivedVarRegistry.ts +19 -6
- package/src/theme/expandColorScale.test.ts +14 -0
- package/src/theme/expandColorScale.ts +24 -9
- package/src/theme/expandTypeScale.test.ts +43 -0
- package/src/theme/expandTypeScale.ts +17 -0
- package/src/theme/generateThemeRules.test.ts +78 -20
- package/src/theme/generateThemeRules.ts +11 -0
- package/src/theme/index.ts +15 -0
- package/src/theme/tokens.stylex.ts +18 -0
- package/src/theme/tokens.test.ts +37 -0
- package/src/utils/focusOutline.stylex.ts +177 -0
- package/src/utils/focusReturn.test.ts +42 -0
- package/src/utils/focusReturn.ts +31 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/interactionModality.ts +74 -0
- package/src/utils/mergeRefs.test.ts +48 -0
- package/src/utils/mergeRefs.ts +12 -6
- package/src/utils/rtlStyles.ts +2 -2
- package/src/utils/themeProps.ts +26 -4
- package/dist/CheckboxInput/checkbox.markers.stylex.d.ts +0 -9
- package/dist/CheckboxInput/checkbox.markers.stylex.d.ts.map +0 -1
- package/dist/CheckboxInput/checkbox.markers.stylex.js +0 -14
- package/dist/RadioList/radio.markers.stylex.d.ts +0 -7
- package/dist/RadioList/radio.markers.stylex.d.ts.map +0 -1
- package/dist/RadioList/radio.markers.stylex.js +0 -12
- package/dist/hooks/containerReveal.pool.stylex.d.ts +0 -303
- package/dist/hooks/containerReveal.pool.stylex.d.ts.map +0 -1
- package/dist/hooks/containerReveal.pool.stylex.js +0 -292
- package/src/CheckboxInput/checkbox.markers.stylex.ts +0 -12
- package/src/RadioList/radio.markers.stylex.ts +0 -10
- package/src/hooks/containerReveal.pool.stylex.ts +0 -571
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Selector.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, @testing-library/user-event
|
|
6
|
-
* @output Unit tests for Selector
|
|
6
|
+
* @output Unit tests for Selector behavior and selected-item geometry
|
|
7
7
|
* @position Tests; validates Selector behavior
|
|
8
8
|
*
|
|
9
9
|
* SYNC: When Selector.tsx API changes, update these tests.
|
|
@@ -18,24 +18,28 @@ import {
|
|
|
18
18
|
within,
|
|
19
19
|
} from '@testing-library/react';
|
|
20
20
|
import userEvent from '@testing-library/user-event';
|
|
21
|
+
import {useState} from 'react';
|
|
21
22
|
import {Selector} from './Selector';
|
|
22
23
|
import {SelectorOption} from './SelectorOption';
|
|
23
24
|
import {Icon} from '../Icon';
|
|
25
|
+
import {RadioIndicator} from '../Indicator';
|
|
24
26
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
25
27
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
28
|
+
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
26
29
|
import {defineTheme} from '../theme/defineTheme';
|
|
30
|
+
import {Theme} from '../theme/Theme';
|
|
27
31
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
32
|
+
import {spacingVars} from '../theme/tokens.stylex';
|
|
28
33
|
|
|
29
34
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
30
35
|
const {prose, component} = generateThemeCSS(theme);
|
|
31
36
|
return [prose, component].filter(Boolean).join('\n\n');
|
|
32
37
|
}
|
|
33
|
-
function politeRegion(): HTMLElement | null {
|
|
34
|
-
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
35
|
-
}
|
|
36
38
|
|
|
37
39
|
// Mock showPopover and hidePopover methods since they're not implemented in jsdom
|
|
38
40
|
beforeEach(() => {
|
|
41
|
+
// The live regions are a document-level singleton; start each test clean.
|
|
42
|
+
__resetLiveRegionsForTest();
|
|
39
43
|
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
40
44
|
this.setAttribute('popover-open', '');
|
|
41
45
|
const event = new Event('toggle', {bubbles: false});
|
|
@@ -70,6 +74,23 @@ const h = {hidden: true} as const;
|
|
|
70
74
|
|
|
71
75
|
const OPTIONS = ['Apple', 'Banana', 'Cherry'];
|
|
72
76
|
|
|
77
|
+
// Mirrors useTypeahead's default resetMs — how long the typed buffer survives.
|
|
78
|
+
const TYPEAHEAD_RESET_MS = 750;
|
|
79
|
+
|
|
80
|
+
const politeRegion = () =>
|
|
81
|
+
document.querySelector('[data-astryx-live-region="polite"]');
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type onto an element with no awaits between keystrokes. Typeahead only
|
|
85
|
+
* accumulates while keys land inside the reset window, so an awaited
|
|
86
|
+
* `user.keyboard` per character would put CI stalls on the critical path.
|
|
87
|
+
*/
|
|
88
|
+
function type(text: string, element: HTMLElement) {
|
|
89
|
+
for (const key of text) {
|
|
90
|
+
fireEvent.keyDown(element, {key});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
function rect({
|
|
74
95
|
top,
|
|
75
96
|
bottom,
|
|
@@ -98,36 +119,103 @@ function rect({
|
|
|
98
119
|
};
|
|
99
120
|
}
|
|
100
121
|
|
|
101
|
-
function mockSelectorRects(
|
|
122
|
+
function mockSelectorRects({
|
|
123
|
+
anchor = rect({top: 160, bottom: 190, height: 30}),
|
|
124
|
+
trigger = rect({top: 160, bottom: 190, height: 30}),
|
|
125
|
+
listbox = rect({top: 190, bottom: 310, height: 120}),
|
|
126
|
+
selectedItem = rect({top: 220, bottom: 250, height: 30}),
|
|
127
|
+
listboxLayoutHeight = listbox.height,
|
|
128
|
+
selectedItemLayoutTop = selectedItem.top - listbox.top,
|
|
129
|
+
selectedItemLayoutHeight = selectedItem.height,
|
|
130
|
+
viewportHeight = 200,
|
|
131
|
+
}: {
|
|
132
|
+
anchor?: DOMRect;
|
|
133
|
+
trigger?: DOMRect;
|
|
134
|
+
listbox?: DOMRect;
|
|
135
|
+
selectedItem?: DOMRect;
|
|
136
|
+
listboxLayoutHeight?: number;
|
|
137
|
+
selectedItemLayoutTop?: number;
|
|
138
|
+
selectedItemLayoutHeight?: number;
|
|
139
|
+
viewportHeight?: number;
|
|
140
|
+
} = {}) {
|
|
102
141
|
const originalGetBoundingClientRect =
|
|
103
142
|
HTMLElement.prototype.getBoundingClientRect;
|
|
104
143
|
const originalInnerHeight = Object.getOwnPropertyDescriptor(
|
|
105
144
|
window,
|
|
106
145
|
'innerHeight',
|
|
107
146
|
);
|
|
147
|
+
const originalOffsetTop = Object.getOwnPropertyDescriptor(
|
|
148
|
+
HTMLElement.prototype,
|
|
149
|
+
'offsetTop',
|
|
150
|
+
);
|
|
151
|
+
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
|
|
152
|
+
HTMLElement.prototype,
|
|
153
|
+
'offsetHeight',
|
|
154
|
+
);
|
|
108
155
|
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
156
|
+
if (this.classList.contains('astryx-selector')) {
|
|
157
|
+
return anchor;
|
|
158
|
+
}
|
|
109
159
|
// The trigger is role="combobox" by default, or a plain button with
|
|
110
160
|
// aria-haspopup="listbox" in hasSearch mode — match either.
|
|
111
161
|
if (
|
|
112
162
|
this.getAttribute('role') === 'combobox' ||
|
|
113
163
|
this.getAttribute('aria-haspopup') === 'listbox'
|
|
114
164
|
) {
|
|
115
|
-
return
|
|
165
|
+
return trigger;
|
|
116
166
|
}
|
|
117
167
|
if (this.getAttribute('role') === 'listbox') {
|
|
118
|
-
return
|
|
168
|
+
return listbox;
|
|
119
169
|
}
|
|
120
170
|
if (this.id.endsWith('-item-1')) {
|
|
121
|
-
return
|
|
171
|
+
return selectedItem;
|
|
122
172
|
}
|
|
123
173
|
return originalGetBoundingClientRect.call(this);
|
|
124
174
|
};
|
|
175
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetTop', {
|
|
176
|
+
configurable: true,
|
|
177
|
+
get() {
|
|
178
|
+
if (this.getAttribute('role') === 'listbox') {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
if (this.id.endsWith('-item-1')) {
|
|
182
|
+
return selectedItemLayoutTop;
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
|
188
|
+
configurable: true,
|
|
189
|
+
get() {
|
|
190
|
+
if (this.getAttribute('role') === 'listbox') {
|
|
191
|
+
return listboxLayoutHeight;
|
|
192
|
+
}
|
|
193
|
+
if (this.id.endsWith('-item-1')) {
|
|
194
|
+
return selectedItemLayoutHeight;
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
},
|
|
198
|
+
});
|
|
125
199
|
Object.defineProperty(window, 'innerHeight', {
|
|
126
|
-
value:
|
|
200
|
+
value: viewportHeight,
|
|
127
201
|
configurable: true,
|
|
128
202
|
});
|
|
129
203
|
return () => {
|
|
130
204
|
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
205
|
+
if (originalOffsetTop) {
|
|
206
|
+
Object.defineProperty(
|
|
207
|
+
HTMLElement.prototype,
|
|
208
|
+
'offsetTop',
|
|
209
|
+
originalOffsetTop,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (originalOffsetHeight) {
|
|
213
|
+
Object.defineProperty(
|
|
214
|
+
HTMLElement.prototype,
|
|
215
|
+
'offsetHeight',
|
|
216
|
+
originalOffsetHeight,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
131
219
|
if (originalInnerHeight) {
|
|
132
220
|
Object.defineProperty(window, 'innerHeight', originalInnerHeight);
|
|
133
221
|
}
|
|
@@ -152,6 +240,60 @@ describe('Selector', () => {
|
|
|
152
240
|
expect(screen.getByRole('combobox')).toHaveTextContent('Banana');
|
|
153
241
|
});
|
|
154
242
|
|
|
243
|
+
it('draws the selected mark through the check indicator', () => {
|
|
244
|
+
render(
|
|
245
|
+
<Selector
|
|
246
|
+
label="Fruit"
|
|
247
|
+
options={OPTIONS}
|
|
248
|
+
value="Banana"
|
|
249
|
+
onChange={() => {}}
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const selected = screen.getByRole('option', {name: /Banana/, hidden: true});
|
|
254
|
+
// The default check indicator IS the glyph — no wrapper element, so the
|
|
255
|
+
// host's theme target sits on the same node as astryx-icon.
|
|
256
|
+
const mark = selected.querySelector('.astryx-selector-check');
|
|
257
|
+
expect(mark).not.toBeNull();
|
|
258
|
+
expect(mark).toHaveClass('astryx-icon');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('lets a theme replace the mark with a radio, which draws when unselected too', () => {
|
|
262
|
+
// The point of the indicator layer: one theme entry, and every
|
|
263
|
+
// single-selection mark becomes a radio — including the empty circle on
|
|
264
|
+
// rows that are NOT selected, which a check-only mark never drew.
|
|
265
|
+
const theme = defineTheme({
|
|
266
|
+
name: 'selector-radio-mark-test',
|
|
267
|
+
indicators: {check: RadioIndicator},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
render(
|
|
271
|
+
<Theme theme={theme}>
|
|
272
|
+
<Selector
|
|
273
|
+
label="Fruit"
|
|
274
|
+
options={OPTIONS}
|
|
275
|
+
value="Banana"
|
|
276
|
+
onChange={() => {}}
|
|
277
|
+
/>
|
|
278
|
+
</Theme>,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const options = screen.getAllByRole('option', {hidden: true});
|
|
282
|
+
expect(options.length).toBeGreaterThan(1);
|
|
283
|
+
|
|
284
|
+
// Every row has a radio, selected or not.
|
|
285
|
+
for (const option of options) {
|
|
286
|
+
expect(option.querySelector('.astryx-radio')).not.toBeNull();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// And exactly the selected one is filled.
|
|
290
|
+
const filled = options.filter(
|
|
291
|
+
o => o.querySelector('.astryx-radio-dot') != null,
|
|
292
|
+
);
|
|
293
|
+
expect(filled).toHaveLength(1);
|
|
294
|
+
expect(filled[0]).toHaveTextContent('Banana');
|
|
295
|
+
});
|
|
296
|
+
|
|
155
297
|
it('renders custom option endContent', async () => {
|
|
156
298
|
const user = userEvent.setup();
|
|
157
299
|
render(
|
|
@@ -265,6 +407,81 @@ describe('Selector', () => {
|
|
|
265
407
|
}
|
|
266
408
|
});
|
|
267
409
|
|
|
410
|
+
it('aligns the selected item using untransformed layout geometry', async () => {
|
|
411
|
+
const restoreRects = mockSelectorRects({
|
|
412
|
+
anchor: rect({top: 160, bottom: 192, height: 32}),
|
|
413
|
+
trigger: rect({top: 166, bottom: 186, height: 20}),
|
|
414
|
+
// Simulate the 0.95 entry scale in visual rects while retaining the
|
|
415
|
+
// untransformed 120px list / 36px item offset used for positioning.
|
|
416
|
+
listbox: rect({top: 190, bottom: 304, height: 114}),
|
|
417
|
+
selectedItem: rect({
|
|
418
|
+
top: 224.2,
|
|
419
|
+
bottom: 254.6,
|
|
420
|
+
height: 30.4,
|
|
421
|
+
}),
|
|
422
|
+
listboxLayoutHeight: 120,
|
|
423
|
+
selectedItemLayoutTop: 36,
|
|
424
|
+
selectedItemLayoutHeight: 32,
|
|
425
|
+
viewportHeight: 900,
|
|
426
|
+
});
|
|
427
|
+
const user = userEvent.setup();
|
|
428
|
+
try {
|
|
429
|
+
render(
|
|
430
|
+
<Selector
|
|
431
|
+
label="Fruit"
|
|
432
|
+
options={OPTIONS}
|
|
433
|
+
value="Banana"
|
|
434
|
+
onChange={() => {}}
|
|
435
|
+
/>,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
await user.click(screen.getByRole('combobox'));
|
|
439
|
+
const popover = screen
|
|
440
|
+
.getByRole('listbox', {hidden: true})
|
|
441
|
+
.closest('[popover]');
|
|
442
|
+
await waitFor(() => {
|
|
443
|
+
// 68px geometric alignment plus the 1px optical correction.
|
|
444
|
+
expect(popover?.getAttribute('style')).toContain(
|
|
445
|
+
'margin-block-start: -69px',
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
} finally {
|
|
449
|
+
restoreRects();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('adds the border inset only to input-variant dropdowns', async () => {
|
|
454
|
+
const user = userEvent.setup();
|
|
455
|
+
const {unmount} = render(
|
|
456
|
+
<Selector
|
|
457
|
+
label="Input fruit"
|
|
458
|
+
options={OPTIONS}
|
|
459
|
+
value="Banana"
|
|
460
|
+
onChange={() => {}}
|
|
461
|
+
/>,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
await user.click(screen.getByRole('combobox'));
|
|
465
|
+
const inputDropdownClass = screen.getByRole('listbox', h).className;
|
|
466
|
+
unmount();
|
|
467
|
+
|
|
468
|
+
render(
|
|
469
|
+
<Selector
|
|
470
|
+
label="Ghost fruit"
|
|
471
|
+
options={OPTIONS}
|
|
472
|
+
value="Banana"
|
|
473
|
+
variant="ghost"
|
|
474
|
+
onChange={() => {}}
|
|
475
|
+
/>,
|
|
476
|
+
);
|
|
477
|
+
await user.click(screen.getByRole('combobox'));
|
|
478
|
+
const ghostDropdownClass = screen.getByRole('listbox', h).className;
|
|
479
|
+
|
|
480
|
+
// The bordered input gets one extra StyleX rule for its border-width
|
|
481
|
+
// correction; the borderless ghost keeps the base menu inset.
|
|
482
|
+
expect(inputDropdownClass).not.toBe(ghostDropdownClass);
|
|
483
|
+
});
|
|
484
|
+
|
|
268
485
|
it('does not apply selected-item overlay offset when placement is explicit', async () => {
|
|
269
486
|
const restoreRects = mockSelectorRects();
|
|
270
487
|
const user = userEvent.setup();
|
|
@@ -293,6 +510,89 @@ describe('Selector', () => {
|
|
|
293
510
|
}
|
|
294
511
|
});
|
|
295
512
|
|
|
513
|
+
describe('menu clearance', () => {
|
|
514
|
+
it('clears the trigger by the standard menu offset when placement is explicit', async () => {
|
|
515
|
+
const user = userEvent.setup();
|
|
516
|
+
render(
|
|
517
|
+
<Selector
|
|
518
|
+
label="Fruit"
|
|
519
|
+
options={OPTIONS}
|
|
520
|
+
value="Banana"
|
|
521
|
+
onChange={() => {}}
|
|
522
|
+
placement="above"
|
|
523
|
+
/>,
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
await user.click(screen.getByRole('combobox'));
|
|
527
|
+
const popover = screen
|
|
528
|
+
.getByRole('listbox', {hidden: true})
|
|
529
|
+
.closest('[popover]') as HTMLElement;
|
|
530
|
+
// Both block edges, so the gap survives a position-try-fallbacks flip
|
|
531
|
+
// to the opposite side (#4803).
|
|
532
|
+
await waitFor(() => {
|
|
533
|
+
expect(popover.style.getPropertyValue('--x-marginBlockStart')).toBe(
|
|
534
|
+
spacingVars['--spacing-1'],
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
expect(popover.style.getPropertyValue('--x-marginBlockEnd')).toBe(
|
|
538
|
+
spacingVars['--spacing-1'],
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('clears the trigger in search mode', async () => {
|
|
543
|
+
const user = userEvent.setup();
|
|
544
|
+
render(
|
|
545
|
+
<Selector
|
|
546
|
+
label="Fruit"
|
|
547
|
+
options={OPTIONS}
|
|
548
|
+
value="Banana"
|
|
549
|
+
onChange={() => {}}
|
|
550
|
+
hasSearch
|
|
551
|
+
/>,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// In hasSearch mode the trigger is a plain button, not a combobox.
|
|
555
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
556
|
+
const popover = screen
|
|
557
|
+
.getByRole('listbox', {hidden: true})
|
|
558
|
+
.closest('[popover]') as HTMLElement;
|
|
559
|
+
await waitFor(() => {
|
|
560
|
+
expect(popover.style.getPropertyValue('--x-marginBlockStart')).toBe(
|
|
561
|
+
spacingVars['--spacing-1'],
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('stays flush in the default selected-item overlay', async () => {
|
|
567
|
+
const restoreRects = mockSelectorRects();
|
|
568
|
+
const user = userEvent.setup();
|
|
569
|
+
try {
|
|
570
|
+
render(
|
|
571
|
+
<Selector
|
|
572
|
+
label="Fruit"
|
|
573
|
+
options={OPTIONS}
|
|
574
|
+
value="Banana"
|
|
575
|
+
onChange={() => {}}
|
|
576
|
+
/>,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
await user.click(screen.getByRole('combobox'));
|
|
580
|
+
const popover = screen
|
|
581
|
+
.getByRole('listbox', {hidden: true})
|
|
582
|
+
.closest('[popover]') as HTMLElement;
|
|
583
|
+
await waitFor(() => {
|
|
584
|
+
expect(popover.getAttribute('style')).toContain(
|
|
585
|
+
'margin-block-start: -110px',
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
expect(popover.style.getPropertyValue('--x-marginBlockStart')).toBe('');
|
|
589
|
+
expect(popover.style.getPropertyValue('--x-marginBlockEnd')).toBe('');
|
|
590
|
+
} finally {
|
|
591
|
+
restoreRects();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
296
596
|
describe('hasClear', () => {
|
|
297
597
|
it('shows selected value label when hasClear is enabled', () => {
|
|
298
598
|
render(
|
|
@@ -988,101 +1288,627 @@ describe('Selector', () => {
|
|
|
988
1288
|
});
|
|
989
1289
|
});
|
|
990
1290
|
|
|
991
|
-
describe('
|
|
992
|
-
it('
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
description="Where the alert should route"
|
|
997
|
-
status={{type: 'error', message: 'Destination is required'}}>
|
|
998
|
-
<InputGroupText>#</InputGroupText>
|
|
999
|
-
<Selector
|
|
1000
|
-
label="Channel"
|
|
1001
|
-
isLabelHidden
|
|
1002
|
-
options={OPTIONS}
|
|
1003
|
-
placeholder="Choose a channel"
|
|
1004
|
-
/>
|
|
1005
|
-
</InputGroup>,
|
|
1006
|
-
);
|
|
1291
|
+
describe('typeahead', () => {
|
|
1292
|
+
it('selects the matching option by typing on the closed trigger', async () => {
|
|
1293
|
+
const user = userEvent.setup();
|
|
1294
|
+
const onChange = vi.fn();
|
|
1295
|
+
render(<Selector label="Fruit" options={OPTIONS} onChange={onChange} />);
|
|
1007
1296
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const trigger = screen.getByRole('combobox', {
|
|
1011
|
-
name: 'Destination Channel',
|
|
1012
|
-
});
|
|
1013
|
-
const labelledByIDs =
|
|
1014
|
-
trigger.getAttribute('aria-labelledby')?.split(' ') ?? [];
|
|
1297
|
+
await user.tab();
|
|
1298
|
+
await user.keyboard('c');
|
|
1015
1299
|
|
|
1016
|
-
expect(
|
|
1017
|
-
|
|
1018
|
-
expect(
|
|
1019
|
-
'
|
|
1020
|
-
|
|
1021
|
-
expect(trigger).toHaveAttribute(
|
|
1022
|
-
'aria-describedby',
|
|
1023
|
-
group.getAttribute('aria-describedby'),
|
|
1300
|
+
expect(onChange).toHaveBeenCalledWith('Cherry');
|
|
1301
|
+
// Native select parity: the value changes without opening the menu.
|
|
1302
|
+
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
1303
|
+
'aria-expanded',
|
|
1304
|
+
'false',
|
|
1024
1305
|
);
|
|
1025
|
-
expect(screen.getByText('#')).toBeInTheDocument();
|
|
1026
1306
|
});
|
|
1027
1307
|
|
|
1028
|
-
it('
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1308
|
+
it('cycles through options sharing a first letter on repeated presses', async () => {
|
|
1309
|
+
const user = userEvent.setup();
|
|
1310
|
+
function Harness() {
|
|
1311
|
+
const [value, setValue] = useState<string | undefined>(undefined);
|
|
1312
|
+
return (
|
|
1032
1313
|
<Selector
|
|
1033
|
-
label="
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
disabledMessage="Choose a project first"
|
|
1314
|
+
label="City"
|
|
1315
|
+
options={['Austin', 'Chicago', 'Cleveland', 'Columbus']}
|
|
1316
|
+
value={value}
|
|
1317
|
+
onChange={setValue}
|
|
1038
1318
|
/>
|
|
1039
|
-
|
|
1040
|
-
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
render(<Harness />);
|
|
1041
1322
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1323
|
+
await user.tab();
|
|
1324
|
+
await user.keyboard('c');
|
|
1325
|
+
expect(screen.getByRole('combobox')).toHaveTextContent('Chicago');
|
|
1326
|
+
await user.keyboard('c');
|
|
1327
|
+
expect(screen.getByRole('combobox')).toHaveTextContent('Cleveland');
|
|
1328
|
+
await user.keyboard('c');
|
|
1329
|
+
expect(screen.getByRole('combobox')).toHaveTextContent('Columbus');
|
|
1330
|
+
// Wraps back around past non-matching options.
|
|
1331
|
+
await user.keyboard('c');
|
|
1332
|
+
expect(screen.getByRole('combobox')).toHaveTextContent('Chicago');
|
|
1333
|
+
});
|
|
1046
1334
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1335
|
+
it('advances past the current selection on a fresh single-letter press', async () => {
|
|
1336
|
+
const user = userEvent.setup();
|
|
1337
|
+
function Harness() {
|
|
1338
|
+
const [value, setValue] = useState<string | undefined>('Chicago');
|
|
1339
|
+
return (
|
|
1340
|
+
<Selector
|
|
1341
|
+
label="City"
|
|
1342
|
+
options={['Austin', 'Chicago', 'Cleveland', 'Columbus']}
|
|
1343
|
+
value={value}
|
|
1344
|
+
onChange={setValue}
|
|
1345
|
+
/>
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
render(<Harness />);
|
|
1349
|
+
|
|
1350
|
+
const trigger = screen.getByRole('combobox');
|
|
1351
|
+
await user.tab();
|
|
1352
|
+
// Native select parity: the selected option's own initial moves on to
|
|
1353
|
+
// the next match. Anchoring the search AT the selection instead of after
|
|
1354
|
+
// it re-matches the current value, and the duplicate-select guard then
|
|
1355
|
+
// swallows the keystroke entirely.
|
|
1356
|
+
type('c', trigger);
|
|
1357
|
+
|
|
1358
|
+
expect(trigger).toHaveTextContent('Cleveland');
|
|
1050
1359
|
});
|
|
1051
|
-
});
|
|
1052
1360
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1361
|
+
it('advances the highlight past the current one with the menu open', async () => {
|
|
1362
|
+
const user = userEvent.setup();
|
|
1055
1363
|
render(
|
|
1056
1364
|
<Selector
|
|
1057
|
-
label="
|
|
1058
|
-
options={
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
data-testid="fruit-selector"
|
|
1365
|
+
label="City"
|
|
1366
|
+
options={['Austin', 'Chicago', 'Cleveland', 'Columbus']}
|
|
1367
|
+
value="Chicago"
|
|
1368
|
+
onChange={() => {}}
|
|
1062
1369
|
/>,
|
|
1063
1370
|
);
|
|
1064
1371
|
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
fireEvent.mouseEnter(container);
|
|
1070
|
-
await waitFor(() => {
|
|
1071
|
-
expect(tooltip).toHaveAttribute('popover-open');
|
|
1072
|
-
});
|
|
1372
|
+
const trigger = screen.getByRole('combobox');
|
|
1373
|
+
await user.tab();
|
|
1374
|
+
await user.keyboard('{Enter}'); // opens with the highlight on Chicago
|
|
1375
|
+
type('c', trigger);
|
|
1073
1376
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1377
|
+
const activeId = trigger.getAttribute('aria-activedescendant');
|
|
1378
|
+
expect(document.getElementById(activeId ?? '')).toHaveTextContent(
|
|
1379
|
+
'Cleveland',
|
|
1380
|
+
);
|
|
1078
1381
|
});
|
|
1079
1382
|
|
|
1080
|
-
it('
|
|
1383
|
+
it('treats a space mid-buffer as part of the match, not as open', async () => {
|
|
1081
1384
|
const user = userEvent.setup();
|
|
1385
|
+
const onChange = vi.fn();
|
|
1082
1386
|
render(
|
|
1083
1387
|
<Selector
|
|
1084
|
-
label="
|
|
1085
|
-
options={
|
|
1388
|
+
label="State"
|
|
1389
|
+
options={['New Jersey', 'New York']}
|
|
1390
|
+
onChange={onChange}
|
|
1391
|
+
/>,
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
const trigger = screen.getByRole('combobox');
|
|
1395
|
+
await user.tab();
|
|
1396
|
+
// Synchronous keydowns: the buffer only accumulates while keystrokes
|
|
1397
|
+
// land inside the TYPEAHEAD_RESET_MS window, and awaiting between them
|
|
1398
|
+
// would put a CI stall on the critical path.
|
|
1399
|
+
type('new y', trigger);
|
|
1400
|
+
|
|
1401
|
+
expect(onChange).toHaveBeenLastCalledWith('New York');
|
|
1402
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
it('opens and seeds the search input when typing on a closed hasSearch trigger', async () => {
|
|
1406
|
+
const user = userEvent.setup();
|
|
1407
|
+
render(<Selector label="Fruit" options={OPTIONS} hasSearch />);
|
|
1408
|
+
|
|
1409
|
+
await user.tab();
|
|
1410
|
+
await user.keyboard('c');
|
|
1411
|
+
|
|
1412
|
+
const search = screen.getByPlaceholderText('Search…');
|
|
1413
|
+
expect(search).toHaveValue('c');
|
|
1414
|
+
await waitFor(() => expect(search).toHaveFocus());
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
it('accumulates a multi-character prefix and resets it after the timeout', async () => {
|
|
1418
|
+
const user = userEvent.setup();
|
|
1419
|
+
const onChange = vi.fn();
|
|
1420
|
+
// Controlled: the committed value has to feed back in, or the match
|
|
1421
|
+
// anchor stays -1 for the whole test and never gets exercised.
|
|
1422
|
+
function Harness() {
|
|
1423
|
+
const [value, setValue] = useState<string | undefined>(undefined);
|
|
1424
|
+
return (
|
|
1425
|
+
<Selector
|
|
1426
|
+
label="Fruit"
|
|
1427
|
+
options={['Apple', 'Banana', 'Blueberry']}
|
|
1428
|
+
value={value}
|
|
1429
|
+
onChange={next => {
|
|
1430
|
+
setValue(next);
|
|
1431
|
+
onChange(next);
|
|
1432
|
+
}}
|
|
1433
|
+
/>
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
render(<Harness />);
|
|
1437
|
+
|
|
1438
|
+
const trigger = screen.getByRole('combobox');
|
|
1439
|
+
await user.tab();
|
|
1440
|
+
type('b', trigger);
|
|
1441
|
+
expect(trigger).toHaveTextContent('Banana');
|
|
1442
|
+
// Within the window the buffer accumulates: "bl" → Blueberry. A
|
|
1443
|
+
// multi-character buffer refines, so it may keep the current match.
|
|
1444
|
+
type('l', trigger);
|
|
1445
|
+
expect(trigger).toHaveTextContent('Blueberry');
|
|
1446
|
+
|
|
1447
|
+
// Past the window the buffer starts fresh: "a" → Apple. A surviving
|
|
1448
|
+
// buffer would search "bla" and match nothing, so only a real reset
|
|
1449
|
+
// gets here — worth the one real wait in the suite.
|
|
1450
|
+
await new Promise(resolve =>
|
|
1451
|
+
setTimeout(resolve, TYPEAHEAD_RESET_MS + 100),
|
|
1452
|
+
);
|
|
1453
|
+
type('a', trigger);
|
|
1454
|
+
expect(trigger).toHaveTextContent('Apple');
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
it('skips disabled options when matching', async () => {
|
|
1458
|
+
const user = userEvent.setup();
|
|
1459
|
+
function Harness() {
|
|
1460
|
+
const [value, setValue] = useState<string | undefined>(undefined);
|
|
1461
|
+
return (
|
|
1462
|
+
<Selector
|
|
1463
|
+
label="Fruit"
|
|
1464
|
+
options={[{value: 'Cherry', disabled: true}, 'Coconut']}
|
|
1465
|
+
value={value}
|
|
1466
|
+
onChange={setValue}
|
|
1467
|
+
/>
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
render(<Harness />);
|
|
1471
|
+
|
|
1472
|
+
const trigger = screen.getByRole('combobox');
|
|
1473
|
+
await user.tab();
|
|
1474
|
+
type('c', trigger);
|
|
1475
|
+
expect(trigger).toHaveTextContent('Coconut');
|
|
1476
|
+
|
|
1477
|
+
// The skip has to survive cycling too: with Coconut current, the next
|
|
1478
|
+
// press wraps onto the disabled Cherry and must pass over it.
|
|
1479
|
+
type('c', trigger);
|
|
1480
|
+
expect(trigger).toHaveTextContent('Coconut');
|
|
1481
|
+
expect(trigger).not.toHaveTextContent('Cherry');
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
it('moves the highlight without committing when typing with the menu open', async () => {
|
|
1485
|
+
const user = userEvent.setup();
|
|
1486
|
+
const onChange = vi.fn();
|
|
1487
|
+
render(
|
|
1488
|
+
<Selector
|
|
1489
|
+
label="City"
|
|
1490
|
+
options={['Austin', 'Chicago', 'Cleveland']}
|
|
1491
|
+
onChange={onChange}
|
|
1492
|
+
/>,
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
await user.tab();
|
|
1496
|
+
await user.keyboard('{Enter}'); // open
|
|
1497
|
+
const trigger = screen.getByRole('combobox');
|
|
1498
|
+
|
|
1499
|
+
await user.keyboard('c');
|
|
1500
|
+
let activeId = trigger.getAttribute('aria-activedescendant');
|
|
1501
|
+
expect(document.getElementById(activeId ?? '')).toHaveTextContent(
|
|
1502
|
+
'Chicago',
|
|
1503
|
+
);
|
|
1504
|
+
|
|
1505
|
+
// Repeated press cycles the highlight, still without committing.
|
|
1506
|
+
await user.keyboard('c');
|
|
1507
|
+
activeId = trigger.getAttribute('aria-activedescendant');
|
|
1508
|
+
expect(document.getElementById(activeId ?? '')).toHaveTextContent(
|
|
1509
|
+
'Cleveland',
|
|
1510
|
+
);
|
|
1511
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1512
|
+
// aria-activedescendant already announces each match, so announcing
|
|
1513
|
+
// again here would make a screen reader say every match twice.
|
|
1514
|
+
// useAnnounce writes its text in a rAF callback, so let a frame pass —
|
|
1515
|
+
// asserting before it runs would pass no matter what the code does.
|
|
1516
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
|
|
1517
|
+
expect(politeRegion()?.textContent ?? '').toBe('');
|
|
1518
|
+
|
|
1519
|
+
await user.keyboard('{Enter}');
|
|
1520
|
+
expect(onChange).toHaveBeenCalledWith('Cleveland');
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
it('does not fire onChange when the only match is already selected', async () => {
|
|
1524
|
+
const user = userEvent.setup();
|
|
1525
|
+
const onChange = vi.fn();
|
|
1526
|
+
render(
|
|
1527
|
+
<Selector
|
|
1528
|
+
label="Fruit"
|
|
1529
|
+
options={OPTIONS}
|
|
1530
|
+
value="Cherry"
|
|
1531
|
+
onChange={onChange}
|
|
1532
|
+
/>,
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
await user.tab();
|
|
1536
|
+
await user.keyboard('c');
|
|
1537
|
+
|
|
1538
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
it('ignores printable keys pressed with ctrl or meta modifiers', async () => {
|
|
1542
|
+
const user = userEvent.setup();
|
|
1543
|
+
const onChange = vi.fn();
|
|
1544
|
+
render(<Selector label="Fruit" options={OPTIONS} onChange={onChange} />);
|
|
1545
|
+
|
|
1546
|
+
await user.tab();
|
|
1547
|
+
await user.keyboard('{Control>}c{/Control}{Meta>}b{/Meta}');
|
|
1548
|
+
|
|
1549
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
it('announces the committed option to screen readers', async () => {
|
|
1553
|
+
const user = userEvent.setup();
|
|
1554
|
+
render(<Selector label="Fruit" options={OPTIONS} onChange={() => {}} />);
|
|
1555
|
+
|
|
1556
|
+
await user.tab();
|
|
1557
|
+
await user.keyboard('c');
|
|
1558
|
+
|
|
1559
|
+
// The trigger keeps focus and the menu never opens, so nothing else
|
|
1560
|
+
// prompts a re-read. The polite live region carries the new value.
|
|
1561
|
+
await waitFor(() => {
|
|
1562
|
+
expect(politeRegion()).toHaveTextContent('Cherry');
|
|
1563
|
+
});
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
it('does not select while focusable-disabled', async () => {
|
|
1567
|
+
const user = userEvent.setup();
|
|
1568
|
+
const onChange = vi.fn();
|
|
1569
|
+
render(
|
|
1570
|
+
<Selector
|
|
1571
|
+
label="Fruit"
|
|
1572
|
+
options={OPTIONS}
|
|
1573
|
+
onChange={onChange}
|
|
1574
|
+
isDisabled
|
|
1575
|
+
disabledMessage="Ask an admin"
|
|
1576
|
+
/>,
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
// aria-disabled keeps the trigger focusable, so keydowns still arrive.
|
|
1580
|
+
screen.getByRole('combobox').focus();
|
|
1581
|
+
await user.keyboard('c');
|
|
1582
|
+
|
|
1583
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
it('cycles without duplicating changeAction while an action is pending', async () => {
|
|
1587
|
+
const user = userEvent.setup();
|
|
1588
|
+
const calls: string[] = [];
|
|
1589
|
+
render(
|
|
1590
|
+
<Selector
|
|
1591
|
+
label="City"
|
|
1592
|
+
options={['Chicago', 'Cleveland', 'Columbus']}
|
|
1593
|
+
value={undefined}
|
|
1594
|
+
changeAction={async value => {
|
|
1595
|
+
calls.push(value);
|
|
1596
|
+
// Never settles, so the value prop never catches up to what the
|
|
1597
|
+
// trigger already shows.
|
|
1598
|
+
await new Promise<void>(() => {});
|
|
1599
|
+
}}
|
|
1600
|
+
/>,
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
const trigger = screen.getByRole('combobox');
|
|
1604
|
+
await user.tab();
|
|
1605
|
+
type('ccc', trigger);
|
|
1606
|
+
|
|
1607
|
+
// The anchor must come from the optimistic value, not the stale prop.
|
|
1608
|
+
// Three options make that observable: with a stale anchor every press
|
|
1609
|
+
// re-matches Chicago, which the duplicate guard then swallows.
|
|
1610
|
+
expect(calls).toEqual(['Chicago', 'Cleveland', 'Columbus']);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
it('starts a fresh buffer after selecting from the open menu', async () => {
|
|
1614
|
+
const user = userEvent.setup();
|
|
1615
|
+
const onChange = vi.fn();
|
|
1616
|
+
render(
|
|
1617
|
+
<Selector
|
|
1618
|
+
label="Animal"
|
|
1619
|
+
options={['Cat', 'Dog']}
|
|
1620
|
+
onChange={onChange}
|
|
1621
|
+
/>,
|
|
1622
|
+
);
|
|
1623
|
+
|
|
1624
|
+
await user.tab();
|
|
1625
|
+
await user.keyboard('{Enter}'); // open
|
|
1626
|
+
await user.keyboard('d'); // highlight Dog
|
|
1627
|
+
await user.keyboard('{Enter}'); // commit Dog, closes
|
|
1628
|
+
onChange.mockClear();
|
|
1629
|
+
|
|
1630
|
+
// The stale 'd' must not linger: 'c' is a fresh buffer, not "dc".
|
|
1631
|
+
await user.keyboard('c');
|
|
1632
|
+
expect(onChange).toHaveBeenCalledWith('Cat');
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
it('starts a fresh buffer after the value is cleared', async () => {
|
|
1636
|
+
const user = userEvent.setup();
|
|
1637
|
+
const onChange = vi.fn();
|
|
1638
|
+
function Harness() {
|
|
1639
|
+
const [value, setValue] = useState<string | null>('Dog');
|
|
1640
|
+
return (
|
|
1641
|
+
<Selector
|
|
1642
|
+
label="Animal"
|
|
1643
|
+
options={['Cat', 'Dog']}
|
|
1644
|
+
hasClear
|
|
1645
|
+
value={value}
|
|
1646
|
+
onChange={next => {
|
|
1647
|
+
setValue(next);
|
|
1648
|
+
onChange(next);
|
|
1649
|
+
}}
|
|
1650
|
+
/>
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
render(<Harness />);
|
|
1654
|
+
|
|
1655
|
+
const trigger = screen.getByRole('combobox');
|
|
1656
|
+
await user.tab();
|
|
1657
|
+
// Fills the buffer with 'd' without committing — Dog is already current.
|
|
1658
|
+
type('d', trigger);
|
|
1659
|
+
fireEvent.keyDown(trigger, {key: 'Delete'});
|
|
1660
|
+
expect(onChange).toHaveBeenLastCalledWith(null);
|
|
1661
|
+
|
|
1662
|
+
// The stale 'd' must not survive the clear: 'c' is a fresh buffer, not
|
|
1663
|
+
// "dc", which would match nothing and leave the value cleared.
|
|
1664
|
+
type('c', trigger);
|
|
1665
|
+
expect(onChange).toHaveBeenLastCalledWith('Cat');
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
it('matches on the label and commits the value', async () => {
|
|
1669
|
+
const user = userEvent.setup();
|
|
1670
|
+
const onChange = vi.fn();
|
|
1671
|
+
// Values deliberately crossed against labels: a native select matches
|
|
1672
|
+
// the rendered text and reports the value.
|
|
1673
|
+
function Harness() {
|
|
1674
|
+
const [value, setValue] = useState<string | undefined>(undefined);
|
|
1675
|
+
return (
|
|
1676
|
+
<Selector
|
|
1677
|
+
label="Fruit"
|
|
1678
|
+
options={[
|
|
1679
|
+
{value: 'zzz', label: 'Apple'},
|
|
1680
|
+
{value: 'apple', label: 'Zebra'},
|
|
1681
|
+
]}
|
|
1682
|
+
value={value}
|
|
1683
|
+
onChange={next => {
|
|
1684
|
+
setValue(next);
|
|
1685
|
+
onChange(next);
|
|
1686
|
+
}}
|
|
1687
|
+
/>
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
render(<Harness />);
|
|
1691
|
+
|
|
1692
|
+
const trigger = screen.getByRole('combobox');
|
|
1693
|
+
await user.tab();
|
|
1694
|
+
type('a', trigger);
|
|
1695
|
+
|
|
1696
|
+
expect(onChange).toHaveBeenCalledWith('zzz');
|
|
1697
|
+
expect(trigger).toHaveTextContent('Apple');
|
|
1698
|
+
|
|
1699
|
+
// No other label starts with "a" — the option whose *value* is 'apple'
|
|
1700
|
+
// is labelled Zebra — so a second press stays put and commits nothing.
|
|
1701
|
+
type('a', trigger);
|
|
1702
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
it('matches across sections, ignoring dividers and group titles', async () => {
|
|
1706
|
+
const user = userEvent.setup();
|
|
1707
|
+
const onChange = vi.fn();
|
|
1708
|
+
render(
|
|
1709
|
+
<Selector
|
|
1710
|
+
label="Fruit"
|
|
1711
|
+
options={[
|
|
1712
|
+
'Almond',
|
|
1713
|
+
{type: 'divider'},
|
|
1714
|
+
{
|
|
1715
|
+
type: 'section',
|
|
1716
|
+
title: 'Tropical',
|
|
1717
|
+
options: [
|
|
1718
|
+
{value: 'mango', label: 'Mango'},
|
|
1719
|
+
{value: 'papaya', label: 'Papaya'},
|
|
1720
|
+
],
|
|
1721
|
+
},
|
|
1722
|
+
]}
|
|
1723
|
+
onChange={onChange}
|
|
1724
|
+
/>,
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
const trigger = screen.getByRole('combobox');
|
|
1728
|
+
await user.tab();
|
|
1729
|
+
type('p', trigger);
|
|
1730
|
+
expect(onChange).toHaveBeenCalledWith('papaya');
|
|
1731
|
+
|
|
1732
|
+
// The section title "Tropical" is decoration, not an option.
|
|
1733
|
+
onChange.mockClear();
|
|
1734
|
+
type('t', trigger);
|
|
1735
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it('keeps aria-activedescendant on the matched option across a section', async () => {
|
|
1739
|
+
const user = userEvent.setup();
|
|
1740
|
+
render(
|
|
1741
|
+
<Selector
|
|
1742
|
+
label="Fruit"
|
|
1743
|
+
options={[
|
|
1744
|
+
'Almond',
|
|
1745
|
+
{type: 'divider'},
|
|
1746
|
+
{
|
|
1747
|
+
type: 'section',
|
|
1748
|
+
title: 'Berries',
|
|
1749
|
+
options: [{value: 'blueberry', label: 'Blueberry'}],
|
|
1750
|
+
},
|
|
1751
|
+
]}
|
|
1752
|
+
value="Almond"
|
|
1753
|
+
onChange={() => {}}
|
|
1754
|
+
/>,
|
|
1755
|
+
);
|
|
1756
|
+
|
|
1757
|
+
const trigger = screen.getByRole('combobox');
|
|
1758
|
+
await user.tab();
|
|
1759
|
+
await user.keyboard('{Enter}'); // opens on Almond
|
|
1760
|
+
type('b', trigger);
|
|
1761
|
+
|
|
1762
|
+
const activeId = trigger.getAttribute('aria-activedescendant');
|
|
1763
|
+
expect(document.getElementById(activeId ?? '')).toHaveTextContent(
|
|
1764
|
+
'Blueberry',
|
|
1765
|
+
);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
it('anchors at the top when the value matches no option', async () => {
|
|
1769
|
+
const user = userEvent.setup();
|
|
1770
|
+
const onChange = vi.fn();
|
|
1771
|
+
render(
|
|
1772
|
+
<Selector
|
|
1773
|
+
label="Fruit"
|
|
1774
|
+
options={['Apple', 'Apricot']}
|
|
1775
|
+
value="Durian"
|
|
1776
|
+
onChange={onChange}
|
|
1777
|
+
/>,
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
const trigger = screen.getByRole('combobox');
|
|
1781
|
+
await user.tab();
|
|
1782
|
+
type('a', trigger);
|
|
1783
|
+
|
|
1784
|
+
// Nothing is really selected, so the first match must stay reachable.
|
|
1785
|
+
expect(onChange).toHaveBeenCalledWith('Apple');
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
it('lets Space open the menu after an abandoned typeahead', async () => {
|
|
1789
|
+
const user = userEvent.setup();
|
|
1790
|
+
render(<Selector label="Fruit" options={OPTIONS} />);
|
|
1791
|
+
|
|
1792
|
+
const trigger = screen.getByRole('combobox');
|
|
1793
|
+
await user.tab();
|
|
1794
|
+
await user.keyboard('{Enter}'); // open
|
|
1795
|
+
await user.keyboard('z'); // no match; buffer holds "z"
|
|
1796
|
+
await user.keyboard('{Escape}'); // close, abandoning the buffer
|
|
1797
|
+
|
|
1798
|
+
// A live "z" buffer would swallow Space as a match character.
|
|
1799
|
+
await user.keyboard(' ');
|
|
1800
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
it('seeds every character typed before the search input takes focus', async () => {
|
|
1804
|
+
const user = userEvent.setup();
|
|
1805
|
+
render(<Selector label="Fruit" options={OPTIONS} hasSearch />);
|
|
1806
|
+
|
|
1807
|
+
await user.tab();
|
|
1808
|
+
// The popup opens on the first key, but focus only moves to the search
|
|
1809
|
+
// input on the next frame — the second key still lands on the trigger.
|
|
1810
|
+
await user.keyboard('ch');
|
|
1811
|
+
|
|
1812
|
+
const search = screen.getByPlaceholderText('Search…');
|
|
1813
|
+
await waitFor(() => expect(search).toHaveValue('ch'));
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
describe('InputGroup integration', () => {
|
|
1818
|
+
it('uses the group Field chrome and composes group and selector labels', () => {
|
|
1819
|
+
render(
|
|
1820
|
+
<InputGroup
|
|
1821
|
+
label="Destination"
|
|
1822
|
+
description="Where the alert should route"
|
|
1823
|
+
status={{type: 'error', message: 'Destination is required'}}>
|
|
1824
|
+
<InputGroupText>#</InputGroupText>
|
|
1825
|
+
<Selector
|
|
1826
|
+
label="Channel"
|
|
1827
|
+
isLabelHidden
|
|
1828
|
+
options={OPTIONS}
|
|
1829
|
+
placeholder="Choose a channel"
|
|
1830
|
+
/>
|
|
1831
|
+
</InputGroup>,
|
|
1832
|
+
);
|
|
1833
|
+
|
|
1834
|
+
const group = screen.getByRole('group', {name: 'Destination'});
|
|
1835
|
+
const groupLabelID = group.getAttribute('aria-labelledby');
|
|
1836
|
+
const trigger = screen.getByRole('combobox', {
|
|
1837
|
+
name: 'Destination Channel',
|
|
1838
|
+
});
|
|
1839
|
+
const labelledByIDs =
|
|
1840
|
+
trigger.getAttribute('aria-labelledby')?.split(' ') ?? [];
|
|
1841
|
+
|
|
1842
|
+
expect(labelledByIDs).toHaveLength(2);
|
|
1843
|
+
expect(labelledByIDs[0]).toBe(groupLabelID);
|
|
1844
|
+
expect(document.getElementById(labelledByIDs[1])).toHaveTextContent(
|
|
1845
|
+
'Channel',
|
|
1846
|
+
);
|
|
1847
|
+
expect(trigger).toHaveAttribute(
|
|
1848
|
+
'aria-describedby',
|
|
1849
|
+
group.getAttribute('aria-describedby'),
|
|
1850
|
+
);
|
|
1851
|
+
expect(screen.getByText('#')).toBeInTheDocument();
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
it('keeps disabled reasons described when grouped', () => {
|
|
1855
|
+
render(
|
|
1856
|
+
<InputGroup label="Destination">
|
|
1857
|
+
<InputGroupText>#</InputGroupText>
|
|
1858
|
+
<Selector
|
|
1859
|
+
label="Channel"
|
|
1860
|
+
isLabelHidden
|
|
1861
|
+
options={OPTIONS}
|
|
1862
|
+
isDisabled
|
|
1863
|
+
disabledMessage="Choose a project first"
|
|
1864
|
+
/>
|
|
1865
|
+
</InputGroup>,
|
|
1866
|
+
);
|
|
1867
|
+
|
|
1868
|
+
const trigger = screen.getByRole('combobox', {
|
|
1869
|
+
name: 'Destination Channel',
|
|
1870
|
+
});
|
|
1871
|
+
const tooltip = screen.getByRole('tooltip', h);
|
|
1872
|
+
|
|
1873
|
+
expect(trigger).not.toBeDisabled();
|
|
1874
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
1875
|
+
expect(trigger.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
1876
|
+
});
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
describe('disabledMessage', () => {
|
|
1880
|
+
it('shows the reason tooltip on hover when disabled with a reason', async () => {
|
|
1881
|
+
render(
|
|
1882
|
+
<Selector
|
|
1883
|
+
label="Fruit"
|
|
1884
|
+
options={OPTIONS}
|
|
1885
|
+
isDisabled
|
|
1886
|
+
disabledMessage="You need the Editor role"
|
|
1887
|
+
data-testid="fruit-selector"
|
|
1888
|
+
/>,
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
const container = screen.getByTestId('fruit-selector');
|
|
1892
|
+
const tooltip = screen.getByRole('tooltip', h);
|
|
1893
|
+
expect(tooltip).toHaveTextContent('You need the Editor role');
|
|
1894
|
+
|
|
1895
|
+
fireEvent.mouseEnter(container);
|
|
1896
|
+
await waitFor(() => {
|
|
1897
|
+
expect(tooltip).toHaveAttribute('popover-open');
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
fireEvent.mouseLeave(container);
|
|
1901
|
+
await waitFor(() => {
|
|
1902
|
+
expect(tooltip).not.toHaveAttribute('popover-open');
|
|
1903
|
+
});
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
it('shows the reason tooltip on keyboard focus', async () => {
|
|
1907
|
+
const user = userEvent.setup();
|
|
1908
|
+
render(
|
|
1909
|
+
<Selector
|
|
1910
|
+
label="Fruit"
|
|
1911
|
+
options={OPTIONS}
|
|
1086
1912
|
isDisabled
|
|
1087
1913
|
disabledMessage="You need the Editor role"
|
|
1088
1914
|
/>,
|
|
@@ -1318,6 +2144,28 @@ describe('Selector statusVariant forwarding', () => {
|
|
|
1318
2144
|
});
|
|
1319
2145
|
});
|
|
1320
2146
|
|
|
2147
|
+
describe('Selector empty-state theme target', () => {
|
|
2148
|
+
const OPTIONS = ['Apple', 'Banana', 'Cherry'];
|
|
2149
|
+
|
|
2150
|
+
it('renders the astryx-selector-empty-state target on the "No results found" element', async () => {
|
|
2151
|
+
const user = userEvent.setup();
|
|
2152
|
+
const {container} = render(
|
|
2153
|
+
<Selector
|
|
2154
|
+
label="Fruit"
|
|
2155
|
+
options={OPTIONS}
|
|
2156
|
+
onChange={() => {}}
|
|
2157
|
+
hasSearch
|
|
2158
|
+
/>,
|
|
2159
|
+
);
|
|
2160
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2161
|
+
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
2162
|
+
|
|
2163
|
+
const empty = container.querySelector('.astryx-selector-empty-state');
|
|
2164
|
+
expect(empty).not.toBeNull();
|
|
2165
|
+
expect(empty).toHaveTextContent('No results found');
|
|
2166
|
+
});
|
|
2167
|
+
});
|
|
2168
|
+
|
|
1321
2169
|
describe('Selector clear icon theme target', () => {
|
|
1322
2170
|
// Resolve the clear glyph span (the astryx-icon element inside the clear
|
|
1323
2171
|
// button), independent of the theme target class.
|
|
@@ -1330,7 +2178,7 @@ describe('Selector clear icon theme target', () => {
|
|
|
1330
2178
|
return icon as HTMLElement;
|
|
1331
2179
|
};
|
|
1332
2180
|
|
|
1333
|
-
it('renders the astryx-
|
|
2181
|
+
it('renders the astryx-input-clear-icon target (plus the legacy alias) on the clear glyph', () => {
|
|
1334
2182
|
render(
|
|
1335
2183
|
<Selector
|
|
1336
2184
|
label="Fruit"
|
|
@@ -1340,11 +2188,13 @@ describe('Selector clear icon theme target', () => {
|
|
|
1340
2188
|
hasClear
|
|
1341
2189
|
/>,
|
|
1342
2190
|
);
|
|
1343
|
-
// The
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
2191
|
+
// The canonical target lands on the icon element itself (not the button),
|
|
2192
|
+
// so a theme can restyle just this glyph (color, size, hover) via
|
|
2193
|
+
// `defineTheme` — a button-level target could not reach the icon's own
|
|
2194
|
+
// color/size. The original per-component name rides along for a
|
|
2195
|
+
// deprecation window.
|
|
1347
2196
|
const icon = getClearIcon();
|
|
2197
|
+
expect(icon).toHaveClass('astryx-input-clear-icon');
|
|
1348
2198
|
expect(icon).toHaveClass('astryx-selector-clear-icon');
|
|
1349
2199
|
expect(icon).toHaveClass('astryx-icon');
|
|
1350
2200
|
});
|
|
@@ -1366,11 +2216,13 @@ describe('Selector clear icon theme target', () => {
|
|
|
1366
2216
|
expect(onChange).toHaveBeenCalledWith(null);
|
|
1367
2217
|
});
|
|
1368
2218
|
|
|
1369
|
-
it('
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
// target
|
|
1373
|
-
//
|
|
2219
|
+
it('routes the clear glyph through the shared clear button, keeping the legacy target', () => {
|
|
2220
|
+
// The clear affordance now composes the shared InputClearButton (a ghost
|
|
2221
|
+
// Button with a secondary/sm glyph), so the icon carries the canonical
|
|
2222
|
+
// `astryx-input-clear-icon` target and — for a deprecation window — the
|
|
2223
|
+
// original `astryx-selector-clear-icon`. Aside from those target classes
|
|
2224
|
+
// it matches the shared button's own `close`/`sm`/`secondary` glyph
|
|
2225
|
+
// exactly, so the default look is defined in one place.
|
|
1374
2226
|
render(
|
|
1375
2227
|
<Selector
|
|
1376
2228
|
label="Fruit"
|
|
@@ -1381,6 +2233,8 @@ describe('Selector clear icon theme target', () => {
|
|
|
1381
2233
|
/>,
|
|
1382
2234
|
);
|
|
1383
2235
|
const icon = getClearIcon();
|
|
2236
|
+
expect(icon).toHaveClass('astryx-input-clear-icon');
|
|
2237
|
+
expect(icon).toHaveClass('astryx-selector-clear-icon');
|
|
1384
2238
|
|
|
1385
2239
|
const {container: refContainer} = render(
|
|
1386
2240
|
<Icon icon="close" size="sm" color="secondary" />,
|
|
@@ -1390,7 +2244,11 @@ describe('Selector clear icon theme target', () => {
|
|
|
1390
2244
|
const styleClasses = (el: HTMLElement) =>
|
|
1391
2245
|
el.className
|
|
1392
2246
|
.split(' ')
|
|
1393
|
-
.filter(
|
|
2247
|
+
.filter(
|
|
2248
|
+
c =>
|
|
2249
|
+
c !== 'astryx-input-clear-icon' &&
|
|
2250
|
+
c !== 'astryx-selector-clear-icon',
|
|
2251
|
+
)
|
|
1394
2252
|
.sort();
|
|
1395
2253
|
|
|
1396
2254
|
expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
|
|
@@ -1464,9 +2322,12 @@ describe('Selector indicator (chevron) icon theme target', () => {
|
|
|
1464
2322
|
});
|
|
1465
2323
|
});
|
|
1466
2324
|
|
|
1467
|
-
it('renders the default icon (
|
|
2325
|
+
it('renders the default icon (secondary color, sm size) byte-identically', () => {
|
|
1468
2326
|
// Pixel-identical default guard: the chevron glyph must carry the exact
|
|
1469
|
-
// same StyleX color/size classes as a standalone
|
|
2327
|
+
// same StyleX color/size classes as a standalone secondary/sm icon. The
|
|
2328
|
+
// glyph now sets --color-icon-secondary itself rather than inheriting it
|
|
2329
|
+
// from a wrapper span that set the same token, so the rendered color is
|
|
2330
|
+
// unchanged. The added
|
|
1470
2331
|
// target class + data-state are purely additive — they change nothing until
|
|
1471
2332
|
// a theme targets them.
|
|
1472
2333
|
const {container} = render(
|
|
@@ -1475,12 +2336,12 @@ describe('Selector indicator (chevron) icon theme target', () => {
|
|
|
1475
2336
|
const icon = getIndicatorIcon(container);
|
|
1476
2337
|
|
|
1477
2338
|
const {container: refContainer} = render(
|
|
1478
|
-
<Icon icon="chevronDown" size="sm" color="
|
|
2339
|
+
<Icon icon="chevronDown" size="sm" color="secondary" />,
|
|
1479
2340
|
);
|
|
1480
2341
|
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
1481
2342
|
|
|
1482
2343
|
// Exclude the additive theme-target classes (the stable target + its
|
|
1483
|
-
// reflected state class) so only
|
|
2344
|
+
// reflected state class) so only StyleX classes remain.
|
|
1484
2345
|
const themeTargetClasses = new Set([
|
|
1485
2346
|
'astryx-selector-indicator-icon',
|
|
1486
2347
|
'collapsed',
|
|
@@ -1492,7 +2353,14 @@ describe('Selector indicator (chevron) icon theme target', () => {
|
|
|
1492
2353
|
.filter(c => !themeTargetClasses.has(c))
|
|
1493
2354
|
.sort();
|
|
1494
2355
|
|
|
1495
|
-
|
|
2356
|
+
// A superset, not an exact match: the chevron additionally carries the
|
|
2357
|
+
// rotation styles, which live on the glyph precisely so a theme can reach
|
|
2358
|
+
// the transform through the same selector as the color. The guard that
|
|
2359
|
+
// matters is that every color/size class of a standalone icon is still
|
|
2360
|
+
// present — i.e. the default look has not drifted.
|
|
2361
|
+
expect(styleClasses(icon)).toEqual(
|
|
2362
|
+
expect.arrayContaining(styleClasses(refIcon)),
|
|
2363
|
+
);
|
|
1496
2364
|
});
|
|
1497
2365
|
|
|
1498
2366
|
it('exposes selector-indicator-icon so a theme reaches the icon size and per-state color', () => {
|
|
@@ -1519,7 +2387,165 @@ describe('Selector indicator (chevron) icon theme target', () => {
|
|
|
1519
2387
|
});
|
|
1520
2388
|
});
|
|
1521
2389
|
|
|
2390
|
+
describe('Selector section headings', () => {
|
|
2391
|
+
it('renders a section title as a plain heading inside the group, not a divider', async () => {
|
|
2392
|
+
const user = userEvent.setup();
|
|
2393
|
+
render(
|
|
2394
|
+
<Selector
|
|
2395
|
+
label="Fruit"
|
|
2396
|
+
options={[
|
|
2397
|
+
{
|
|
2398
|
+
type: 'section',
|
|
2399
|
+
title: 'Citrus',
|
|
2400
|
+
options: [
|
|
2401
|
+
{value: 'orange', label: 'Orange'},
|
|
2402
|
+
{value: 'lemon', label: 'Lemon'},
|
|
2403
|
+
],
|
|
2404
|
+
},
|
|
2405
|
+
]}
|
|
2406
|
+
value={undefined}
|
|
2407
|
+
onChange={() => {}}
|
|
2408
|
+
/>,
|
|
2409
|
+
);
|
|
2410
|
+
await user.click(screen.getByRole('combobox'));
|
|
2411
|
+
|
|
2412
|
+
// A labeled Divider used to stand in for the heading; it rendered a
|
|
2413
|
+
// role="separator" as a direct child of the listbox and stacked a second
|
|
2414
|
+
// rule under the search row's own.
|
|
2415
|
+
expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
2416
|
+
|
|
2417
|
+
const group = screen.getByRole('group', {name: 'Citrus', hidden: true});
|
|
2418
|
+
const heading = group.querySelector('.astryx-selector-section-heading');
|
|
2419
|
+
expect(heading).toBeTruthy();
|
|
2420
|
+
expect(heading).toHaveTextContent('Citrus');
|
|
2421
|
+
// The group already carries the title as its accessible name, so the
|
|
2422
|
+
// visible heading must not announce it a second time.
|
|
2423
|
+
expect(heading).toHaveAttribute('aria-hidden', 'true');
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
describe('Selector search focus ring', () => {
|
|
2428
|
+
// The ring is for keyboard focus only. `:focus-visible` cannot express that
|
|
2429
|
+
// on its own: per CSS Selectors 4 a pointer-focused text input matches it
|
|
2430
|
+
// too (verified in Chromium), which is why a modality gate sits alongside
|
|
2431
|
+
// it. jsdom does not implement `:focus-visible`, so these assert the gate;
|
|
2432
|
+
// the painted ring is verified in real Chromium.
|
|
2433
|
+
//
|
|
2434
|
+
// Focus moves into the search input on the frame after the panel opens, and
|
|
2435
|
+
// the gate is read at that moment: every case must wait for the focus to
|
|
2436
|
+
// land before asserting or typing, or a slow frame reads the modality of
|
|
2437
|
+
// whatever the test did next.
|
|
2438
|
+
beforeEach(() => {
|
|
2439
|
+
__resetInteractionModalityForTest();
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
const field = () =>
|
|
2443
|
+
screen.getByRole('combobox', {hidden: true}).parentElement;
|
|
2444
|
+
|
|
2445
|
+
const waitForSearchFocus = async () =>
|
|
2446
|
+
waitFor(() =>
|
|
2447
|
+
expect(screen.getByRole('combobox', {hidden: true})).toHaveFocus(),
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
it('does not ring when the panel is opened by mouse', async () => {
|
|
2451
|
+
const user = userEvent.setup();
|
|
2452
|
+
render(
|
|
2453
|
+
<Selector
|
|
2454
|
+
label="Fruit"
|
|
2455
|
+
options={OPTIONS}
|
|
2456
|
+
value={undefined}
|
|
2457
|
+
onChange={() => {}}
|
|
2458
|
+
hasSearch
|
|
2459
|
+
/>,
|
|
2460
|
+
);
|
|
2461
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2462
|
+
await waitForSearchFocus();
|
|
2463
|
+
expect(field()).not.toHaveAttribute('data-keyboard-focus');
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
it('does not ring when the query is typed after a mouse open', async () => {
|
|
2467
|
+
const user = userEvent.setup();
|
|
2468
|
+
render(
|
|
2469
|
+
<Selector
|
|
2470
|
+
label="Fruit"
|
|
2471
|
+
options={OPTIONS}
|
|
2472
|
+
value={undefined}
|
|
2473
|
+
onChange={() => {}}
|
|
2474
|
+
hasSearch
|
|
2475
|
+
/>,
|
|
2476
|
+
);
|
|
2477
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2478
|
+
await waitForSearchFocus();
|
|
2479
|
+
await user.keyboard('an');
|
|
2480
|
+
// Typing does not retroactively make a pointer focus a keyboard one; the
|
|
2481
|
+
// caret already shows where the text is going.
|
|
2482
|
+
expect(field()).not.toHaveAttribute('data-keyboard-focus');
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
it('rings when the panel is opened from the keyboard', async () => {
|
|
2486
|
+
const user = userEvent.setup();
|
|
2487
|
+
render(
|
|
2488
|
+
<Selector
|
|
2489
|
+
label="Fruit"
|
|
2490
|
+
options={OPTIONS}
|
|
2491
|
+
value={undefined}
|
|
2492
|
+
onChange={() => {}}
|
|
2493
|
+
hasSearch
|
|
2494
|
+
/>,
|
|
2495
|
+
);
|
|
2496
|
+
await user.tab();
|
|
2497
|
+
await user.keyboard('{Enter}');
|
|
2498
|
+
await waitForSearchFocus();
|
|
2499
|
+
expect(field()).toHaveAttribute('data-keyboard-focus', 'true');
|
|
2500
|
+
});
|
|
2501
|
+
});
|
|
2502
|
+
|
|
1522
2503
|
describe('Selector search affordances', () => {
|
|
2504
|
+
it('renders the search row seamlessly — no nested input box, a divider under it', async () => {
|
|
2505
|
+
const user = userEvent.setup();
|
|
2506
|
+
const {container} = render(
|
|
2507
|
+
<Selector
|
|
2508
|
+
label="Fruit"
|
|
2509
|
+
options={OPTIONS}
|
|
2510
|
+
value="Apple"
|
|
2511
|
+
onChange={() => {}}
|
|
2512
|
+
hasSearch
|
|
2513
|
+
/>,
|
|
2514
|
+
);
|
|
2515
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2516
|
+
|
|
2517
|
+
const search = screen.getByRole('combobox', {hidden: true});
|
|
2518
|
+
// The row is the outer gutter; the input sits inside the rounded field.
|
|
2519
|
+
const row = search.closest('.astryx-selector-search');
|
|
2520
|
+
const field = search.parentElement;
|
|
2521
|
+
if (!row || !field) {
|
|
2522
|
+
throw new Error('search row not found');
|
|
2523
|
+
}
|
|
2524
|
+
// The panel is already a bordered surface: the field inside it must not be
|
|
2525
|
+
// a second bordered box (this used to render a TextInput).
|
|
2526
|
+
expect(row).not.toHaveClass('astryx-text-input');
|
|
2527
|
+
expect(search.closest('.astryx-text-input')).toBeNull();
|
|
2528
|
+
// The field is a rounded box inset from the panel edge, shaped like the
|
|
2529
|
+
// option rows under it — not a full-bleed header strip.
|
|
2530
|
+
expect(field).not.toBe(row);
|
|
2531
|
+
expect(getComputedStyle(field).borderRadius).not.toBe('');
|
|
2532
|
+
// ...and a divider separates it from the options.
|
|
2533
|
+
const separator =
|
|
2534
|
+
container.ownerDocument.querySelector('[role="separator"]');
|
|
2535
|
+
if (!separator) {
|
|
2536
|
+
throw new Error('divider not found');
|
|
2537
|
+
}
|
|
2538
|
+
// Order: row, then divider, then the listbox.
|
|
2539
|
+
expect(
|
|
2540
|
+
row.compareDocumentPosition(separator) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2541
|
+
).toBeTruthy();
|
|
2542
|
+
const listbox = screen.getByRole('listbox', {hidden: true});
|
|
2543
|
+
expect(
|
|
2544
|
+
separator.compareDocumentPosition(listbox) &
|
|
2545
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2546
|
+
).toBeTruthy();
|
|
2547
|
+
});
|
|
2548
|
+
|
|
1523
2549
|
it('renders a decorative (aria-hidden) magnifier icon whenever hasSearch is on', async () => {
|
|
1524
2550
|
const user = userEvent.setup();
|
|
1525
2551
|
render(
|
|
@@ -1533,8 +2559,7 @@ describe('Selector search affordances', () => {
|
|
|
1533
2559
|
);
|
|
1534
2560
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1535
2561
|
const search = screen.getByRole('combobox', {hidden: true});
|
|
1536
|
-
// The
|
|
1537
|
-
// sits inside the input container as a sibling of the <input>.
|
|
2562
|
+
// The magnifier leads the search row, as a sibling of the <input>.
|
|
1538
2563
|
const container = search.parentElement;
|
|
1539
2564
|
const magnifier = container?.querySelector('.astryx-icon');
|
|
1540
2565
|
expect(magnifier).toBeTruthy();
|
|
@@ -1640,3 +2665,272 @@ describe('Selector search affordances', () => {
|
|
|
1640
2665
|
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1641
2666
|
});
|
|
1642
2667
|
});
|
|
2668
|
+
|
|
2669
|
+
describe('Selector selected-marker theme target (selector-check)', () => {
|
|
2670
|
+
const openOptions = (): HTMLElement[] => screen.getAllByRole('option', h);
|
|
2671
|
+
|
|
2672
|
+
it('renders the astryx-selector-check target on the selected row only', () => {
|
|
2673
|
+
render(
|
|
2674
|
+
<Selector
|
|
2675
|
+
label="Fruit"
|
|
2676
|
+
options={OPTIONS}
|
|
2677
|
+
value="Banana"
|
|
2678
|
+
onChange={() => {}}
|
|
2679
|
+
isDefaultOpen
|
|
2680
|
+
/>,
|
|
2681
|
+
);
|
|
2682
|
+
const options = openOptions();
|
|
2683
|
+
const selected = options.find(
|
|
2684
|
+
o => o.getAttribute('aria-selected') === 'true',
|
|
2685
|
+
)!;
|
|
2686
|
+
const check = selected.querySelector('.astryx-selector-check');
|
|
2687
|
+
expect(check).toBeInTheDocument();
|
|
2688
|
+
// The target lands on the checkmark glyph itself, so a theme can restyle or
|
|
2689
|
+
// hide it (e.g. to compose its own selected indicator via renderOption).
|
|
2690
|
+
expect(check).toHaveClass('astryx-icon');
|
|
2691
|
+
|
|
2692
|
+
const unselected = options.filter(
|
|
2693
|
+
o => o.getAttribute('aria-selected') !== 'true',
|
|
2694
|
+
);
|
|
2695
|
+
for (const row of unselected) {
|
|
2696
|
+
expect(
|
|
2697
|
+
row.querySelector('.astryx-selector-check'),
|
|
2698
|
+
).not.toBeInTheDocument();
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
2701
|
+
|
|
2702
|
+
it('renders the default checkmark byte-identically aside from the target class', () => {
|
|
2703
|
+
// The added target class is purely additive — it changes nothing about the
|
|
2704
|
+
// glyph's own color/size until a theme targets it.
|
|
2705
|
+
render(
|
|
2706
|
+
<Selector
|
|
2707
|
+
label="Fruit"
|
|
2708
|
+
options={OPTIONS}
|
|
2709
|
+
value="Banana"
|
|
2710
|
+
onChange={() => {}}
|
|
2711
|
+
isDefaultOpen
|
|
2712
|
+
/>,
|
|
2713
|
+
);
|
|
2714
|
+
const selected = screen
|
|
2715
|
+
.getAllByRole('option', h)
|
|
2716
|
+
.find(o => o.getAttribute('aria-selected') === 'true')!;
|
|
2717
|
+
const check = selected.querySelector(
|
|
2718
|
+
'.astryx-selector-check',
|
|
2719
|
+
) as HTMLElement;
|
|
2720
|
+
|
|
2721
|
+
const {container: refContainer} = render(
|
|
2722
|
+
<Icon icon="check" size="sm" color="accent" />,
|
|
2723
|
+
);
|
|
2724
|
+
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
2725
|
+
const styleClasses = (el: HTMLElement) =>
|
|
2726
|
+
el.className
|
|
2727
|
+
.split(' ')
|
|
2728
|
+
.filter(c => c !== 'astryx-selector-check')
|
|
2729
|
+
.sort();
|
|
2730
|
+
expect(styleClasses(check)).toEqual(styleClasses(refIcon));
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
it('exposes selector-check so a theme can hide or restyle the marker', () => {
|
|
2734
|
+
const theme = defineTheme({
|
|
2735
|
+
name: 'selector-check-test',
|
|
2736
|
+
components: {
|
|
2737
|
+
'selector-check': {
|
|
2738
|
+
base: {display: 'none'},
|
|
2739
|
+
},
|
|
2740
|
+
},
|
|
2741
|
+
});
|
|
2742
|
+
const css = generateThemeTestCSS(theme);
|
|
2743
|
+
expect(css).toContain('.astryx-selector-check {');
|
|
2744
|
+
expect(css).toContain('display: none');
|
|
2745
|
+
});
|
|
2746
|
+
});
|
|
2747
|
+
|
|
2748
|
+
describe('Selector disabled state theme target', () => {
|
|
2749
|
+
const getSelectorRoot = (container: HTMLElement): HTMLElement => {
|
|
2750
|
+
const root = container.querySelector('.astryx-selector');
|
|
2751
|
+
if (root == null) {
|
|
2752
|
+
throw new Error('selector root not found');
|
|
2753
|
+
}
|
|
2754
|
+
return root as HTMLElement;
|
|
2755
|
+
};
|
|
2756
|
+
|
|
2757
|
+
it('reflects data-disabled="disabled" on the root when disabled', () => {
|
|
2758
|
+
const {container} = render(
|
|
2759
|
+
<Selector
|
|
2760
|
+
label="Fruit"
|
|
2761
|
+
options={OPTIONS}
|
|
2762
|
+
onChange={() => {}}
|
|
2763
|
+
isDisabled
|
|
2764
|
+
/>,
|
|
2765
|
+
);
|
|
2766
|
+
expect(getSelectorRoot(container)).toHaveAttribute(
|
|
2767
|
+
'data-disabled',
|
|
2768
|
+
'disabled',
|
|
2769
|
+
);
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
it('omits the disabled class/attribute when enabled', () => {
|
|
2773
|
+
const {container} = render(
|
|
2774
|
+
<Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
|
|
2775
|
+
);
|
|
2776
|
+
const root = getSelectorRoot(container);
|
|
2777
|
+
expect(root).not.toHaveAttribute('data-disabled');
|
|
2778
|
+
expect(root).not.toHaveClass('disabled');
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
it('exposes the disabled state so a theme can key on it', () => {
|
|
2782
|
+
const theme = defineTheme({
|
|
2783
|
+
name: 'selector-disabled-state-test',
|
|
2784
|
+
components: {
|
|
2785
|
+
selector: {
|
|
2786
|
+
'disabled:disabled': {opacity: '0.4'},
|
|
2787
|
+
},
|
|
2788
|
+
},
|
|
2789
|
+
});
|
|
2790
|
+
const css = generateThemeTestCSS(theme);
|
|
2791
|
+
expect(css).toContain('.astryx-selector.disabled');
|
|
2792
|
+
expect(css).toContain('opacity: 0.4');
|
|
2793
|
+
});
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
describe('Selector indicatorPosition', () => {
|
|
2797
|
+
const openRows = (): HTMLElement[] => screen.getAllByRole('option', h);
|
|
2798
|
+
const rowFor = (label: string): HTMLElement =>
|
|
2799
|
+
openRows().find(row => row.textContent?.includes(label))!;
|
|
2800
|
+
|
|
2801
|
+
it('draws the mark after the option content by default', () => {
|
|
2802
|
+
render(
|
|
2803
|
+
<Selector
|
|
2804
|
+
label="Fruit"
|
|
2805
|
+
options={OPTIONS}
|
|
2806
|
+
value="Banana"
|
|
2807
|
+
onChange={() => {}}
|
|
2808
|
+
isDefaultOpen
|
|
2809
|
+
/>,
|
|
2810
|
+
);
|
|
2811
|
+
const row = rowFor('Banana');
|
|
2812
|
+
const mark = row.querySelector('.astryx-selector-check')!;
|
|
2813
|
+
const content = row.querySelector('.astryx-selector-option')!;
|
|
2814
|
+
expect(
|
|
2815
|
+
content.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2816
|
+
).toBeTruthy();
|
|
2817
|
+
});
|
|
2818
|
+
|
|
2819
|
+
it('draws the mark before the option content when set to start', () => {
|
|
2820
|
+
render(
|
|
2821
|
+
<Selector
|
|
2822
|
+
label="Fruit"
|
|
2823
|
+
options={OPTIONS}
|
|
2824
|
+
value="Banana"
|
|
2825
|
+
onChange={() => {}}
|
|
2826
|
+
indicatorPosition="start"
|
|
2827
|
+
isDefaultOpen
|
|
2828
|
+
/>,
|
|
2829
|
+
);
|
|
2830
|
+
const row = rowFor('Banana');
|
|
2831
|
+
const mark = row.querySelector('.astryx-selector-check')!;
|
|
2832
|
+
const content = row.querySelector('.astryx-selector-option')!;
|
|
2833
|
+
expect(
|
|
2834
|
+
content.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_PRECEDING,
|
|
2835
|
+
).toBeTruthy();
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
it('reserves the mark column on every row, at either position', () => {
|
|
2839
|
+
// The default check draws nothing when unchecked, so without a reserved
|
|
2840
|
+
// column the chosen row would be laid out differently from the rest —
|
|
2841
|
+
// indented at the start, truncating earlier at the end. Every row is two
|
|
2842
|
+
// children wide either way, so a row's geometry does not depend on whether
|
|
2843
|
+
// it happens to be the chosen one.
|
|
2844
|
+
const {unmount} = render(
|
|
2845
|
+
<Selector
|
|
2846
|
+
label="Fruit"
|
|
2847
|
+
options={OPTIONS}
|
|
2848
|
+
value="Banana"
|
|
2849
|
+
onChange={() => {}}
|
|
2850
|
+
indicatorPosition="start"
|
|
2851
|
+
isDefaultOpen
|
|
2852
|
+
/>,
|
|
2853
|
+
);
|
|
2854
|
+
for (const row of openRows()) {
|
|
2855
|
+
expect(row.children).toHaveLength(2);
|
|
2856
|
+
}
|
|
2857
|
+
unmount();
|
|
2858
|
+
|
|
2859
|
+
render(
|
|
2860
|
+
<Selector
|
|
2861
|
+
label="Fruit"
|
|
2862
|
+
options={OPTIONS}
|
|
2863
|
+
value="Banana"
|
|
2864
|
+
onChange={() => {}}
|
|
2865
|
+
isDefaultOpen
|
|
2866
|
+
/>,
|
|
2867
|
+
);
|
|
2868
|
+
for (const row of openRows()) {
|
|
2869
|
+
expect(row.children).toHaveLength(2);
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
|
|
2873
|
+
it('positions a themed replacement indicator the same way', () => {
|
|
2874
|
+
const theme = defineTheme({
|
|
2875
|
+
name: 'selector-start-radio-mark-test',
|
|
2876
|
+
indicators: {check: RadioIndicator},
|
|
2877
|
+
});
|
|
2878
|
+
render(
|
|
2879
|
+
<Theme theme={theme}>
|
|
2880
|
+
<Selector
|
|
2881
|
+
label="Fruit"
|
|
2882
|
+
options={OPTIONS}
|
|
2883
|
+
value="Banana"
|
|
2884
|
+
onChange={() => {}}
|
|
2885
|
+
indicatorPosition="start"
|
|
2886
|
+
isDefaultOpen
|
|
2887
|
+
/>
|
|
2888
|
+
</Theme>,
|
|
2889
|
+
);
|
|
2890
|
+
for (const row of openRows()) {
|
|
2891
|
+
const radio = row.querySelector('.astryx-radio')!;
|
|
2892
|
+
const content = row.querySelector('.astryx-selector-option')!;
|
|
2893
|
+
expect(
|
|
2894
|
+
content.compareDocumentPosition(radio) &
|
|
2895
|
+
Node.DOCUMENT_POSITION_PRECEDING,
|
|
2896
|
+
).toBeTruthy();
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
describe('Selector popup theme target', () => {
|
|
2902
|
+
// The surface is the same element whether or not the popup has a search
|
|
2903
|
+
// field — which is the reason the target lives there. Rendered on the
|
|
2904
|
+
// component's own content, it would land on the listbox in one branch and
|
|
2905
|
+
// on a wrapper in the other, so one theme rule would style two different
|
|
2906
|
+
// boxes.
|
|
2907
|
+
it.each([
|
|
2908
|
+
['without search', false],
|
|
2909
|
+
['with search', true],
|
|
2910
|
+
])(
|
|
2911
|
+
'puts astryx-selector-popup on the painting surface, %s',
|
|
2912
|
+
async (_label, hasSearch) => {
|
|
2913
|
+
const user = userEvent.setup();
|
|
2914
|
+
render(
|
|
2915
|
+
<Selector
|
|
2916
|
+
label="Fruit"
|
|
2917
|
+
options={['Apple', 'Banana']}
|
|
2918
|
+
value="Apple"
|
|
2919
|
+
onChange={() => {}}
|
|
2920
|
+
hasSearch={hasSearch}
|
|
2921
|
+
/>,
|
|
2922
|
+
);
|
|
2923
|
+
// The trigger is a combobox in the plain variant and a listbox-popup
|
|
2924
|
+
// button in the search variant; the surface is the same either way.
|
|
2925
|
+
await user.click(
|
|
2926
|
+
screen.queryByRole('combobox') ??
|
|
2927
|
+
screen.getByRole('button', {name: /Fruit/}),
|
|
2928
|
+
);
|
|
2929
|
+
|
|
2930
|
+
const popup = document.querySelector('.astryx-selector-popup');
|
|
2931
|
+
expect(popup).not.toBeNull();
|
|
2932
|
+
expect(popup).toHaveClass('astryx-popover-surface');
|
|
2933
|
+
expect(popup?.querySelector('[role="listbox"]')).not.toBeNull();
|
|
2934
|
+
},
|
|
2935
|
+
);
|
|
2936
|
+
});
|