@astryxdesign/core 0.7.5 → 0.7.6-canary.00146a7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AlertDialog/AlertDialog.d.ts +1 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +5 -5
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +177 -5
- package/dist/AppShell/observeMainOverflow.d.ts +9 -0
- package/dist/AppShell/observeMainOverflow.d.ts.map +1 -0
- package/dist/AppShell/observeMainOverflow.js +125 -0
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +8 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +7 -3
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +38 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -1
- package/dist/BottomSheet/useMobileKeyboard.js +44 -8
- package/dist/BottomSheet/useSheetGestures.d.ts +48 -6
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +97 -13
- package/dist/Breadcrumbs/BreadcrumbItem.js +4 -2
- package/dist/Button/Button.js +4 -4
- package/dist/Carousel/Carousel.d.ts +3 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +12 -3
- package/dist/Center/Center.d.ts +1 -1
- package/dist/Center/Center.js +1 -1
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +10 -5
- package/dist/Chat/ChatComposerDrawer.d.ts +1 -1
- package/dist/Chat/ChatComposerDrawer.js +1 -1
- package/dist/Chat/ChatComposerInput.d.ts +1 -1
- package/dist/Chat/ChatComposerInput.js +1 -1
- package/dist/Chat/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayout.js +1 -1
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +3 -1
- package/dist/Chat/ChatPastedTextToken.d.ts.map +1 -1
- package/dist/Chat/ChatPastedTextToken.js +15 -4
- package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
- package/dist/Chat/ChatSystemMessage.js +2 -1
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -5
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +5 -2
- package/dist/CheckboxList/CheckboxListContext.d.ts +8 -0
- package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +5 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +37 -4
- package/dist/Code/Code.d.ts +1 -1
- package/dist/Code/Code.js +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +8 -5
- package/dist/ComplexSelector/ComplexSelector.d.ts +2 -2
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +48 -6
- package/dist/ContextMenu/ContextMenu.d.ts +6 -0
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +31 -7
- package/dist/DateInput/DateInput.d.ts +3 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +12 -4
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +108 -23
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +8 -3
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +11 -7
- package/dist/DateInput/usePointerDragScroll.d.ts +10 -1
- package/dist/DateInput/usePointerDragScroll.d.ts.map +1 -1
- package/dist/DateInput/usePointerDragScroll.js +25 -9
- package/dist/DateInput/useScrollSettle.d.ts +3 -1
- package/dist/DateInput/useScrollSettle.d.ts.map +1 -1
- package/dist/DateInput/useScrollSettle.js +37 -8
- package/dist/DateRangeInput/DateRangeInput.d.ts +3 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +9 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +23 -12
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +30 -10
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +5 -4
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +2 -1
- package/dist/Drawer/Drawer.d.ts +2 -1
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +8 -3
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +56 -30
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +3 -2
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -3
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +2 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +4 -3
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +10 -6
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +15 -8
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +20 -2
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +31 -3
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
- package/dist/DropdownMenu/renderDropdownItems.js +21 -16
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +2 -1
- package/dist/Field/Field.d.ts +2 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/FieldLabel.d.ts +1 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +23 -6
- package/dist/Field/InputClearButton.js +3 -3
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +12 -7
- package/dist/Field/fieldStatusPlacement.d.ts +15 -0
- package/dist/Field/fieldStatusPlacement.d.ts.map +1 -0
- package/dist/Field/fieldStatusPlacement.js +1 -0
- package/dist/FieldStatus/FieldStatus.d.ts +16 -28
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +27 -5
- package/dist/FieldStatus/index.d.ts +7 -0
- package/dist/FieldStatus/index.d.ts.map +1 -1
- package/dist/FileInput/FileInput.d.ts +10 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +93 -36
- package/dist/Heading/Heading.js +1 -1
- package/dist/Icon/Icon.js +1 -1
- package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckIndicator.js +2 -1
- package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckboxIndicator.js +3 -1
- package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
- package/dist/Indicator/RadioIndicator.js +3 -1
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +26 -12
- package/dist/Item/ItemSwipeLayer.d.ts.map +1 -1
- package/dist/Item/ItemSwipeLayer.js +9 -3
- package/dist/Layer/popoverOperations.d.ts +27 -0
- package/dist/Layer/popoverOperations.d.ts.map +1 -0
- package/dist/Layer/popoverOperations.js +159 -0
- package/dist/Layer/useLayer.d.ts +9 -1
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +126 -69
- package/dist/Layer/useTouchTrigger.d.ts +35 -3
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
- package/dist/Layer/useTouchTrigger.js +80 -11
- package/dist/Layout/Layout.js +1 -1
- package/dist/Layout/LayoutContent.js +1 -1
- package/dist/Layout/LayoutFooter.js +1 -1
- package/dist/Layout/LayoutHeader.js +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +5 -0
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +18 -4
- package/dist/List/List.js +1 -1
- package/dist/List/ListItem.js +3 -3
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +1 -0
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +2 -1
- package/dist/MetadataList/MetadataListItem.d.ts.map +1 -1
- package/dist/MetadataList/MetadataListItem.js +2 -1
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +7 -2
- package/dist/MultiSelector/MultiSelector.d.ts +3 -3
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +61 -35
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +4 -2
- package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenuItem.js +5 -29
- package/dist/NumberInput/NumberInput.d.ts +6 -3
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +21 -4
- package/dist/NumberInput/numberParser.d.ts +3 -0
- package/dist/NumberInput/numberParser.d.ts.map +1 -1
- package/dist/NumberInput/numberParser.js +7 -44
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +27 -0
- package/dist/OverflowList/OverflowList.d.ts.map +1 -1
- package/dist/OverflowList/OverflowList.js +2 -1
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +34 -14
- package/dist/Popover/usePopover.d.ts +7 -1
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +18 -5
- package/dist/PowerSearch/PowerSearch.d.ts +3 -2
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +41 -32
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +16 -12
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +7 -2
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +19 -5
- package/dist/RadioList/RadioListReasonContext.d.ts +2 -0
- package/dist/RadioList/RadioListReasonContext.d.ts.map +1 -0
- package/dist/RadioList/RadioListReasonContext.js +22 -0
- package/dist/Resizable/resizableModel.d.ts +64 -0
- package/dist/Resizable/resizableModel.d.ts.map +1 -0
- package/dist/Resizable/resizableModel.js +287 -0
- package/dist/Resizable/useResizable.d.ts +2 -2
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +74 -288
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +22 -5
- package/dist/SegmentedControl/SegmentedControlContext.d.ts +6 -0
- package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +20 -4
- package/dist/Selector/Selector.d.ts +3 -3
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +14 -8
- package/dist/Selector/hooks.js +2 -2
- package/dist/Selector/useSelectorPresentation.d.ts +14 -1
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
- package/dist/Selector/useSelectorPresentation.js +22 -5
- package/dist/SideNav/SideNavItem.d.ts +2 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +18 -7
- package/dist/SideNav/railMotion.d.ts +5 -5
- package/dist/SideNav/railMotion.d.ts.map +1 -1
- package/dist/SideNav/railMotion.js +69 -177
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +110 -19
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +3 -3
- package/dist/Stack/Stack.js +1 -1
- package/dist/Stack/StackItem.js +1 -1
- package/dist/Stepper/Step.d.ts.map +1 -1
- package/dist/Stepper/Step.js +26 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +10 -3
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +16 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +7 -6
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +36 -10
- package/dist/Table/glidingDivider.d.ts +8 -1
- package/dist/Table/glidingDivider.d.ts.map +1 -1
- package/dist/Table/glidingDivider.js +8 -3
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +46 -58
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +1 -2
- package/dist/Text/Text.js +1 -1
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +23 -5
- package/dist/TextArea/TextArea.d.ts +3 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +13 -2
- package/dist/TextInput/TextInput.d.ts +3 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +3 -3
- package/dist/TimeInput/TimeInput.d.ts +9 -4
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +21 -14
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +5 -4
- package/dist/Timer/Timer.js +1 -1
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +2 -2
- package/dist/Timestamp/TimestampHoverCard.js +4 -4
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +60 -20
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +101 -54
- package/dist/Toast/useToast.d.ts.map +1 -1
- package/dist/Toast/useToast.js +8 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +11 -3
- package/dist/ToggleButton/ToggleButtonGroup.d.ts +7 -1
- package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButtonGroup.js +16 -2
- package/dist/Token/Token.d.ts +1 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +52 -5
- package/dist/Token/TokenLink.d.ts.map +1 -1
- package/dist/Token/TokenLink.js +14 -2
- package/dist/Tokenizer/Tokenizer.d.ts +3 -2
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +101 -56
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts +1 -1
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
- package/dist/Tokenizer/TokenizerBottomSheet.js +12 -18
- package/dist/Tokenizer/TokenizerClearAllMessageContext.d.ts +2 -0
- package/dist/Tokenizer/TokenizerClearAllMessageContext.d.ts.map +1 -0
- package/dist/Tokenizer/TokenizerClearAllMessageContext.js +17 -0
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +2 -1
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +7 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +1 -0
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +6 -3
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +31 -7
- package/dist/Typeahead/BaseTypeahead.d.ts +8 -5
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +303 -48
- package/dist/Typeahead/Typeahead.d.ts +4 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +43 -9
- package/dist/Typeahead/index.d.ts +1 -1
- package/dist/Typeahead/index.d.ts.map +1 -1
- package/dist/Typeahead/types.d.ts +26 -3
- package/dist/Typeahead/types.d.ts.map +1 -1
- package/dist/astryx.css +27 -6
- package/dist/gestures/GestureArena.d.ts +32 -1
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +110 -9
- package/dist/gestures/GestureHost.d.ts +351 -0
- package/dist/gestures/GestureHost.d.ts.map +1 -0
- package/dist/gestures/GestureHost.js +1773 -0
- package/dist/gestures/GestureRecognizer.d.ts +13 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +19 -1
- package/dist/gestures/LongPressGestureRecognizer.d.ts +1 -0
- package/dist/gestures/LongPressGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/LongPressGestureRecognizer.js +3 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +1 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PanGestureRecognizer.js +3 -0
- package/dist/gestures/PinchGestureRecognizer.d.ts +31 -0
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/PinchGestureRecognizer.js +128 -0
- package/dist/gestures/PressTracker.d.ts +112 -0
- package/dist/gestures/PressTracker.d.ts.map +1 -0
- package/dist/gestures/PressTracker.js +267 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +71 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +223 -0
- package/dist/gestures/SwipeGestureRecognizer.d.ts +37 -0
- package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/SwipeGestureRecognizer.js +148 -0
- package/dist/gestures/TapGestureRecognizer.d.ts +1 -0
- package/dist/gestures/TapGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/TapGestureRecognizer.js +3 -0
- package/dist/gestures/VelocityTracker.d.ts +6 -2
- package/dist/gestures/VelocityTracker.d.ts.map +1 -1
- package/dist/gestures/VelocityTracker.js +7 -3
- package/dist/gestures/haptics.d.ts +65 -0
- package/dist/gestures/haptics.d.ts.map +1 -0
- package/dist/gestures/haptics.js +178 -0
- package/dist/gestures/index.d.ts +37 -0
- package/dist/gestures/index.d.ts.map +1 -0
- package/dist/gestures/index.js +25 -0
- package/dist/gestures/nativeScroll.d.ts +42 -0
- package/dist/gestures/nativeScroll.d.ts.map +1 -0
- package/dist/gestures/nativeScroll.js +228 -0
- package/dist/gestures/touchAction.d.ts +60 -0
- package/dist/gestures/touchAction.d.ts.map +1 -0
- package/dist/gestures/touchAction.js +157 -0
- package/dist/gestures/touchActionRule.d.ts +34 -0
- package/dist/gestures/touchActionRule.d.ts.map +1 -0
- package/dist/gestures/touchActionRule.js +140 -0
- package/dist/gestures/types.d.ts +18 -7
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +22 -2
- package/dist/gestures/useGestureRecognizer.d.ts +56 -0
- package/dist/gestures/useGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/useGestureRecognizer.js +226 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/menuPressMarker.d.ts +13 -0
- package/dist/hooks/menuPressMarker.d.ts.map +1 -0
- package/dist/hooks/menuPressMarker.js +15 -0
- package/dist/hooks/modalAnnounce.d.ts.map +1 -1
- package/dist/hooks/modalAnnounce.js +107 -61
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -1
- package/dist/hooks/scrollKeyboardDelegation.js +7 -3
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
- package/dist/hooks/useAdaptivePresentation.js +4 -1
- package/dist/hooks/useAnnounce.d.ts +101 -42
- package/dist/hooks/useAnnounce.d.ts.map +1 -1
- package/dist/hooks/useAnnounce.js +161 -75
- package/dist/hooks/useAnnounceRenderedText.d.ts +11 -3
- package/dist/hooks/useAnnounceRenderedText.d.ts.map +1 -1
- package/dist/hooks/useAnnounceRenderedText.js +5 -3
- package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -1
- package/dist/hooks/useBusyAnnouncement.js +6 -12
- package/dist/hooks/useClickableContainer.d.ts +24 -4
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +41 -14
- package/dist/hooks/useFocusReturnVisibility.d.ts +8 -1
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -1
- package/dist/hooks/useFocusReturnVisibility.js +14 -8
- package/dist/hooks/useInputStatusIcon.d.ts +2 -2
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -2
- package/dist/hooks/useListFocus.d.ts +27 -0
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +35 -4
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +12 -1
- package/dist/hooks/useMenuPress.d.ts +2 -2
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +5 -6
- package/dist/hooks/useScrollOverflow.d.ts +2 -1
- package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
- package/dist/hooks/useScrollOverflow.js +56 -10
- package/dist/hooks/useTreeFocus.d.ts.map +1 -1
- package/dist/hooks/useTreeFocus.js +13 -5
- package/dist/hooks/useTypeahead.d.ts +3 -1
- package/dist/hooks/useTypeahead.d.ts.map +1 -1
- package/dist/hooks/useTypeahead.js +21 -15
- package/dist/i18n/generated-locales/af-ZA.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/af-ZA.generated.js +0 -5
- package/dist/i18n/generated-locales/ar-SA.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/ar-SA.generated.js +0 -5
- package/dist/i18n/generated-locales/ca-ES.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/ca-ES.generated.js +0 -5
- package/dist/i18n/generated-locales/cs-CZ.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/cs-CZ.generated.js +0 -5
- package/dist/i18n/generated-locales/da-DK.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/da-DK.generated.js +0 -5
- package/dist/i18n/generated-locales/de-DE.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/de-DE.generated.js +0 -5
- package/dist/i18n/generated-locales/el-GR.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/el-GR.generated.js +0 -5
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +14 -5
- package/dist/i18n/generated-locales/es-ES.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/es-ES.generated.js +0 -5
- package/dist/i18n/generated-locales/fi-FI.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/fi-FI.generated.js +0 -5
- package/dist/i18n/generated-locales/fr-FR.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/fr-FR.generated.js +0 -5
- package/dist/i18n/generated-locales/he-IL.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/he-IL.generated.js +0 -5
- package/dist/i18n/generated-locales/hu-HU.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/hu-HU.generated.js +0 -5
- package/dist/i18n/generated-locales/it-IT.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/it-IT.generated.js +0 -5
- package/dist/i18n/generated-locales/ja-JP.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/ja-JP.generated.js +0 -5
- package/dist/i18n/generated-locales/ko-KR.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/ko-KR.generated.js +0 -5
- package/dist/i18n/generated-locales/nl-NL.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/nl-NL.generated.js +0 -5
- package/dist/i18n/generated-locales/no-NO.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/no-NO.generated.js +0 -5
- package/dist/i18n/generated-locales/pl-PL.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pl-PL.generated.js +0 -5
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +14 -5
- package/dist/i18n/generated-locales/pt-BR.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pt-BR.generated.js +0 -5
- package/dist/i18n/generated-locales/pt-PT.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pt-PT.generated.js +0 -5
- package/dist/i18n/generated-locales/ro-RO.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/ro-RO.generated.js +0 -5
- package/dist/i18n/generated-locales/ru-RU.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/ru-RU.generated.js +0 -5
- package/dist/i18n/generated-locales/sr-SP.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/sr-SP.generated.js +0 -5
- package/dist/i18n/generated-locales/sv-SE.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/sv-SE.generated.js +0 -5
- package/dist/i18n/generated-locales/tr-TR.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/tr-TR.generated.js +0 -5
- package/dist/i18n/generated-locales/uk-UA.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/uk-UA.generated.js +0 -5
- package/dist/i18n/generated-locales/vi-VN.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/vi-VN.generated.js +0 -5
- package/dist/i18n/generated-locales/zh-CN.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/zh-CN.generated.js +0 -5
- package/dist/i18n/generated-locales/zh-TW.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/zh-TW.generated.js +0 -5
- package/dist/i18n/useListFormatter.d.ts +9 -0
- package/dist/i18n/useListFormatter.d.ts.map +1 -0
- package/dist/i18n/useListFormatter.js +41 -0
- package/dist/motion/animatedLayout.d.ts +130 -5
- package/dist/motion/animatedLayout.d.ts.map +1 -1
- package/dist/motion/animatedLayout.js +771 -44
- package/dist/motion/animator.d.ts +83 -8
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +739 -121
- package/dist/motion/coast.d.ts +4 -1
- package/dist/motion/coast.d.ts.map +1 -1
- package/dist/motion/coast.js +4 -2
- package/dist/motion/reducedMotion.d.ts.map +1 -1
- package/dist/motion/reducedMotion.js +6 -2
- package/dist/motion/reducedMotionPlan.d.ts +7 -0
- package/dist/motion/reducedMotionPlan.d.ts.map +1 -1
- package/dist/motion/reducedMotionPlan.js +14 -1
- package/dist/motion/resolveMotion.d.ts +42 -0
- package/dist/motion/resolveMotion.d.ts.map +1 -0
- package/dist/motion/resolveMotion.js +43 -0
- package/dist/motion/roleValue.d.ts +2 -1
- package/dist/motion/roleValue.d.ts.map +1 -1
- package/dist/motion/roleValue.js +2 -1
- package/dist/motion/uikit/timing.d.ts +7 -1
- package/dist/motion/uikit/timing.d.ts.map +1 -1
- package/dist/motion/uikit/timing.js +19 -1
- package/dist/privateAttributes/segmentDisabled.stylex.d.ts +5 -0
- package/dist/privateAttributes/segmentDisabled.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/segmentDisabled.stylex.js +28 -0
- package/dist/theme/Theme.d.ts +10 -0
- package/dist/theme/Theme.d.ts.map +1 -1
- package/dist/theme/Theme.js +8 -4
- package/dist/theme/defineTheme.d.ts +19 -5
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +64 -4
- package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
- package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
- package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
- package/dist/theme/domainTokens/index.d.ts +5 -4
- package/dist/theme/domainTokens/index.d.ts.map +1 -1
- package/dist/theme/domainTokens/index.js +5 -9
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/motionKnobs.d.ts +56 -0
- package/dist/theme/motionKnobs.d.ts.map +1 -0
- package/dist/theme/motionKnobs.js +48 -0
- package/dist/theme/resolveThemeValues.d.ts +11 -4
- package/dist/theme/resolveThemeValues.d.ts.map +1 -1
- package/dist/theme/resolveThemeValues.js +40 -4
- package/dist/theme/themeAdaptations.d.ts +7 -3
- package/dist/theme/themeAdaptations.d.ts.map +1 -1
- package/dist/theme/themeAdaptations.js +36 -7
- package/dist/theme/themeMotion.d.ts +96 -0
- package/dist/theme/themeMotion.d.ts.map +1 -0
- package/dist/theme/themeMotion.js +177 -0
- package/dist/theme/tokenReferences.d.ts +31 -0
- package/dist/theme/tokenReferences.d.ts.map +1 -0
- package/dist/theme/tokenReferences.js +132 -0
- package/dist/theme/tokens.d.ts +1 -0
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +9 -129
- 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 +12 -0
- package/dist/theme/useTheme.d.ts.map +1 -1
- package/dist/theme/useTheme.js +7 -1
- package/dist/utils/focusOutline.stylex.d.ts +6 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +7 -0
- package/dist/utils/focusReturn.d.ts +19 -1
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +13 -4
- package/dist/utils/getNodeText.d.ts +8 -0
- package/dist/utils/getNodeText.d.ts.map +1 -0
- package/dist/utils/getNodeText.js +38 -0
- package/dist/utils/interactionModality.d.ts +84 -4
- package/dist/utils/interactionModality.d.ts.map +1 -1
- package/dist/utils/interactionModality.js +152 -16
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +17 -8
- package/dist/utils/plainDate.d.ts +2 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +46 -3
- package/dist/utils/presentation.d.ts +50 -0
- package/dist/utils/presentation.d.ts.map +1 -0
- package/dist/utils/presentation.js +125 -0
- package/dist/utils/pressDisabled.d.ts +21 -0
- package/dist/utils/pressDisabled.d.ts.map +1 -0
- package/dist/utils/pressDisabled.js +32 -0
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +25 -4
- package/dist/utils/primaryClick.d.ts +11 -0
- package/dist/utils/primaryClick.d.ts.map +1 -0
- package/dist/utils/primaryClick.js +25 -0
- package/dist/utils/sharedMutationObserver.d.ts +19 -11
- package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
- package/dist/utils/sharedMutationObserver.js +66 -26
- package/dist/utils/timeFormat.d.ts +47 -0
- package/dist/utils/timeFormat.d.ts.map +1 -0
- package/dist/utils/timeFormat.js +240 -0
- package/dist/utils/touchPresentation.d.ts +2 -2
- package/dist/utils/touchPresentation.d.ts.map +1 -1
- package/dist/utils/touchPresentation.js +13 -8
- package/dist/utils/touchPresentation.stylex.d.ts +15 -0
- package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
- package/dist/utils/touchPresentation.stylex.js +33 -0
- package/dist/utils/touchQueries.d.ts +13 -0
- package/dist/utils/touchQueries.d.ts.map +1 -0
- package/dist/utils/touchQueries.js +15 -0
- package/dist/utils/typeaheadMatch.d.ts +24 -0
- package/dist/utils/typeaheadMatch.d.ts.map +1 -0
- package/dist/utils/typeaheadMatch.js +112 -0
- package/dist/utils/unicodeDigits.d.ts +20 -0
- package/dist/utils/unicodeDigits.d.ts.map +1 -0
- package/dist/utils/unicodeDigits.js +70 -0
- package/dist/utils/useFocusAcrossSwap.d.ts +19 -0
- package/dist/utils/useFocusAcrossSwap.d.ts.map +1 -0
- package/dist/utils/useFocusAcrossSwap.js +41 -0
- package/locales/af-ZA.json +0 -21
- package/locales/ar-SA.json +0 -21
- package/locales/ca-ES.json +0 -21
- package/locales/cs-CZ.json +0 -21
- package/locales/da-DK.json +0 -21
- package/locales/de-DE.json +0 -21
- package/locales/el-GR.json +0 -21
- package/locales/en.json +60 -24
- package/locales/es-ES.json +0 -21
- package/locales/fi-FI.json +0 -21
- package/locales/fr-FR.json +0 -21
- package/locales/he-IL.json +0 -21
- package/locales/hu-HU.json +0 -21
- package/locales/it-IT.json +0 -21
- package/locales/ja-JP.json +0 -21
- package/locales/ko-KR.json +0 -21
- package/locales/nl-NL.json +0 -21
- package/locales/no-NO.json +0 -21
- package/locales/pl-PL.json +0 -21
- package/locales/pseudo.json +41 -14
- package/locales/pt-BR.json +0 -21
- package/locales/pt-PT.json +0 -21
- package/locales/ro-RO.json +0 -21
- package/locales/ru-RU.json +0 -21
- package/locales/sr-SP.json +0 -21
- package/locales/sv-SE.json +0 -21
- package/locales/tr-TR.json +0 -21
- package/locales/uk-UA.json +0 -21
- package/locales/vi-VN.json +0 -21
- package/locales/zh-CN.json +0 -21
- package/locales/zh-TW.json +0 -21
- package/package.json +20 -5
- package/src/AlertDialog/AlertDialog.tsx +6 -5
- package/src/AppShell/AppShell.doc.mjs +1 -1
- package/src/AppShell/AppShell.tsx +216 -5
- package/src/AppShell/observeMainOverflow.ts +143 -0
- package/src/Avatar/Avatar.tsx +18 -1
- package/src/Badge/Badge.doc.mjs +1 -1
- package/src/Badge/Badge.tsx +20 -2
- package/src/BottomSheet/BottomSheet.tsx +5 -3
- package/src/BottomSheet/BottomSheetPanel.tsx +41 -9
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -1
- package/src/BottomSheet/useMobileKeyboard.ts +51 -10
- package/src/BottomSheet/useSheetGestures.ts +129 -21
- package/src/Breadcrumbs/BreadcrumbItem.tsx +2 -1
- package/src/Button/Button.tsx +4 -4
- package/src/Carousel/Carousel.doc.mjs +1 -1
- package/src/Carousel/Carousel.tsx +10 -2
- package/src/Center/Center.tsx +1 -1
- package/src/Chat/ChatComposer.tsx +7 -10
- package/src/Chat/ChatComposerDrawer.tsx +1 -1
- package/src/Chat/ChatComposerInput.tsx +1 -1
- package/src/Chat/ChatLayout.tsx +1 -6
- package/src/Chat/ChatMessageList.tsx +1 -0
- package/src/Chat/ChatPastedTextToken.tsx +14 -4
- package/src/Chat/ChatSystemMessage.tsx +4 -1
- package/src/Chat/ChatToolCalls.tsx +5 -0
- package/src/CheckboxInput/CheckboxInput.tsx +6 -5
- package/src/CheckboxList/CheckboxList.tsx +7 -1
- package/src/CheckboxList/CheckboxListContext.tsx +8 -0
- package/src/CheckboxList/CheckboxListItem.tsx +8 -2
- package/src/ClickableCard/ClickableCard.tsx +48 -4
- package/src/Code/Code.tsx +1 -1
- package/src/CommandPalette/CommandPaletteInput.tsx +5 -8
- package/src/ComplexSelector/ComplexSelector.tsx +63 -8
- package/src/ContextMenu/ContextMenu.tsx +30 -6
- package/src/DateInput/DateInput.tsx +31 -16
- package/src/DateInput/MonthScroller.tsx +108 -24
- package/src/DateInput/TouchDateField.tsx +14 -3
- package/src/DateInput/Wheel.tsx +9 -6
- package/src/DateInput/usePointerDragScroll.ts +34 -10
- package/src/DateInput/useScrollSettle.ts +38 -7
- package/src/DateRangeInput/DateRangeInput.tsx +36 -17
- package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
- package/src/DateTimeInput/DateTimeInput.tsx +46 -24
- package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
- package/src/Dialog/Dialog.tsx +28 -11
- package/src/Dialog/DialogHeader.tsx +5 -4
- package/src/Divider/Divider.tsx +2 -1
- package/src/Drawer/Drawer.tsx +14 -3
- package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenu.tsx +67 -30
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +15 -5
- package/src/DropdownMenu/DropdownMenuGroup.tsx +2 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +7 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +16 -6
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -4
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +12 -8
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +36 -7
- package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
- package/src/EmptyState/EmptyState.tsx +2 -1
- package/src/Field/Field.doc.mjs +2 -1
- package/src/Field/Field.tsx +2 -2
- package/src/Field/FieldLabel.doc.mjs +4 -2
- package/src/Field/FieldLabel.tsx +23 -6
- package/src/Field/InputClearButton.tsx +3 -3
- package/src/Field/PanelSearchInput.tsx +9 -10
- package/src/Field/fieldStatusPlacement.ts +18 -0
- package/src/FieldStatus/FieldStatus.tsx +35 -5
- package/src/FieldStatus/index.ts +7 -0
- package/src/FileInput/FileInput.doc.mjs +3 -3
- package/src/FileInput/FileInput.tsx +107 -37
- package/src/Heading/Heading.tsx +1 -1
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/useHoverCard.doc.mjs +1 -1
- package/src/Icon/Icon.tsx +1 -1
- package/src/Indicator/CheckIndicator.tsx +2 -1
- package/src/Indicator/CheckboxIndicator.tsx +3 -1
- package/src/Indicator/RadioIndicator.tsx +3 -1
- package/src/Item/Item.tsx +21 -13
- package/src/Item/ItemSwipeLayer.tsx +9 -2
- package/src/Layer/popoverOperations.ts +172 -0
- package/src/Layer/useLayer.tsx +168 -81
- package/src/Layer/useTouchTrigger.ts +107 -18
- package/src/Layout/Layout.tsx +1 -1
- package/src/Layout/LayoutContent.tsx +1 -1
- package/src/Layout/LayoutFooter.tsx +1 -1
- package/src/Layout/LayoutHeader.tsx +1 -1
- package/src/Lightbox/Lightbox.tsx +5 -0
- package/src/Link/Link.tsx +20 -2
- package/src/List/List.tsx +1 -1
- package/src/List/ListItem.tsx +3 -3
- package/src/Markdown/Markdown.tsx +1 -0
- package/src/MetadataList/MetadataList.tsx +4 -2
- package/src/MetadataList/MetadataListItem.tsx +2 -1
- package/src/MobileNav/MobileNav.tsx +9 -1
- package/src/MultiSelector/MultiSelector.tsx +82 -46
- package/src/MultiSelector/hooks.ts +7 -5
- package/src/NavMenu/NavHeadingMenuItem.tsx +5 -39
- package/src/NumberInput/NumberInput.doc.mjs +13 -7
- package/src/NumberInput/NumberInput.tsx +39 -6
- package/src/NumberInput/numberParser.ts +7 -45
- package/src/Outline/Outline.tsx +23 -0
- package/src/OverflowList/OverflowList.tsx +2 -1
- package/src/Pagination/Pagination.tsx +1 -0
- package/src/Popover/Popover.tsx +36 -16
- package/src/Popover/usePopover.tsx +17 -5
- package/src/PowerSearch/PowerSearch.tsx +46 -33
- package/src/ProgressBar/ProgressBar.tsx +58 -10
- package/src/RadioList/RadioList.tsx +13 -2
- package/src/RadioList/RadioListItem.tsx +20 -3
- package/src/RadioList/RadioListReasonContext.ts +25 -0
- package/src/Resizable/resizableModel.ts +364 -0
- package/src/Resizable/useResizable.ts +88 -353
- package/src/SegmentedControl/SegmentedControl.tsx +43 -8
- package/src/SegmentedControl/SegmentedControlContext.ts +6 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +23 -6
- package/src/Selector/Selector.tsx +23 -9
- package/src/Selector/hooks.ts +2 -2
- package/src/Selector/useSelectorPresentation.ts +39 -4
- package/src/SideNav/SideNavItem.tsx +27 -10
- package/src/SideNav/railMotion.ts +73 -179
- package/src/Skeleton/Skeleton.tsx +3 -1
- package/src/Slider/Slider.tsx +124 -22
- package/src/Spinner/Spinner.tsx +5 -3
- package/src/Stack/Stack.tsx +1 -1
- package/src/Stack/StackItem.tsx +1 -1
- package/src/Stepper/Step.tsx +33 -6
- package/src/Stepper/Stepper.doc.mjs +1 -1
- package/src/Switch/Switch.tsx +11 -3
- package/src/TabList/TabList.tsx +16 -1
- package/src/Table/BaseTable.tsx +10 -6
- package/src/Table/Table.tsx +43 -11
- package/src/Table/glidingDivider.ts +8 -3
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
- package/src/Table/plugins/sortable/useTableSortable.tsx +46 -73
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +1 -2
- package/src/Text/Text.tsx +1 -1
- package/src/Text/useTruncation.ts +23 -5
- package/src/TextArea/TextArea.tsx +18 -3
- package/src/TextInput/TextInput.tsx +5 -3
- package/src/TimeInput/TimeInput.doc.mjs +8 -8
- package/src/TimeInput/TimeInput.tsx +38 -20
- package/src/TimeInput/TouchTimeField.tsx +4 -6
- package/src/Timer/Timer.tsx +1 -1
- package/src/Timestamp/Timestamp.tsx +6 -2
- package/src/Timestamp/TimestampHoverCard.tsx +4 -4
- package/src/Toast/Toast.tsx +104 -36
- package/src/Toast/ToastViewport.tsx +121 -53
- package/src/Toast/useToast.tsx +8 -3
- package/src/ToggleButton/ToggleButton.tsx +10 -3
- package/src/ToggleButton/ToggleButtonGroup.tsx +20 -1
- package/src/Token/Token.doc.mjs +2 -2
- package/src/Token/Token.tsx +75 -5
- package/src/Token/TokenLink.tsx +21 -7
- package/src/Tokenizer/Tokenizer.doc.mjs +12 -8
- package/src/Tokenizer/Tokenizer.tsx +119 -65
- package/src/Tokenizer/TokenizerBottomSheet.tsx +12 -18
- package/src/Tokenizer/TokenizerClearAllMessageContext.ts +21 -0
- package/src/Toolbar/Toolbar.tsx +2 -1
- package/src/Tooltip/Tooltip.doc.mjs +3 -3
- package/src/Tooltip/useTooltip.doc.mjs +1 -1
- package/src/TopNav/TopNavMegaMenu.tsx +8 -4
- package/src/TopNav/TopNavMenu.tsx +1 -0
- package/src/TreeList/TreeList.tsx +6 -3
- package/src/TreeList/TreeListItem.tsx +31 -7
- package/src/Typeahead/BaseTypeahead.doc.mjs +10 -7
- package/src/Typeahead/BaseTypeahead.tsx +361 -48
- package/src/Typeahead/Typeahead.doc.mjs +21 -4
- package/src/Typeahead/Typeahead.tsx +53 -11
- package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
- package/src/Typeahead/index.ts +1 -1
- package/src/Typeahead/types.ts +27 -3
- package/src/gestures/GestureArena.ts +111 -11
- package/src/gestures/GestureHost.ts +2064 -0
- package/src/gestures/GestureRecognizer.ts +21 -1
- package/src/gestures/LongPressGestureRecognizer.ts +4 -0
- package/src/gestures/PanGestureRecognizer.ts +4 -0
- package/src/gestures/PinchGestureRecognizer.ts +148 -0
- package/src/gestures/PressTracker.ts +386 -0
- package/src/gestures/README.md +420 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +256 -0
- package/src/gestures/SwipeGestureRecognizer.ts +197 -0
- package/src/gestures/TapGestureRecognizer.ts +4 -0
- package/src/gestures/VelocityTracker.ts +10 -3
- package/src/gestures/haptics.ts +196 -0
- package/src/gestures/index.ts +79 -0
- package/src/gestures/nativeScroll.ts +277 -0
- package/src/gestures/touchAction.ts +191 -0
- package/src/gestures/touchActionRule.ts +157 -0
- package/src/gestures/types.ts +30 -9
- package/src/gestures/useGestureRecognizer.ts +305 -0
- package/src/hooks/index.ts +5 -1
- package/src/hooks/menuPressMarker.ts +15 -0
- package/src/hooks/modalAnnounce.ts +129 -72
- package/src/hooks/scrollKeyboardDelegation.ts +9 -3
- package/src/hooks/useAdaptivePresentation.ts +4 -1
- package/src/hooks/useAnnounce.doc.mjs +35 -3
- package/src/hooks/useAnnounce.ts +209 -102
- package/src/hooks/useAnnounceRenderedText.ts +17 -3
- package/src/hooks/useBusyAnnouncement.ts +7 -10
- package/src/hooks/useClickableContainer.ts +48 -17
- package/src/hooks/useFocusReturnVisibility.ts +15 -12
- package/src/hooks/useInputStatusIcon.tsx +2 -2
- package/src/hooks/useListFocus.ts +54 -3
- package/src/hooks/useMenuHover.ts +15 -1
- package/src/hooks/useMenuPress.ts +5 -5
- package/src/hooks/useScrollOverflow.doc.mjs +1 -1
- package/src/hooks/useScrollOverflow.ts +59 -10
- package/src/hooks/useTreeFocus.doc.mjs +2 -2
- package/src/hooks/useTreeFocus.ts +16 -10
- package/src/hooks/useTypeahead.doc.mjs +2 -2
- package/src/hooks/useTypeahead.ts +21 -20
- package/src/i18n/generated-locales/af-ZA.generated.ts +0 -7
- package/src/i18n/generated-locales/ar-SA.generated.ts +0 -6
- package/src/i18n/generated-locales/ca-ES.generated.ts +0 -7
- package/src/i18n/generated-locales/cs-CZ.generated.ts +0 -6
- package/src/i18n/generated-locales/da-DK.generated.ts +0 -6
- package/src/i18n/generated-locales/de-DE.generated.ts +0 -6
- package/src/i18n/generated-locales/el-GR.generated.ts +0 -7
- package/src/i18n/generated-locales/en.generated.ts +17 -6
- package/src/i18n/generated-locales/es-ES.generated.ts +0 -7
- package/src/i18n/generated-locales/fi-FI.generated.ts +0 -6
- package/src/i18n/generated-locales/fr-FR.generated.ts +0 -7
- package/src/i18n/generated-locales/he-IL.generated.ts +0 -6
- package/src/i18n/generated-locales/hu-HU.generated.ts +0 -6
- package/src/i18n/generated-locales/it-IT.generated.ts +0 -7
- package/src/i18n/generated-locales/ja-JP.generated.ts +0 -6
- package/src/i18n/generated-locales/ko-KR.generated.ts +0 -6
- package/src/i18n/generated-locales/nl-NL.generated.ts +0 -6
- package/src/i18n/generated-locales/no-NO.generated.ts +0 -6
- package/src/i18n/generated-locales/pl-PL.generated.ts +0 -7
- package/src/i18n/generated-locales/pseudo.generated.ts +17 -6
- package/src/i18n/generated-locales/pt-BR.generated.ts +0 -7
- package/src/i18n/generated-locales/pt-PT.generated.ts +0 -7
- package/src/i18n/generated-locales/ro-RO.generated.ts +0 -6
- package/src/i18n/generated-locales/ru-RU.generated.ts +0 -7
- package/src/i18n/generated-locales/sr-SP.generated.ts +0 -6
- package/src/i18n/generated-locales/sv-SE.generated.ts +0 -6
- package/src/i18n/generated-locales/tr-TR.generated.ts +0 -7
- package/src/i18n/generated-locales/uk-UA.generated.ts +0 -7
- package/src/i18n/generated-locales/vi-VN.generated.ts +0 -6
- package/src/i18n/generated-locales/zh-CN.generated.ts +0 -6
- package/src/i18n/generated-locales/zh-TW.generated.ts +0 -6
- package/src/i18n/useListFormatter.ts +42 -0
- package/src/motion/MEASUREMENTS.md +2 -1
- package/src/motion/README.md +113 -33
- package/src/motion/animatedLayout.ts +977 -45
- package/src/motion/animator.ts +987 -132
- package/src/motion/coast.ts +10 -3
- package/src/motion/reducedMotion.ts +12 -2
- package/src/motion/reducedMotionPlan.ts +20 -1
- package/src/motion/resolveMotion.ts +68 -0
- package/src/motion/roleValue.ts +2 -1
- package/src/motion/uikit/timing.ts +15 -1
- package/src/privateAttributes/segmentDisabled.stylex.ts +28 -0
- package/src/theme/Theme.doc.mjs +1 -1
- package/src/theme/Theme.tsx +8 -4
- package/src/theme/defineTheme.ts +117 -8
- package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
- package/src/theme/domainTokens/index.ts +6 -10
- package/src/theme/index.ts +5 -0
- package/src/theme/motionKnobs.ts +82 -0
- package/src/theme/resolveThemeValues.ts +68 -6
- package/src/theme/themeAdaptations.ts +52 -12
- package/src/theme/themeMotion.ts +221 -0
- package/src/theme/tokenReferences.ts +149 -0
- package/src/theme/tokens.stylex.ts +10 -0
- package/src/theme/tokens.ts +15 -158
- package/src/theme/useTheme.ts +6 -1
- package/src/utils/focusOutline.stylex.ts +28 -0
- package/src/utils/focusReturn.ts +19 -6
- package/src/utils/getNodeText.ts +38 -0
- package/src/utils/interactionModality.ts +197 -17
- package/src/utils/interactionOverlay.stylex.ts +12 -3
- package/src/utils/plainDate.ts +58 -3
- package/src/utils/presentation.ts +180 -0
- package/src/utils/pressDisabled.ts +32 -0
- package/src/utils/pressFeedback.ts +39 -14
- package/src/utils/primaryClick.ts +28 -0
- package/src/utils/sharedMutationObserver.ts +87 -29
- package/src/utils/timeFormat.ts +314 -0
- package/src/utils/touchPresentation.stylex.ts +43 -0
- package/src/utils/touchPresentation.ts +13 -8
- package/src/utils/touchQueries.ts +15 -0
- package/src/utils/typeaheadMatch.ts +141 -0
- package/src/utils/unicodeDigits.ts +71 -0
- package/src/utils/useFocusAcrossSwap.ts +55 -0
- package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.d.ts +0 -2
- package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.js +0 -132
- package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.d.ts +0 -2
- package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.js +0 -78
- package/dist/Lightbox/LightboxViewport.a11y.browser.spec.d.ts +0 -2
- package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +0 -65
- package/dist/List/ListMarkers.a11y.chromium.spec.d.ts +0 -2
- package/dist/List/ListMarkers.a11y.chromium.spec.js +0 -677
- package/dist/Markdown/Markdown.fr23.bench.d.ts +0 -2
- package/dist/Markdown/Markdown.fr23.bench.d.ts.map +0 -1
- package/dist/Markdown/Markdown.fr23.bench.js +0 -152
- package/dist/Markdown/MarkdownTable.a11y.chromium.spec.d.ts +0 -2
- package/dist/Markdown/MarkdownTable.a11y.chromium.spec.js +0 -590
- package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.d.ts +0 -2
- package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.js +0 -657
- package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.d.ts +0 -2
- package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.js +0 -153
- package/dist/MultiSelector/CreateRow.a11y.chromium.spec.d.ts +0 -2
- package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +0 -116
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +0 -2
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +0 -49
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.d.ts +0 -2
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +0 -273
- package/dist/Outline/OutlineFocus.a11y.chromium.spec.d.ts +0 -2
- package/dist/Outline/OutlineFocus.a11y.chromium.spec.js +0 -421
- package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.d.ts +0 -2
- package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.js +0 -118
- package/dist/reset.a11y.chromium.spec.d.ts +0 -2
- package/dist/reset.a11y.chromium.spec.js +0 -231
- package/dist/reset.until-found.a11y.browser.spec.d.ts +0 -2
- package/dist/reset.until-found.a11y.browser.spec.js +0 -72
- package/src/AlertDialog/AlertDialog.spec.md +0 -259
- package/src/AlertDialog/AlertDialog.test.tsx +0 -519
- package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +0 -116
- package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +0 -52
- package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +0 -68
- package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +0 -144
- package/src/AlertDialog/useImperativeAlertDialog.test.tsx +0 -163
- package/src/AppShell/AppShell.spec.md +0 -334
- package/src/AppShell/AppShell.test.tsx +0 -1148
- package/src/AspectRatio/AspectRatio.spec.md +0 -179
- package/src/AspectRatio/AspectRatio.test.tsx +0 -398
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +0 -158
- package/src/Avatar/Avatar.spec.md +0 -253
- package/src/Avatar/Avatar.test.tsx +0 -867
- package/src/Avatar/AvatarStatusDot.spec.md +0 -208
- package/src/Avatar/AvatarStatusDot.test.tsx +0 -306
- package/src/AvatarGroup/AvatarGroup.spec.md +0 -205
- package/src/AvatarGroup/AvatarGroup.test.tsx +0 -313
- package/src/AvatarGroup/AvatarGroupOverflow.spec.md +0 -210
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +0 -263
- package/src/Badge/Badge.spec.md +0 -207
- package/src/Badge/Badge.test.tsx +0 -125
- package/src/Banner/Banner.spec.md +0 -282
- package/src/Banner/Banner.test.tsx +0 -879
- package/src/Blockquote/Blockquote.spec.md +0 -171
- package/src/Blockquote/Blockquote.test.tsx +0 -121
- package/src/BottomSheet/BottomSheet.spec.md +0 -457
- package/src/BottomSheet/BottomSheet.test.tsx +0 -3108
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +0 -248
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +0 -195
- package/src/BottomSheet/BottomSheetPanel.test.tsx +0 -549
- package/src/BottomSheet/BottomSheetReplace.test.tsx +0 -1638
- package/src/BottomSheet/BottomSheetStacking.announce.test.tsx +0 -246
- package/src/BottomSheet/BottomSheetStacking.test.tsx +0 -3184
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +0 -367
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +0 -2010
- package/src/BottomSheet/__tests__/BottomSheetDrag.a11y.chromium.spec.ts +0 -129
- package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +0 -97
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +0 -415
- package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +0 -462
- package/src/BottomSheet/__tests__/BottomSheetPinchZoom.a11y.chromium.spec.ts +0 -247
- package/src/BottomSheet/__tests__/BottomSheetToastOutlet.a11y.chromium.spec.ts +0 -83
- package/src/BottomSheet/snapOffsets.test.ts +0 -254
- package/src/BottomSheet/useSheetGestures.test.ts +0 -1638
- package/src/Breadcrumbs/BreadcrumbItem.spec.md +0 -269
- package/src/Breadcrumbs/Breadcrumbs.spec.md +0 -165
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +0 -921
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +0 -120
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.renders.tsx +0 -45
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.states.ts +0 -81
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.test.tsx +0 -125
- package/src/Button/Button.spec.md +0 -322
- package/src/Button/Button.test.tsx +0 -864
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +0 -355
- package/src/Button/__tests__/Button.a11y.harnesses.tsx +0 -47
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -25
- package/src/Button/__tests__/Button.a11y.renders.tsx +0 -139
- package/src/Button/__tests__/Button.a11y.states.ts +0 -317
- package/src/Button/__tests__/Button.a11y.test.tsx +0 -187
- package/src/ButtonGroup/ButtonGroup.spec.md +0 -220
- package/src/ButtonGroup/ButtonGroup.test.tsx +0 -1025
- package/src/Calendar/Calendar.spec.md +0 -209
- package/src/Calendar/Calendar.test.tsx +0 -2186
- package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +0 -127
- package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +0 -12
- package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +0 -57
- package/src/Calendar/__tests__/Calendar.a11y.states.ts +0 -139
- package/src/Calendar/__tests__/Calendar.a11y.test.tsx +0 -137
- package/src/Calendar/dayCellUtils.test.ts +0 -520
- package/src/Calendar/getInitialFocusDate.test.ts +0 -94
- package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +0 -54
- package/src/Calendar/hooks/useCalendarConstraints.test.ts +0 -153
- package/src/Calendar/hooks/useCalendarDays.test.ts +0 -265
- package/src/Calendar/hooks/useCalendarNavigation.test.ts +0 -252
- package/src/Card/Card.spec.md +0 -205
- package/src/Card/Card.test.tsx +0 -202
- package/src/Carousel/Carousel.spec.md +0 -146
- package/src/Carousel/Carousel.test.tsx +0 -915
- package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +0 -193
- package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +0 -27
- package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +0 -39
- package/src/Carousel/__tests__/Carousel.a11y.states.ts +0 -139
- package/src/Carousel/__tests__/Carousel.a11y.test.tsx +0 -127
- package/src/Center/Center.spec.md +0 -205
- package/src/Center/Center.test.tsx +0 -406
- package/src/Chat/ChatComposer.spec.md +0 -230
- package/src/Chat/ChatComposer.test.tsx +0 -364
- package/src/Chat/ChatComposerDrawer.spec.md +0 -267
- package/src/Chat/ChatComposerDrawer.test.tsx +0 -130
- package/src/Chat/ChatComposerInput.spec.md +0 -224
- package/src/Chat/ChatComposerInput.test.tsx +0 -2136
- package/src/Chat/ChatComposerTokenElement.spec.md +0 -152
- package/src/Chat/ChatComposerTokenElement.test.tsx +0 -85
- package/src/Chat/ChatDictationButton.spec.md +0 -160
- package/src/Chat/ChatDictationButton.test.tsx +0 -108
- package/src/Chat/ChatLayout.spec.md +0 -271
- package/src/Chat/ChatLayout.test.tsx +0 -311
- package/src/Chat/ChatLayoutScrollButton.spec.md +0 -338
- package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -99
- package/src/Chat/ChatMessage.spec.md +0 -265
- package/src/Chat/ChatMessage.test.tsx +0 -344
- package/src/Chat/ChatMessageBubble.spec.md +0 -263
- package/src/Chat/ChatMessageBubble.test.tsx +0 -294
- package/src/Chat/ChatMessageList.spec.md +0 -222
- package/src/Chat/ChatMessageList.test.tsx +0 -198
- package/src/Chat/ChatMessageMetadata.spec.md +0 -220
- package/src/Chat/ChatMessageMetadata.test.tsx +0 -66
- package/src/Chat/ChatPastedTextToken.test.tsx +0 -71
- package/src/Chat/ChatSendButton.spec.md +0 -227
- package/src/Chat/ChatSendButton.test.tsx +0 -309
- package/src/Chat/ChatSystemMessage.spec.md +0 -224
- package/src/Chat/ChatSystemMessage.test.tsx +0 -189
- package/src/Chat/ChatTokenizedText.spec.md +0 -190
- package/src/Chat/ChatTokenizedText.test.tsx +0 -163
- package/src/Chat/ChatToolCalls.spec.md +0 -194
- package/src/Chat/ChatToolCalls.test.tsx +0 -413
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -577
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
- package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +0 -457
- package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +0 -91
- package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +0 -91
- package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +0 -465
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +0 -416
- package/src/Chat/__tests__/ChatSendButton.a11y.chromium.spec.ts +0 -1523
- package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +0 -1004
- package/src/Chat/__tests__/ChatTokenizedText.a11y.chromium.spec.ts +0 -574
- package/src/Chat/__tests__/useChatStreamScroll.a11y.chromium.spec.ts +0 -263
- package/src/Chat/__tests__/useChatStreamScroll.fixture.tsx +0 -61
- package/src/Chat/useChatDictation.test.tsx +0 -369
- package/src/Chat/useChatNewMessages.test.tsx +0 -209
- package/src/Chat/useChatPasteAsToken.test.ts +0 -191
- package/src/Chat/useChatStreamScroll.test.tsx +0 -381
- package/src/Chat/useTriggerMenu.test.tsx +0 -994
- package/src/CheckboxInput/CheckboxInput.spec.md +0 -218
- package/src/CheckboxInput/CheckboxInput.test.tsx +0 -926
- package/src/CheckboxInput/__tests__/Checkbox.a11y.chromium.spec.ts +0 -354
- package/src/CheckboxInput/__tests__/Checkbox.a11y.known-failures.ts +0 -54
- package/src/CheckboxInput/__tests__/Checkbox.a11y.renders.tsx +0 -310
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +0 -404
- package/src/CheckboxInput/__tests__/Checkbox.a11y.test.tsx +0 -128
- package/src/CheckboxList/CheckboxList.fr10.test.tsx +0 -51
- package/src/CheckboxList/CheckboxList.spec.md +0 -332
- package/src/CheckboxList/CheckboxList.test.tsx +0 -1159
- package/src/CheckboxList/CheckboxListItem.spec.md +0 -302
- package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +0 -1998
- package/src/Citation/Citation.spec.md +0 -179
- package/src/Citation/Citation.test.tsx +0 -361
- package/src/ClickableCard/ClickableCard.spec.md +0 -241
- package/src/ClickableCard/ClickableCard.test.tsx +0 -286
- package/src/Code/Code.spec.md +0 -170
- package/src/Code/Code.test.tsx +0 -75
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +0 -87
- package/src/CodeBlock/CodeBlock.spec.md +0 -243
- package/src/CodeBlock/CodeBlock.test.tsx +0 -752
- package/src/CodeBlock/CodeLanguage.test.tsx +0 -173
- package/src/CodeBlock/__tests__/CodeBlockCopyClearance.a11y.chromium.spec.ts +0 -290
- package/src/CodeBlock/__tests__/CodeBlockLanguages.a11y.chromium.spec.ts +0 -64
- package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +0 -126
- package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +0 -94
- package/src/CodeBlock/highlightRanges.test.ts +0 -253
- package/src/CodeBlock/languages.test.ts +0 -402
- package/src/CodeBlock/tokenizer.test.ts +0 -280
- package/src/CodeBlock/tokenizerBudget.test.ts +0 -112
- package/src/Collapsible/Collapsible.spec.md +0 -245
- package/src/Collapsible/Collapsible.test.tsx +0 -620
- package/src/Collapsible/CollapsibleGroup.spec.md +0 -201
- package/src/Collapsible/CollapsibleGroup.test.tsx +0 -656
- package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +0 -183
- package/src/Collapsible/__tests__/Collapsible.a11y.states.ts +0 -117
- package/src/Collapsible/__tests__/Collapsible.a11y.test.tsx +0 -140
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.chromium.spec.ts +0 -559
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +0 -45
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +0 -139
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +0 -135
- package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +0 -129
- package/src/Collapsible/useCollapsible.test.tsx +0 -236
- package/src/CommandPalette/CommandPalette.perf.test.ts +0 -100
- package/src/CommandPalette/CommandPalette.spec.md +0 -278
- package/src/CommandPalette/CommandPalette.test.tsx +0 -750
- package/src/CommandPalette/CommandPaletteEmpty.spec.md +0 -190
- package/src/CommandPalette/CommandPaletteEmpty.test.tsx +0 -101
- package/src/CommandPalette/CommandPaletteFooter.test.tsx +0 -122
- package/src/CommandPalette/CommandPaletteGroup.test.tsx +0 -129
- package/src/CommandPalette/CommandPaletteInput.spec.md +0 -215
- package/src/CommandPalette/CommandPaletteInput.test.tsx +0 -539
- package/src/CommandPalette/CommandPaletteItem.spec.md +0 -223
- package/src/CommandPalette/CommandPaletteItem.test.tsx +0 -417
- package/src/CommandPalette/CommandPaletteList.spec.md +0 -194
- package/src/CommandPalette/CommandPaletteList.test.tsx +0 -184
- package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +0 -600
- package/src/CommandPalette/__tests__/CommandPaletteEmptyAudit.a11y.chromium.spec.ts +0 -715
- package/src/CommandPalette/__tests__/CommandPaletteFooter.a11y.chromium.spec.ts +0 -582
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +0 -336
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +0 -523
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +0 -76
- package/src/ComplexSelector/ComplexSelector.fr10.test.tsx +0 -262
- package/src/ComplexSelector/ComplexSelector.spec.md +0 -266
- package/src/ComplexSelector/ComplexSelector.test.tsx +0 -1023
- package/src/ComplexSelector/__tests__/ComplexSelector.a11y.chromium.spec.ts +0 -965
- package/src/ContextMenu/ContextMenu.spec.md +0 -251
- package/src/ContextMenu/ContextMenu.test.tsx +0 -1582
- package/src/ContextMenu/ContextMenu.triggerProps.test.tsx +0 -54
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +0 -439
- package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +0 -154
- package/src/ContextMenu/__tests__/ContextMenuLongPress.a11y.chromium.spec.ts +0 -117
- package/src/DateInput/DateInput.fr10.test.tsx +0 -283
- package/src/DateInput/DateInput.spec.md +0 -319
- package/src/DateInput/DateInput.test.tsx +0 -1459
- package/src/DateInput/DateInputTouch.test.tsx +0 -2828
- package/src/DateInput/NativeDateField.test.tsx +0 -741
- package/src/DateInput/Presentation.test.tsx +0 -121
- package/src/DateInput/__tests__/DateInputAudit.a11y.chromium.spec.ts +0 -577
- package/src/DateInput/__tests__/WheelPinchZoom.a11y.chromium.spec.ts +0 -171
- package/src/DateRangeInput/DateRangeInput.fr10.test.tsx +0 -165
- package/src/DateRangeInput/DateRangeInput.spec.md +0 -203
- package/src/DateRangeInput/DateRangeInput.test.tsx +0 -1108
- package/src/DateTimeInput/DateTimeInput.fr10.test.tsx +0 -319
- package/src/DateTimeInput/DateTimeInput.spec.md +0 -246
- package/src/DateTimeInput/DateTimeInput.test.tsx +0 -2575
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +0 -1071
- package/src/DateTimeInput/NativePickerSegments.test.tsx +0 -657
- package/src/DateTimeInput/Presentation.test.tsx +0 -89
- package/src/Dialog/Dialog.source-build.test.mjs +0 -43
- package/src/Dialog/Dialog.spec.md +0 -189
- package/src/Dialog/Dialog.test.tsx +0 -1385
- package/src/Dialog/DialogHeader.test.tsx +0 -477
- package/src/Dialog/__tests__/Dialog.a11y.browser.spec.ts +0 -111
- package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +0 -218
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -28
- package/src/Dialog/__tests__/Dialog.a11y.renders.tsx +0 -107
- package/src/Dialog/__tests__/Dialog.a11y.states.ts +0 -104
- package/src/Dialog/__tests__/Dialog.a11y.test.tsx +0 -102
- package/src/Dialog/__tests__/Dialog.reduced-motion.a11y.browser.spec.ts +0 -252
- package/src/Dialog/modules/DialogHeader.spec.md +0 -211
- package/src/Dialog/useImperativeDialog.test.tsx +0 -78
- package/src/Divider/Divider.spec.md +0 -252
- package/src/Divider/Divider.test.tsx +0 -155
- package/src/Drawer/Drawer.spec.md +0 -357
- package/src/Drawer/Drawer.test.tsx +0 -1300
- package/src/Drawer/DrawerDirection.test.tsx +0 -127
- package/src/Drawer/DrawerHeader.test.tsx +0 -102
- package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +0 -402
- package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +0 -298
- package/src/DropdownMenu/DropdownMenu.spec.md +0 -582
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +0 -93
- package/src/DropdownMenu/DropdownMenu.test.tsx +0 -4340
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +0 -22
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +0 -192
- package/src/DropdownMenu/DropdownMenuItem.spec.md +0 -209
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +0 -126
- package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +0 -190
- package/src/DropdownMenu/DropdownMenuRadioItem.spec.md +0 -226
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -635
- package/src/DropdownMenu/DropdownMenuSheetFirstOpen.test.tsx +0 -72
- package/src/DropdownMenu/DropdownMenuSubMenu.spec.md +0 -225
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +0 -2408
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +0 -281
- package/src/DropdownMenu/__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts +0 -175
- package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +0 -74
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +0 -335
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +0 -367
- package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +0 -328
- package/src/DropdownMenu/__tests__/DropdownMenuRadioItemAudit.a11y.chromium.spec.ts +0 -540
- package/src/DropdownMenu/__tests__/DropdownMenuSubMenuAudit.a11y.chromium.spec.ts +0 -671
- package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +0 -146
- package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +0 -26
- package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +0 -65
- package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +0 -138
- package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +0 -136
- package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +0 -137
- package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +0 -12
- package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +0 -67
- package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +0 -141
- package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +0 -138
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +0 -231
- package/src/DropdownMenu/__tests__/MenuHoverFocus.a11y.chromium.spec.ts +0 -238
- package/src/DropdownMenu/__tests__/MenuPinchZoom.a11y.chromium.spec.ts +0 -173
- package/src/DropdownMenu/__tests__/MenuPress.a11y.chromium.spec.ts +0 -503
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +0 -268
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +0 -43
- package/src/DropdownMenu/modules/useMenuHover.spec.md +0 -283
- package/src/DropdownMenu/modules/useMenuPress.spec.md +0 -348
- package/src/EmptyState/EmptyState.announce.test.tsx +0 -227
- package/src/EmptyState/EmptyState.spec.md +0 -197
- package/src/EmptyState/EmptyState.test.tsx +0 -284
- package/src/Field/Field.spec.md +0 -226
- package/src/Field/Field.test.tsx +0 -800
- package/src/Field/FieldLabel.spec.md +0 -183
- package/src/Field/FieldLabel.test.tsx +0 -244
- package/src/Field/InputClearButton.test.tsx +0 -395
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +0 -297
- package/src/Field/fieldInputFamily.test.tsx +0 -160
- package/src/FieldStatus/FieldStatus.spec.md +0 -224
- package/src/FieldStatus/FieldStatus.test.tsx +0 -482
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +0 -206
- package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +0 -13
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -287
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +0 -302
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +0 -154
- package/src/FileInput/FileInput.fr10.test.tsx +0 -21
- package/src/FileInput/FileInput.spec.md +0 -199
- package/src/FileInput/FileInput.test.tsx +0 -1270
- package/src/FormLayout/FormLayout.spec.md +0 -210
- package/src/FormLayout/FormLayout.test.tsx +0 -444
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -46
- package/src/Grid/Grid.spec.md +0 -193
- package/src/Grid/Grid.test.tsx +0 -509
- package/src/Grid/GridSpan.spec.md +0 -158
- package/src/HStack/HStack.test.tsx +0 -129
- package/src/Heading/Heading.spec.md +0 -198
- package/src/Heading/Heading.test.tsx +0 -328
- package/src/HoverCard/HoverCard.spec.md +0 -244
- package/src/HoverCard/HoverCard.test.tsx +0 -1339
- package/src/Icon/Icon.spec.md +0 -244
- package/src/Icon/Icon.test.tsx +0 -990
- package/src/Icon/globalIconRegistry.test.tsx +0 -613
- package/src/IconButton/IconButton.spec.md +0 -197
- package/src/IconButton/IconButton.test.tsx +0 -156
- package/src/Indicator/CheckIndicator.spec.md +0 -159
- package/src/Indicator/CheckboxIndicator.spec.md +0 -161
- package/src/Indicator/Indicator.test.tsx +0 -389
- package/src/Indicator/indicatorRegistry.test.tsx +0 -106
- package/src/InputGroup/InputGroup.spec.md +0 -199
- package/src/InputGroup/InputGroup.test.tsx +0 -502
- package/src/InputGroup/InputGroupText.spec.md +0 -139
- package/src/InteractiveRoleContext/InteractiveRoleContext.test.tsx +0 -185
- package/src/Item/Item.source-build.test.mjs +0 -88
- package/src/Item/Item.spec.md +0 -226
- package/src/Item/Item.swipeLayer.test.tsx +0 -74
- package/src/Item/Item.test.tsx +0 -1455
- package/src/Item/ItemMarkerColumns.a11y.chromium.spec.ts +0 -211
- package/src/Item/__tests__/ItemSwipeActions.a11y.chromium.spec.ts +0 -337
- package/src/Item/__tests__/ItemSwipePinchZoom.a11y.chromium.spec.ts +0 -186
- package/src/Kbd/Kbd.spec.md +0 -142
- package/src/Kbd/Kbd.test.tsx +0 -299
- package/src/Layer/LayerProvider.spec.md +0 -149
- package/src/Layer/LayerProvider.test.tsx +0 -143
- package/src/Layer/__tests__/LayerReducedMotion.a11y.chromium.spec.ts +0 -176
- package/src/Layer/__tests__/TouchTrigger.a11y.chromium.spec.ts +0 -200
- package/src/Layer/anchorName.test.ts +0 -146
- package/src/Layer/layerDismissalFamilies.test.tsx +0 -487
- package/src/Layer/layerDismissalInvariants.test.tsx +0 -501
- package/src/Layer/layerHost.test.ts +0 -117
- package/src/Layer/layerScopedContext.test.tsx +0 -499
- package/src/Layer/layerViewportInset.source-build.test.mjs +0 -90
- package/src/Layer/layerViewportInset.test.ts +0 -120
- package/src/Layer/useLayer.test.tsx +0 -2423
- package/src/Layer/useLayerDismissal.test.tsx +0 -580
- package/src/Layer/useTouchTrigger.test.ts +0 -70
- package/src/Layout/Layout.spec.md +0 -247
- package/src/Layout/Layout.test.tsx +0 -375
- package/src/Layout/LayoutSlots.test.tsx +0 -376
- package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +0 -122
- package/src/Layout/__tests__/Layout.a11y.renders.tsx +0 -110
- package/src/Layout/__tests__/Layout.a11y.states.ts +0 -93
- package/src/Layout/__tests__/Layout.a11y.test.tsx +0 -142
- package/src/Layout/__tests__/childrenAsContent.test.tsx +0 -59
- package/src/Layout/__tests__/contentWidth.test.tsx +0 -303
- package/src/Layout/__tests__/edgeCompensation.test.tsx +0 -191
- package/src/Layout/__tests__/nestedPadding.test.tsx +0 -264
- package/src/Layout/overlayPaddingReset.test.tsx +0 -224
- package/src/Lightbox/Lightbox.spec.md +0 -199
- package/src/Lightbox/Lightbox.test.tsx +0 -819
- package/src/Lightbox/LightboxKeyboard.a11y.browser.spec.ts +0 -78
- package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +0 -70
- package/src/Lightbox/useLightbox.test.tsx +0 -263
- package/src/Link/Link.spec.md +0 -229
- package/src/Link/Link.test.tsx +0 -457
- package/src/Link/LinkProvider.spec.md +0 -167
- package/src/Link/__tests__/Link.a11y.chromium.spec.ts +0 -143
- package/src/Link/__tests__/Link.a11y.renders.tsx +0 -97
- package/src/Link/__tests__/Link.a11y.states.ts +0 -104
- package/src/Link/__tests__/Link.a11y.test.tsx +0 -127
- package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -262
- package/src/Link/useLinkComponent.test.tsx +0 -572
- package/src/Link/useLinkify.test.tsx +0 -146
- package/src/List/List.spec.md +0 -336
- package/src/List/List.test.tsx +0 -1052
- package/src/List/ListItem.source-build.test.mjs +0 -41
- package/src/List/ListMarkers.a11y.chromium.spec.ts +0 -904
- package/src/Markdown/Markdown.codeLanguages.test.tsx +0 -137
- package/src/Markdown/Markdown.fr23.bench.ts +0 -176
- package/src/Markdown/Markdown.fr23.perf.test.ts +0 -484
- package/src/Markdown/Markdown.helpers.perf.test.ts +0 -495
- package/src/Markdown/Markdown.public.test.ts +0 -309
- package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
- package/src/Markdown/Markdown.rerender.test.tsx +0 -143
- package/src/Markdown/Markdown.spec.md +0 -595
- package/src/Markdown/Markdown.test.tsx +0 -1383
- package/src/Markdown/MarkdownTable.a11y.chromium.spec.ts +0 -727
- package/src/Markdown/__tests__/MarkdownNesting.a11y.chromium.spec.ts +0 -102
- package/src/Markdown/angleDestinations.test.tsx +0 -104
- package/src/Markdown/ast.test.ts +0 -374
- package/src/Markdown/atxHeadingIndentation.test.tsx +0 -53
- package/src/Markdown/blockNesting.test.tsx +0 -124
- package/src/Markdown/blockquoteMarkers.test.tsx +0 -69
- package/src/Markdown/characterReferences.test.ts +0 -47
- package/src/Markdown/codeSpans.test.tsx +0 -65
- package/src/Markdown/emphasis.test.tsx +0 -214
- package/src/Markdown/fenceInfo.test.tsx +0 -47
- package/src/Markdown/imageAlt.test.tsx +0 -84
- package/src/Markdown/incremental.test.ts +0 -1198
- package/src/Markdown/incrementalIndentation.test.tsx +0 -36
- package/src/Markdown/indentedFences.test.tsx +0 -374
- package/src/Markdown/indentedTables.test.tsx +0 -53
- package/src/Markdown/linkBrackets.test.tsx +0 -116
- package/src/Markdown/linkDestinationLinear.test.tsx +0 -54
- package/src/Markdown/linkDestinations.test.tsx +0 -127
- package/src/Markdown/linkTextCode.test.tsx +0 -65
- package/src/Markdown/listBullets.test.tsx +0 -55
- package/src/Markdown/listContinuation.test.tsx +0 -94
- package/src/Markdown/listThematicBreak.test.tsx +0 -49
- package/src/Markdown/longLinkLabels.test.tsx +0 -49
- package/src/Markdown/modules/headingLinks.spec.md +0 -262
- package/src/Markdown/modules/remark.spec.md +0 -269
- package/src/Markdown/modules/softBreaks.spec.md +0 -137
- package/src/Markdown/modules/sourceLines.spec.md +0 -162
- package/src/Markdown/openFenceFinalNewline.test.tsx +0 -85
- package/src/Markdown/parser.perf.test.ts +0 -398
- package/src/Markdown/parser.public.test.ts +0 -144
- package/src/Markdown/parser.test.ts +0 -1925
- package/src/Markdown/plugin-renderer/MarkdownPluginNodeRenderer.test.tsx +0 -300
- package/src/Markdown/plugins/frontmatter.test.tsx +0 -150
- package/src/Markdown/plugins/headingLinks.a11y.chromium.spec.ts +0 -856
- package/src/Markdown/plugins/headingLinks.ssr.test.tsx +0 -143
- package/src/Markdown/plugins/headingLinks.test.tsx +0 -637
- package/src/Markdown/plugins/protocol.test.tsx +0 -1083
- package/src/Markdown/plugins/semanticFence.test.tsx +0 -655
- package/src/Markdown/plugins/softBreaks.test.tsx +0 -172
- package/src/Markdown/plugins/sourceDecoration.test.tsx +0 -799
- package/src/Markdown/plugins/sourceLines.test.tsx +0 -715
- package/src/Markdown/plugins/textTransform.test.ts +0 -467
- package/src/Markdown/remark.test.tsx +0 -1877
- package/src/Markdown/streaming.test.ts +0 -148
- package/src/Markdown/streamingListShapes.test.tsx +0 -93
- package/src/Markdown/trailingSpaces.test.tsx +0 -45
- package/src/MetadataList/MetadataList.test.tsx +0 -383
- package/src/MobileNav/MobileNav.spec.md +0 -306
- package/src/MobileNav/MobileNav.test.tsx +0 -281
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +0 -584
- package/src/MobileNav/MobileNavCloseTiming.test.ts +0 -58
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +0 -172
- package/src/MobileNav/MobileNavEntryAnimation.test.tsx +0 -212
- package/src/MobileNav/MobileNavFocus.test.tsx +0 -553
- package/src/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.ts +0 -147
- package/src/MobileNav/MobileNavReopen.test.tsx +0 -118
- package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +0 -109
- package/src/MobileNav/MobileNavToggle.test.tsx +0 -179
- package/src/MobileNav/__tests__/MobileNavWidth.a11y.chromium.spec.ts +0 -103
- package/src/MoreMenu/MoreMenu.spec.md +0 -268
- package/src/MoreMenu/MoreMenu.test.tsx +0 -356
- package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +0 -149
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +0 -72
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +0 -121
- package/src/MultiSelector/MultiSelector.fr10.test.tsx +0 -221
- package/src/MultiSelector/MultiSelector.spec.md +0 -354
- package/src/MultiSelector/MultiSelector.test.tsx +0 -4702
- package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +0 -310
- package/src/MultiSelector/__tests__/MultiSelector.a11y.chromium.spec.ts +0 -46
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +0 -128
- package/src/NavIcon/NavIcon.spec.md +0 -158
- package/src/NavIcon/NavIcon.test.tsx +0 -41
- package/src/NavMenu/NavHeadingMenu.spec.md +0 -268
- package/src/NavMenu/NavHeadingMenu.test.tsx +0 -714
- package/src/NavMenu/__tests__/NavMenusSlideSelection.a11y.chromium.spec.ts +0 -137
- package/src/NumberInput/NumberInput.fr10.test.tsx +0 -21
- package/src/NumberInput/NumberInput.spec.md +0 -248
- package/src/NumberInput/NumberInput.test.tsx +0 -2636
- package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +0 -124
- package/src/NumberInput/__tests__/NumberInput.a11y.known-failures.ts +0 -26
- package/src/NumberInput/__tests__/NumberInput.a11y.renders.tsx +0 -77
- package/src/NumberInput/__tests__/NumberInput.a11y.states.ts +0 -116
- package/src/NumberInput/__tests__/NumberInput.a11y.test.tsx +0 -154
- package/src/NumberInput/numberInputCommit.test.ts +0 -72
- package/src/NumberInput/numberParser.docblock.test.ts +0 -234
- package/src/NumberInput/numberParser.test.ts +0 -222
- package/src/Outline/Outline.spec.md +0 -162
- package/src/Outline/Outline.test.tsx +0 -1069
- package/src/Outline/OutlineFocus.a11y.chromium.spec.ts +0 -551
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +0 -151
- package/src/Outline/parseOutlineFromMarkdown.test.ts +0 -104
- package/src/OverflowList/OverflowList.spec.md +0 -157
- package/src/OverflowList/OverflowList.test.tsx +0 -1073
- package/src/Overlay/Overlay.spec.md +0 -304
- package/src/Overlay/Overlay.test.tsx +0 -424
- package/src/Pagination/Pagination.spec.md +0 -226
- package/src/Pagination/Pagination.test.tsx +0 -1754
- package/src/Popover/Popover.spec.md +0 -525
- package/src/Popover/Popover.test.tsx +0 -1424
- package/src/Popover/PopoverPresentation.test.tsx +0 -952
- package/src/Popover/__tests__/PopoverSecondDocument.a11y.chromium.spec.ts +0 -205
- package/src/PowerSearch/PowerSearch.spec.md +0 -661
- package/src/PowerSearch/PowerSearch.test.tsx +0 -1003
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +0 -1190
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +0 -377
- package/src/PowerSearch/PowerSearchToken.test.tsx +0 -230
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -754
- package/src/PowerSearch/formatFilterValue.test.ts +0 -339
- package/src/PowerSearch/resolveDateTimeRangePart.test.ts +0 -40
- package/src/PowerSearch/usePowerSearchConfig.test.ts +0 -832
- package/src/PowerSearch/usePowerSearchSource.test.ts +0 -621
- package/src/ProgressBar/ProgressBar.spec.md +0 -261
- package/src/ProgressBar/ProgressBar.test.tsx +0 -852
- package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +0 -115
- package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +0 -12
- package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +0 -38
- package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +0 -123
- package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +0 -107
- package/src/RadioList/RadioList.fr10.test.tsx +0 -67
- package/src/RadioList/RadioList.spec.md +0 -227
- package/src/RadioList/RadioList.test.tsx +0 -967
- package/src/RadioList/RadioListItem.press.source-build.test.mjs +0 -80
- package/src/RadioList/RadioListItem.spec.md +0 -223
- package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +0 -274
- package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +0 -12
- package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +0 -232
- package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +0 -503
- package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +0 -217
- package/src/Resizable/Resizable.spec.md +0 -278
- package/src/Resizable/ResizeHandle.test.tsx +0 -1067
- package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +0 -120
- package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +0 -39
- package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +0 -80
- package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +0 -197
- package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +0 -123
- package/src/Resizable/shippedDirections.test.ts +0 -462
- package/src/Resizable/useResizable.test.ts +0 -1682
- package/src/Resizable/utils.test.ts +0 -74
- package/src/ScrollableArea/ScrollableArea.spec.md +0 -190
- package/src/ScrollableArea/ScrollableArea.test.tsx +0 -304
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +0 -149
- package/src/Section/Section.spec.md +0 -197
- package/src/Section/Section.test.tsx +0 -376
- package/src/SegmentedControl/SegmentedControl.spec.md +0 -261
- package/src/SegmentedControl/SegmentedControl.test.tsx +0 -897
- package/src/SegmentedControl/SegmentedControlItem.spec.md +0 -206
- package/src/SelectableCard/SelectableCard.spec.md +0 -230
- package/src/SelectableCard/SelectableCard.test.tsx +0 -216
- package/src/Selector/Selector.focusableDisabled.test.tsx +0 -127
- package/src/Selector/Selector.fr10.test.tsx +0 -149
- package/src/Selector/Selector.sheetActiveDescendant.test.tsx +0 -95
- package/src/Selector/Selector.source-build.test.mjs +0 -79
- package/src/Selector/Selector.spec.md +0 -379
- package/src/Selector/Selector.test.tsx +0 -5028
- package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +0 -27
- package/src/Selector/__tests__/Combobox.a11y.states.ts +0 -145
- package/src/Selector/__tests__/ComboboxField.a11y.chromium.spec.ts +0 -458
- package/src/Selector/__tests__/EmptyStateAnnouncement.a11y.chromium.spec.ts +0 -91
- package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +0 -131
- package/src/Selector/__tests__/Listbox.a11y.dom.ts +0 -38
- package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +0 -63
- package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +0 -12
- package/src/Selector/__tests__/Listbox.a11y.states.ts +0 -232
- package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +0 -46
- package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +0 -109
- package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +0 -126
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +0 -121
- package/src/Selector/useCombobox.public.test.tsx +0 -89
- package/src/Selector/useCombobox.test.tsx +0 -444
- package/src/SideNav/SideNav.spec.md +0 -247
- package/src/SideNav/SideNav.test.tsx +0 -4128
- package/src/SideNav/SideNavCollapseButton.spec.md +0 -161
- package/src/SideNav/SideNavHeading.spec.md +0 -226
- package/src/SideNav/SideNavItem.spec.md +0 -246
- package/src/SideNav/SideNavRailMotion.test.tsx +0 -620
- package/src/SideNav/SideNavSection.spec.md +0 -153
- package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +0 -417
- package/src/SizeContext/SizeContext.test.tsx +0 -226
- package/src/Skeleton/Skeleton.spec.md +0 -184
- package/src/Skeleton/Skeleton.test.tsx +0 -148
- package/src/Slider/Slider.fr10.test.tsx +0 -93
- package/src/Slider/Slider.spec.md +0 -285
- package/src/Slider/Slider.test.tsx +0 -1630
- package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +0 -113
- package/src/Slider/__tests__/Slider.a11y.known-failures.ts +0 -12
- package/src/Slider/__tests__/Slider.a11y.renders.tsx +0 -42
- package/src/Slider/__tests__/Slider.a11y.states.ts +0 -106
- package/src/Slider/__tests__/Slider.a11y.test.tsx +0 -105
- package/src/Spinner/Spinner.spec.md +0 -231
- package/src/Spinner/Spinner.test.tsx +0 -484
- package/src/Stack/Stack.spec.md +0 -203
- package/src/Stack/Stack.test.tsx +0 -473
- package/src/Stack/StackItem.test.tsx +0 -105
- package/src/StatusDot/StatusDot.spec.md +0 -224
- package/src/StatusDot/StatusDot.test.tsx +0 -213
- package/src/Stepper/Step.spec.md +0 -206
- package/src/Stepper/Stepper.public.test.ts +0 -91
- package/src/Stepper/Stepper.spec.md +0 -327
- package/src/Stepper/Stepper.test.tsx +0 -2068
- package/src/Switch/Switch.focusRing.source-build.test.mjs +0 -81
- package/src/Switch/Switch.spec.md +0 -277
- package/src/Switch/Switch.test.tsx +0 -1071
- package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +0 -211
- package/src/Switch/__tests__/Switch.a11y.known-failures.ts +0 -28
- package/src/Switch/__tests__/Switch.a11y.states.ts +0 -275
- package/src/Switch/__tests__/Switch.a11y.test.tsx +0 -144
- package/src/TabList/Tab.spec.md +0 -201
- package/src/TabList/TabList.spec.md +0 -217
- package/src/TabList/TabList.test.tsx +0 -1701
- package/src/TabList/TabMenu.spec.md +0 -198
- package/src/TabList/__tests__/TabListOverflow.interaction.browser.spec.ts +0 -99
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +0 -199
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +0 -12
- package/src/TabList/__tests__/Tabs.a11y.renders.tsx +0 -92
- package/src/TabList/__tests__/Tabs.a11y.states.ts +0 -247
- package/src/TabList/__tests__/Tabs.a11y.test.tsx +0 -153
- package/src/Table/Table.perf.test.tsx +0 -569
- package/src/Table/Table.spec.md +0 -395
- package/src/Table/Table.test.tsx +0 -1851
- package/src/Table/TableHeaderCell.test.tsx +0 -291
- package/src/Table/TableSelectionToolbar.test.tsx +0 -109
- package/src/Table/__tests__/Table.a11y.chromium.spec.ts +0 -129
- package/src/Table/__tests__/Table.a11y.contracts.ts +0 -35
- package/src/Table/__tests__/Table.a11y.renders.tsx +0 -112
- package/src/Table/__tests__/Table.a11y.states.ts +0 -140
- package/src/Table/__tests__/Table.a11y.test.tsx +0 -198
- package/src/Table/__tests__/TableMinWidth.a11y.chromium.spec.ts +0 -75
- package/src/Table/__tests__/TableRowGlide.a11y.browser.spec.ts +0 -382
- package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +0 -138
- package/src/Table/__tests__/TableStickyDividers.a11y.browser.spec.ts +0 -560
- package/src/Table/plugins/activation/useTableActivation.spec.md +0 -254
- package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +0 -373
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +0 -919
- package/src/Table/plugins/columnSettings/useTableColumnSettings.test.tsx +0 -175
- package/src/Table/plugins/columnSettings/useTableColumnSettingsState.test.tsx +0 -296
- package/src/Table/plugins/filtering/useTableFilterState.test.tsx +0 -143
- package/src/Table/plugins/filtering/useTableFiltering.test.tsx +0 -357
- package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +0 -112
- package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +0 -409
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +0 -244
- package/src/Table/plugins/pagination/useTablePagination-perf.test.tsx +0 -232
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +0 -666
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.glide.test.tsx +0 -280
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -681
- package/src/Table/plugins/rowIndex/useTableRowIndex.test.tsx +0 -132
- package/src/Table/plugins/rowStatus/useTableRowStatus.public.test.ts +0 -92
- package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +0 -355
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +0 -519
- package/src/Table/plugins/selection/useTableSelection-perf.test.tsx +0 -258
- package/src/Table/plugins/selection/useTableSelection.test.tsx +0 -424
- package/src/Table/plugins/selection/useTableSelectionState.test.tsx +0 -451
- package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.test.tsx +0 -174
- package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.ts +0 -86
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +0 -857
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +0 -592
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +0 -334
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +0 -163
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +0 -992
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +0 -740
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +0 -266
- package/src/Table/tableContextMenu.test.tsx +0 -208
- package/src/Text/Text.spec.md +0 -193
- package/src/Text/Text.test.tsx +0 -405
- package/src/Text/useTruncation.test.tsx +0 -69
- package/src/TextArea/TextArea.fr10.test.tsx +0 -21
- package/src/TextArea/TextArea.spec.md +0 -303
- package/src/TextArea/TextArea.test.tsx +0 -1257
- package/src/TextInput/TextInput.fr10.test.tsx +0 -21
- package/src/TextInput/TextInput.spec.md +0 -260
- package/src/TextInput/TextInput.test.tsx +0 -1259
- package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +0 -214
- package/src/TextInput/__tests__/TextInput.a11y.known-failures.ts +0 -40
- package/src/TextInput/__tests__/TextInput.a11y.renders.tsx +0 -202
- package/src/TextInput/__tests__/TextInput.a11y.states.ts +0 -384
- package/src/TextInput/__tests__/TextInput.a11y.test.tsx +0 -130
- package/src/Thumbnail/Thumbnail.spec.md +0 -244
- package/src/Thumbnail/Thumbnail.test.tsx +0 -384
- package/src/TimeInput/NativeTimeInput.test.tsx +0 -326
- package/src/TimeInput/Presentation.test.tsx +0 -171
- package/src/TimeInput/TimeInput.fr10.test.tsx +0 -312
- package/src/TimeInput/TimeInput.spec.md +0 -273
- package/src/TimeInput/TimeInput.test.tsx +0 -831
- package/src/Timer/Timer.spec.md +0 -215
- package/src/Timer/Timer.test.tsx +0 -302
- package/src/Timestamp/Timestamp.spec.md +0 -252
- package/src/Timestamp/Timestamp.test.tsx +0 -1636
- package/src/Timestamp/tooltipEntries.test.ts +0 -566
- package/src/Toast/Toast.spec.md +0 -300
- package/src/Toast/ToastViewport.modalHost.test.tsx +0 -891
- package/src/Toast/ToastViewport.test.tsx +0 -2333
- package/src/Toast/useToast.test.tsx +0 -258
- package/src/ToggleButton/ToggleButton.spec.md +0 -256
- package/src/ToggleButton/ToggleButton.test.tsx +0 -973
- package/src/ToggleButton/ToggleButtonGroup.spec.md +0 -195
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +0 -177
- package/src/ToggleButton/__tests__/ToggleButton.a11y.renders.tsx +0 -133
- package/src/ToggleButton/__tests__/ToggleButton.a11y.states.ts +0 -167
- package/src/ToggleButton/__tests__/ToggleButton.a11y.test.tsx +0 -103
- package/src/Token/Token.spec.md +0 -271
- package/src/Token/Token.test.tsx +0 -640
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +0 -87
- package/src/Tokenizer/Tokenizer.fr10.test.tsx +0 -28
- package/src/Tokenizer/Tokenizer.props-type.test.tsx +0 -126
- package/src/Tokenizer/Tokenizer.spec.md +0 -601
- package/src/Tokenizer/Tokenizer.ssr.test.tsx +0 -196
- package/src/Tokenizer/Tokenizer.test.tsx +0 -2121
- package/src/Tokenizer/TokenizerBottomSheet.test.tsx +0 -2719
- package/src/Tokenizer/__tests__/TokenizerEndLane.a11y.chromium.spec.ts +0 -505
- package/src/Toolbar/Toolbar.spec.md +0 -228
- package/src/Toolbar/Toolbar.test.tsx +0 -570
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +0 -164
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +0 -63
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +0 -142
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +0 -126
- package/src/Tooltip/Tooltip.spec.md +0 -267
- package/src/Tooltip/Tooltip.test.tsx +0 -819
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +0 -130
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +0 -86
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +0 -132
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +0 -126
- package/src/Tooltip/__tests__/TooltipFocusReturn.a11y.chromium.spec.ts +0 -130
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +0 -75
- package/src/Tooltip/__tests__/TooltipTallContent.a11y.browser.spec.ts +0 -673
- package/src/Tooltip/__tests__/TooltipTallContentTracking.a11y.browser.spec.ts +0 -1162
- package/src/Tooltip/useTooltip.test.tsx +0 -483
- package/src/TopNav/TopNav.spec.md +0 -190
- package/src/TopNav/TopNav.test.tsx +0 -836
- package/src/TopNav/TopNavHeading.spec.md +0 -211
- package/src/TopNav/TopNavItem.spec.md +0 -189
- package/src/TopNav/TopNavMegaMenu.reduced-motion.source-build.test.mjs +0 -83
- package/src/TopNav/TopNavMegaMenu.spec.md +0 -190
- package/src/TopNav/TopNavMegaMenu.test.tsx +0 -1132
- package/src/TopNav/TopNavMegaMenuFeaturedCard.spec.md +0 -150
- package/src/TopNav/TopNavMegaMenuItem.spec.md +0 -155
- package/src/TopNav/TopNavMenu.spec.md +0 -202
- package/src/TopNav/TopNavMenu.test.tsx +0 -485
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +0 -75
- package/src/TreeList/TreeList.spec.md +0 -328
- package/src/TreeList/TreeList.test.tsx +0 -1711
- package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +0 -138
- package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +0 -44
- package/src/TreeList/__tests__/TreeList.a11y.states.ts +0 -164
- package/src/TreeList/__tests__/TreeList.a11y.test.tsx +0 -138
- package/src/TreeList/__tests__/TreeListGuideAlignment.interaction.browser.spec.ts +0 -127
- package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +0 -83
- package/src/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.ts +0 -138
- package/src/Typeahead/BaseTypeahead.spec.md +0 -270
- package/src/Typeahead/BaseTypeahead.test.tsx +0 -553
- package/src/Typeahead/Typeahead.fr10.test.tsx +0 -28
- package/src/Typeahead/Typeahead.spec.md +0 -257
- package/src/Typeahead/Typeahead.test.tsx +0 -2824
- package/src/Typeahead/TypeaheadItem.test.tsx +0 -134
- package/src/VStack/VStack.test.tsx +0 -128
- package/src/VisuallyHidden/VisuallyHidden.spec.md +0 -170
- package/src/VisuallyHidden/VisuallyHidden.test.tsx +0 -115
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +0 -853
- package/src/__tests__/CardPress.a11y.chromium.spec.ts +0 -353
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +0 -87
- package/src/__tests__/PressModality.a11y.chromium.spec.ts +0 -392
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +0 -814
- package/src/__tests__/TestIcon.tsx +0 -26
- package/src/__tests__/babelPluginAddExtensions.test.ts +0 -190
- package/src/__tests__/fastRoleQueries.ts +0 -66
- package/src/__tests__/forcedColors.ts +0 -60
- package/src/__tests__/pressState.ts +0 -212
- package/src/__tests__/stubMatchMedia.test.ts +0 -79
- package/src/__tests__/stubMatchMedia.ts +0 -95
- package/src/__tests__/stylexDeclarations.ts +0 -51
- package/src/__tests__/unthemedLength.ts +0 -34
- package/src/docPropLiterals.test.ts +0 -964
- package/src/docPropReferences.test.ts +0 -178
- package/src/docs-script.test.mjs +0 -55
- package/src/gestures/__tests__/LongPressGestureRecognizer.test.ts +0 -141
- package/src/gestures/__tests__/PanGestureRecognizer.test.ts +0 -327
- package/src/gestures/__tests__/TapGestureRecognizer.test.ts +0 -154
- package/src/gestures/__tests__/VelocityTracker.streams.test.ts +0 -243
- package/src/gestures/__tests__/VelocityTracker.test.ts +0 -261
- package/src/gestures/__tests__/arena.test.ts +0 -824
- package/src/gestures/__tests__/arenaContinuous.test.ts +0 -207
- package/src/gestures/__tests__/arenaTapLongPress.test.ts +0 -211
- package/src/gestures/__tests__/fixtures/pointer-streams/README.md +0 -82
- package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-fling.json +0 -130
- package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-slow.json +0 -860
- package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-stop-release.json +0 -357
- package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-fling.json +0 -66
- package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-slow.json +0 -268
- package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-stop-release.json +0 -128
- package/src/gestures/__tests__/fixtures/pointer-streams/capture.html +0 -123
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-fling.json +0 -64
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-slow.json +0 -268
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-stop-release.json +0 -124
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-fling.json +0 -64
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-slow.json +0 -268
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-stop-release.json +0 -124
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-fling.json +0 -84
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-slow.json +0 -381
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-stop-release.json +0 -171
- package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-fling.json +0 -45
- package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-slow.json +0 -101
- package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-stop-release.json +0 -130
- package/src/gestures/__tests__/harness.ts +0 -204
- package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +0 -144
- package/src/hooks/__tests__/menuLinkClicks.ts +0 -71
- package/src/hooks/actionGroupTracker.exports.test.ts +0 -28
- package/src/hooks/actionGroupTracker.test.ts +0 -510
- package/src/hooks/computeOverflow.test.ts +0 -422
- package/src/hooks/deprecatedApis.public.test.ts +0 -33
- package/src/hooks/menuPressGesture.test.ts +0 -253
- package/src/hooks/scrollKeyboardDelegation.test.ts +0 -155
- package/src/hooks/scrollbarGutter.test.ts +0 -167
- package/src/hooks/useAnnounce.modal.test.tsx +0 -1291
- package/src/hooks/useAnnounce.test.tsx +0 -293
- package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +0 -226
- package/src/hooks/useAutoMediaMode.test.ts +0 -109
- package/src/hooks/useBusyAnnouncement.adopters.test.tsx +0 -69
- package/src/hooks/useBusyAnnouncement.test.tsx +0 -112
- package/src/hooks/useClickableContainer.test.tsx +0 -236
- package/src/hooks/useClipboard.test.tsx +0 -151
- package/src/hooks/useContainerReveal.test.tsx +0 -163
- package/src/hooks/useDevWarning.test.tsx +0 -45
- package/src/hooks/useEntryAnimation.ssr.test.tsx +0 -143
- package/src/hooks/useFocusTrap.test.tsx +0 -635
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +0 -166
- package/src/hooks/useGridFocus.test.tsx +0 -165
- package/src/hooks/useHotkeys.test.ts +0 -265
- package/src/hooks/useImageMode.test.ts +0 -133
- package/src/hooks/useInputContainer.test.tsx +0 -180
- package/src/hooks/useInputStatusIcon.test.tsx +0 -240
- package/src/hooks/useIsHydrating.test.tsx +0 -70
- package/src/hooks/useKeyboardHint.test.tsx +0 -92
- package/src/hooks/useListFocus.test.tsx +0 -664
- package/src/hooks/useLongPress.test.tsx +0 -152
- package/src/hooks/useMediaQuery.test.ts +0 -165
- package/src/hooks/useMenuHover.test.tsx +0 -536
- package/src/hooks/useMenuPress.test.tsx +0 -565
- package/src/hooks/useMergedRefs.test.tsx +0 -62
- package/src/hooks/useOverflow.test.ts +0 -504
- package/src/hooks/useRenamedProp.test.tsx +0 -95
- package/src/hooks/useScrollLock.test.ts +0 -248
- package/src/hooks/useScrollOverflow.test.tsx +0 -222
- package/src/hooks/useScrollableArea.test.tsx +0 -836
- package/src/hooks/useStreamingText.test.ts +0 -237
- package/src/hooks/useTreeFocus.test.tsx +0 -351
- package/src/hooks/useTypeahead.test.tsx +0 -351
- package/src/i18n/InternationalizationProvider.spec.md +0 -252
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +0 -128
- package/src/i18n/__tests__/e2e-pagination.test.tsx +0 -172
- package/src/i18n/__tests__/e2e-powersearch.test.tsx +0 -166
- package/src/i18n/__tests__/getLocaleDirection.test.ts +0 -40
- package/src/i18n/__tests__/resolve.test.ts +0 -325
- package/src/i18n/__tests__/useCollator.test.tsx +0 -115
- package/src/i18n/__tests__/useDirection.test.tsx +0 -66
- package/src/i18n/__tests__/useLocale.test.tsx +0 -52
- package/src/i18n/useTranslator.test.tsx +0 -267
- package/src/inputFontFloor.test.ts +0 -106
- package/src/internal/tokenizer-presentation.test.ts +0 -29
- package/src/internal/touch.test.ts +0 -44
- package/src/motion/__tests__/animatedLayout.interaction.browser.spec.ts +0 -1613
- package/src/motion/__tests__/animatedLayoutAdopters.interaction.browser.spec.ts +0 -217
- package/src/motion/__tests__/animatedLayoutBundle.ts +0 -17
- package/src/motion/animatedLayout.boundary.test.ts +0 -155
- package/src/motion/animatedLayout.swap.test.ts +0 -827
- package/src/motion/animatedLayout.test.ts +0 -1154
- package/src/motion/animatedLayoutAdopters.test.tsx +0 -500
- package/src/motion/animator.test.ts +0 -2653
- package/src/motion/bezier.test.ts +0 -52
- package/src/motion/coast.test.ts +0 -99
- package/src/motion/decay.test.ts +0 -120
- package/src/motion/projection.test.ts +0 -107
- package/src/motion/reducedMotion.test.ts +0 -129
- package/src/motion/reducedMotionPlan.test.ts +0 -182
- package/src/motion/resolveSnapPoint.test.ts +0 -189
- package/src/motion/roles.test.ts +0 -256
- package/src/motion/rubberBand.test.ts +0 -88
- package/src/motion/selectPage.test.ts +0 -31
- package/src/motion/spring.test.ts +0 -650
- package/src/motion/uikit/gestures.test.ts +0 -89
- package/src/motion/uikit/springRows.test.ts +0 -71
- package/src/motion/uikit/uikit.test.ts +0 -364
- package/src/motion/uikit/values.test.ts +0 -110
- package/src/naming.test.ts +0 -44
- package/src/postinstall.test.mjs +0 -216
- package/src/reset.a11y.chromium.spec.ts +0 -238
- package/src/reset.test.ts +0 -205
- package/src/reset.until-found.a11y.browser.spec.ts +0 -76
- package/src/reset.until-found.test.ts +0 -32
- package/src/serverSafeComponents.test.ts +0 -675
- package/src/stylexValidation.source-build.test.mjs +0 -42
- package/src/tailwind-theme.test.ts +0 -153
- package/src/theme/MediaTheme.dom.test.tsx +0 -97
- package/src/theme/Theme.test.tsx +0 -337
- package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +0 -199
- package/src/theme/contrast.test.ts +0 -94
- package/src/theme/declarationBoundary.test.ts +0 -126
- package/src/theme/defineTheme.test.ts +0 -2028
- package/src/theme/derivedVarRegistry.test.ts +0 -588
- package/src/theme/expandColorScale.test.ts +0 -651
- package/src/theme/expandMotionScale.test.ts +0 -145
- package/src/theme/expandRadiusScale.test.ts +0 -59
- package/src/theme/expandTypeScale.test.ts +0 -351
- package/src/theme/extensibleAxes.test.ts +0 -432
- package/src/theme/generateThemeRules.test.ts +0 -1870
- package/src/theme/hct.test.ts +0 -93
- package/src/theme/onMediaTokens.test.ts +0 -431
- package/src/theme/publicThemeHelperContract.test.ts +0 -67
- package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +0 -91
- package/src/theme/syntax/serverSafeSyntax.test.ts +0 -68
- package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +0 -40
- package/src/theme/themeAdaptations.test.ts +0 -1795
- package/src/theme/themeRegistry.test.ts +0 -59
- package/src/theme/themingTargets.test.ts +0 -589
- package/src/theme/tokenValueCompat.test.ts +0 -204
- package/src/theme/tokens.test.ts +0 -441
- package/src/theme/useTheme.test.tsx +0 -322
- package/src/unicodePropertyEscapes.source-build.test.mjs +0 -69
- package/src/utils/chainEventHandlers.test.ts +0 -39
- package/src/utils/characters.test.ts +0 -141
- package/src/utils/color.test.ts +0 -233
- package/src/utils/composeEventHandlers.test.ts +0 -71
- package/src/utils/customPropertyArmLayering.test.ts +0 -131
- package/src/utils/dateParser.test.ts +0 -402
- package/src/utils/dateTypes.test.ts +0 -40
- package/src/utils/devWarning.test.ts +0 -56
- package/src/utils/edgeAutoscroll.test.ts +0 -192
- package/src/utils/focusReturn.test.ts +0 -119
- package/src/utils/getKey.test.ts +0 -25
- package/src/utils/hitArea.test.ts +0 -628
- package/src/utils/ime.test.ts +0 -32
- package/src/utils/inputAria.test.ts +0 -39
- package/src/utils/interactionModality.test.ts +0 -100
- package/src/utils/isApplePlatform.test.ts +0 -61
- package/src/utils/mergeProps.test.ts +0 -166
- package/src/utils/mergeRefs.test.ts +0 -48
- package/src/utils/parseStyleKey.test.ts +0 -56
- package/src/utils/plainDate.test.ts +0 -848
- package/src/utils/pressFeedback.test.ts +0 -874
- package/src/utils/pressGesture.test.ts +0 -323
- package/src/utils/pressPaintSource.test.ts +0 -65
- package/src/utils/pressableCoverage.test.ts +0 -192
- package/src/utils/safeUrl.test.ts +0 -101
- package/src/utils/sharedMutationObserver.test.ts +0 -53
- package/src/utils/sharedResizeObserver.test.ts +0 -283
- package/src/utils/themeProps.test.ts +0 -62
- package/src/utils/timeParser.test.ts +0 -295
- package/src/utils/touchPresentation.test.ts +0 -323
|
@@ -1,4702 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file MultiSelector.test.tsx
|
|
5
|
-
* @input Uses vitest, @testing-library/react, @testing-library/user-event
|
|
6
|
-
* @output Unit tests for MultiSelector
|
|
7
|
-
* @position Tests; validates MultiSelector behavior
|
|
8
|
-
*
|
|
9
|
-
* SYNC: When MultiSelector.tsx API changes, update these tests.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
13
|
-
import {
|
|
14
|
-
act,
|
|
15
|
-
render,
|
|
16
|
-
screen,
|
|
17
|
-
fireEvent,
|
|
18
|
-
waitFor,
|
|
19
|
-
within,
|
|
20
|
-
} from '@testing-library/react';
|
|
21
|
-
import userEvent from '@testing-library/user-event';
|
|
22
|
-
import * as stylex from '@stylexjs/stylex';
|
|
23
|
-
import {
|
|
24
|
-
MultiSelector,
|
|
25
|
-
type MultiSelectorHandle,
|
|
26
|
-
type MultiSelectorChange,
|
|
27
|
-
} from './MultiSelector';
|
|
28
|
-
import {useRef, useState} from 'react';
|
|
29
|
-
import {Icon} from '../Icon';
|
|
30
|
-
import {InternationalizationProvider} from '../i18n';
|
|
31
|
-
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
32
|
-
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
33
|
-
import {defineTheme} from '../theme/defineTheme';
|
|
34
|
-
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
35
|
-
import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
|
|
36
|
-
import {groupStyles} from '../InputGroup/groupStyles';
|
|
37
|
-
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
38
|
-
import {colorVars, focusVars} from '../theme/tokens.stylex';
|
|
39
|
-
import {COMPACT_TOUCH_PRESENTATION_QUERY} from '../hooks/useAdaptivePresentation';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Enters a search query as one change. Typing it key by key can land several
|
|
43
|
-
* keystrokes in one frame, whose result announcements are then joined into
|
|
44
|
-
* one (spec:AST-086 FR9), so a test of what one query announces pastes it.
|
|
45
|
-
*/
|
|
46
|
-
async function enterQuery(
|
|
47
|
-
user: ReturnType<typeof userEvent.setup>,
|
|
48
|
-
input: HTMLElement,
|
|
49
|
-
query: string,
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
await user.click(input);
|
|
52
|
-
await user.paste(query);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
56
|
-
const {prose, component} = generateThemeCSS(theme);
|
|
57
|
-
return [prose, component].filter(Boolean).join('\n\n');
|
|
58
|
-
}
|
|
59
|
-
// Module-level constants to satisfy @eslint-react/no-unstable-default-props.
|
|
60
|
-
const ANNOUNCE_OPTIONS = ['Apple', 'Banana', 'Orange'] as const;
|
|
61
|
-
const EMPTY_VALUE: string[] = [];
|
|
62
|
-
|
|
63
|
-
function politeRegion(): HTMLElement | null {
|
|
64
|
-
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Mock showPopover and hidePopover methods since they're not implemented in jsdom
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
__resetInteractionModalityForTest();
|
|
70
|
-
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
71
|
-
this: HTMLDialogElement,
|
|
72
|
-
) {
|
|
73
|
-
this.setAttribute('open', '');
|
|
74
|
-
});
|
|
75
|
-
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
76
|
-
this.removeAttribute('open');
|
|
77
|
-
});
|
|
78
|
-
vi.stubGlobal(
|
|
79
|
-
'matchMedia',
|
|
80
|
-
vi.fn().mockImplementation((query: string) => ({
|
|
81
|
-
matches: false,
|
|
82
|
-
media: query,
|
|
83
|
-
onchange: null,
|
|
84
|
-
addEventListener: vi.fn(),
|
|
85
|
-
removeEventListener: vi.fn(),
|
|
86
|
-
addListener: vi.fn(),
|
|
87
|
-
removeListener: vi.fn(),
|
|
88
|
-
dispatchEvent: vi.fn(),
|
|
89
|
-
})),
|
|
90
|
-
);
|
|
91
|
-
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
92
|
-
this.setAttribute('popover-open', '');
|
|
93
|
-
const event = new Event('toggle', {bubbles: false});
|
|
94
|
-
Object.defineProperty(event, 'newState', {value: 'open'});
|
|
95
|
-
this.dispatchEvent(event);
|
|
96
|
-
});
|
|
97
|
-
HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
|
|
98
|
-
this.removeAttribute('popover-open');
|
|
99
|
-
const event = new Event('toggle', {bubbles: false});
|
|
100
|
-
Object.defineProperty(event, 'newState', {value: 'closed'});
|
|
101
|
-
this.dispatchEvent(event);
|
|
102
|
-
});
|
|
103
|
-
const originalMatches = HTMLElement.prototype.matches;
|
|
104
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
-
(HTMLElement.prototype as any).matches = function (
|
|
106
|
-
selector: string,
|
|
107
|
-
): boolean {
|
|
108
|
-
if (selector === ':popover-open') {
|
|
109
|
-
return this.hasAttribute('popover-open');
|
|
110
|
-
}
|
|
111
|
-
return originalMatches.call(this, selector);
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
afterEach(() => {
|
|
116
|
-
__resetLiveRegionsForTest();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Helper: jsdom popover content is in the DOM but may not be
|
|
120
|
-
// "visible" in the accessibility tree. Use hidden: true to find it.
|
|
121
|
-
const h = {hidden: true} as const;
|
|
122
|
-
|
|
123
|
-
/** A busy control's Spinner is decorative and silent (spec:AST-086 FR8). */
|
|
124
|
-
function expectSilentBusySpinner() {
|
|
125
|
-
const spinner = document.querySelector('.astryx-spinner');
|
|
126
|
-
expect(spinner).not.toBeNull();
|
|
127
|
-
expect(spinner).toHaveAttribute('aria-hidden', 'true');
|
|
128
|
-
expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
describe('MultiSelector', () => {
|
|
132
|
-
const defaultOptions = ['Apple', 'Banana', 'Orange'];
|
|
133
|
-
|
|
134
|
-
it('uses a bottom sheet and stays open while toggling options', async () => {
|
|
135
|
-
const user = userEvent.setup();
|
|
136
|
-
const onChange = vi.fn();
|
|
137
|
-
render(
|
|
138
|
-
<MultiSelector
|
|
139
|
-
label="Fruit"
|
|
140
|
-
options={defaultOptions}
|
|
141
|
-
value={[]}
|
|
142
|
-
onChange={onChange}
|
|
143
|
-
presentation="bottom-sheet"
|
|
144
|
-
/>,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const trigger = screen.getByRole('combobox');
|
|
148
|
-
await user.click(trigger);
|
|
149
|
-
|
|
150
|
-
expect(
|
|
151
|
-
await screen.findByRole('dialog', {name: 'Fruit'}),
|
|
152
|
-
).toBeInTheDocument();
|
|
153
|
-
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
154
|
-
|
|
155
|
-
await user.click(screen.getByRole('option', {name: 'Banana'}));
|
|
156
|
-
expect(onChange).toHaveBeenCalledWith(['Banana']);
|
|
157
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('uses a bottom sheet for adaptive presentation on compact touch', async () => {
|
|
161
|
-
vi.stubGlobal(
|
|
162
|
-
'matchMedia',
|
|
163
|
-
vi.fn().mockImplementation((query: string) => ({
|
|
164
|
-
matches: query === COMPACT_TOUCH_PRESENTATION_QUERY,
|
|
165
|
-
media: query,
|
|
166
|
-
onchange: null,
|
|
167
|
-
addEventListener: vi.fn(),
|
|
168
|
-
removeEventListener: vi.fn(),
|
|
169
|
-
addListener: vi.fn(),
|
|
170
|
-
removeListener: vi.fn(),
|
|
171
|
-
dispatchEvent: vi.fn(),
|
|
172
|
-
})),
|
|
173
|
-
);
|
|
174
|
-
const user = userEvent.setup();
|
|
175
|
-
render(
|
|
176
|
-
<MultiSelector
|
|
177
|
-
label="Fruit"
|
|
178
|
-
options={defaultOptions}
|
|
179
|
-
value={[]}
|
|
180
|
-
onChange={() => {}}
|
|
181
|
-
presentation="adaptive"
|
|
182
|
-
/>,
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
await user.click(screen.getByRole('combobox'));
|
|
186
|
-
expect(
|
|
187
|
-
await screen.findByRole('dialog', {name: 'Fruit'}),
|
|
188
|
-
).toBeInTheDocument();
|
|
189
|
-
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('moves keyboard focus into a bottom-sheet listbox', async () => {
|
|
193
|
-
const user = userEvent.setup();
|
|
194
|
-
render(
|
|
195
|
-
<MultiSelector
|
|
196
|
-
label="Fruit"
|
|
197
|
-
options={defaultOptions}
|
|
198
|
-
value={[]}
|
|
199
|
-
onChange={() => {}}
|
|
200
|
-
presentation="bottom-sheet"
|
|
201
|
-
/>,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
const trigger = screen.getByRole('combobox');
|
|
205
|
-
trigger.focus();
|
|
206
|
-
await user.keyboard('{Enter}');
|
|
207
|
-
|
|
208
|
-
await waitFor(() => expect(screen.getByRole('listbox')).toHaveFocus());
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('restores touch focus without painting a trigger focus ring', async () => {
|
|
212
|
-
render(
|
|
213
|
-
<MultiSelector
|
|
214
|
-
label="Fruit"
|
|
215
|
-
options={defaultOptions}
|
|
216
|
-
value={[]}
|
|
217
|
-
onChange={() => {}}
|
|
218
|
-
presentation="bottom-sheet"
|
|
219
|
-
/>,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const trigger = screen.getByRole('combobox');
|
|
223
|
-
fireEvent.pointerDown(trigger, {pointerType: 'touch'});
|
|
224
|
-
fireEvent.click(trigger, {detail: 1});
|
|
225
|
-
const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
|
|
226
|
-
fireEvent.pointerDown(dialog, {pointerType: 'touch'});
|
|
227
|
-
fireEvent.click(dialog, {detail: 1});
|
|
228
|
-
|
|
229
|
-
trigger.focus();
|
|
230
|
-
expect(trigger).toHaveFocus();
|
|
231
|
-
expect(trigger.parentElement).toHaveClass(
|
|
232
|
-
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
233
|
-
);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('shows no disabled focus ring after a pointer pick (spec:AST-085 FR6)', async () => {
|
|
237
|
-
const field = (isDisabled: boolean) => (
|
|
238
|
-
<MultiSelector
|
|
239
|
-
label="Fruit"
|
|
240
|
-
options={defaultOptions}
|
|
241
|
-
value={[]}
|
|
242
|
-
onChange={() => {}}
|
|
243
|
-
presentation="bottom-sheet"
|
|
244
|
-
isDisabled={isDisabled}
|
|
245
|
-
/>
|
|
246
|
-
);
|
|
247
|
-
const {rerender} = render(field(false));
|
|
248
|
-
const trigger = screen.getByRole('combobox');
|
|
249
|
-
fireEvent.pointerDown(trigger, {pointerType: 'touch'});
|
|
250
|
-
fireEvent.click(trigger, {detail: 1});
|
|
251
|
-
const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
|
|
252
|
-
fireEvent.pointerDown(dialog, {pointerType: 'touch'});
|
|
253
|
-
fireEvent.click(dialog, {detail: 1});
|
|
254
|
-
rerender(field(true));
|
|
255
|
-
const ring = stylex
|
|
256
|
-
.props(
|
|
257
|
-
stylex.create({
|
|
258
|
-
ring: {
|
|
259
|
-
boxShadow: {
|
|
260
|
-
default: null,
|
|
261
|
-
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
}).ring,
|
|
265
|
-
)
|
|
266
|
-
.className!.split(' ')
|
|
267
|
-
.pop()!;
|
|
268
|
-
const wrapper = screen.getByRole('combobox').parentElement!;
|
|
269
|
-
expect(wrapper).toHaveClass(
|
|
270
|
-
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
271
|
-
);
|
|
272
|
-
expect(wrapper).not.toHaveClass(ring);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('renders with label', () => {
|
|
276
|
-
render(
|
|
277
|
-
<MultiSelector
|
|
278
|
-
label="Fruit"
|
|
279
|
-
options={defaultOptions}
|
|
280
|
-
value={[]}
|
|
281
|
-
onChange={() => {}}
|
|
282
|
-
/>,
|
|
283
|
-
);
|
|
284
|
-
expect(screen.getByLabelText('Fruit')).toBeInTheDocument();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('applies distinct trigger chrome to the secondary variant', () => {
|
|
288
|
-
// data-testid reaches the native control; the variant is on the trigger root.
|
|
289
|
-
const triggerRoot = () =>
|
|
290
|
-
screen
|
|
291
|
-
.getByTestId('selector')
|
|
292
|
-
.closest('.astryx-multi-selector') as HTMLElement;
|
|
293
|
-
const {rerender} = render(
|
|
294
|
-
<MultiSelector
|
|
295
|
-
label="Fruit"
|
|
296
|
-
options={defaultOptions}
|
|
297
|
-
value={[]}
|
|
298
|
-
onChange={() => {}}
|
|
299
|
-
variant="input"
|
|
300
|
-
data-testid="selector"
|
|
301
|
-
/>,
|
|
302
|
-
);
|
|
303
|
-
const inputClasses = triggerRoot().className;
|
|
304
|
-
|
|
305
|
-
rerender(
|
|
306
|
-
<MultiSelector
|
|
307
|
-
label="Fruit"
|
|
308
|
-
options={defaultOptions}
|
|
309
|
-
value={[]}
|
|
310
|
-
onChange={() => {}}
|
|
311
|
-
variant="secondary"
|
|
312
|
-
data-testid="selector"
|
|
313
|
-
/>,
|
|
314
|
-
);
|
|
315
|
-
const secondary = triggerRoot();
|
|
316
|
-
const secondaryClasses = secondary.className;
|
|
317
|
-
expect(secondary).toHaveAttribute('data-variant', 'secondary');
|
|
318
|
-
|
|
319
|
-
rerender(
|
|
320
|
-
<MultiSelector
|
|
321
|
-
label="Fruit"
|
|
322
|
-
options={defaultOptions}
|
|
323
|
-
value={[]}
|
|
324
|
-
onChange={() => {}}
|
|
325
|
-
variant="ghost"
|
|
326
|
-
data-testid="selector"
|
|
327
|
-
/>,
|
|
328
|
-
);
|
|
329
|
-
const ghostClasses = triggerRoot().className;
|
|
330
|
-
|
|
331
|
-
expect(secondaryClasses).not.toBe(inputClasses);
|
|
332
|
-
expect(secondaryClasses).not.toBe(ghostClasses);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('renders custom option content with renderOption', async () => {
|
|
336
|
-
const user = userEvent.setup();
|
|
337
|
-
render(
|
|
338
|
-
<MultiSelector
|
|
339
|
-
label="Fruit"
|
|
340
|
-
options={[{value: 'apple', label: 'Apple'}]}
|
|
341
|
-
value={[]}
|
|
342
|
-
onChange={() => {}}
|
|
343
|
-
renderOption={option => (
|
|
344
|
-
<span data-testid="custom-option">{option.label}</span>
|
|
345
|
-
)}
|
|
346
|
-
/>,
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
await user.click(screen.getByRole('combobox'));
|
|
350
|
-
expect(screen.getByTestId('custom-option')).toHaveTextContent('Apple');
|
|
351
|
-
});
|
|
352
|
-
it('renders placeholder when no value selected', () => {
|
|
353
|
-
render(
|
|
354
|
-
<MultiSelector
|
|
355
|
-
label="Fruit"
|
|
356
|
-
options={defaultOptions}
|
|
357
|
-
value={[]}
|
|
358
|
-
onChange={() => {}}
|
|
359
|
-
placeholder="Pick fruits..."
|
|
360
|
-
/>,
|
|
361
|
-
);
|
|
362
|
-
expect(screen.getByText('Pick fruits...')).toBeInTheDocument();
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('shows count display by default', () => {
|
|
366
|
-
render(
|
|
367
|
-
<MultiSelector
|
|
368
|
-
label="Fruit"
|
|
369
|
-
options={defaultOptions}
|
|
370
|
-
value={['Apple', 'Banana']}
|
|
371
|
-
onChange={() => {}}
|
|
372
|
-
/>,
|
|
373
|
-
);
|
|
374
|
-
expect(screen.getByText('2 selected')).toBeInTheDocument();
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('formats the count display with formatValue', () => {
|
|
378
|
-
render(
|
|
379
|
-
<MultiSelector
|
|
380
|
-
label="Spaces"
|
|
381
|
-
options={defaultOptions}
|
|
382
|
-
value={['Apple', 'Banana']}
|
|
383
|
-
onChange={() => {}}
|
|
384
|
-
formatValue={items => `${items.length} spaces selected`}
|
|
385
|
-
/>,
|
|
386
|
-
);
|
|
387
|
-
expect(screen.getByText('2 spaces selected')).toBeInTheDocument();
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('passes selected values and resolved labels to formatValue', () => {
|
|
391
|
-
const formatValue = vi.fn(
|
|
392
|
-
(items: {value: string; label: string}[]) =>
|
|
393
|
-
`${items[0].label} and ${items.length - 1} more`,
|
|
394
|
-
);
|
|
395
|
-
render(
|
|
396
|
-
<MultiSelector
|
|
397
|
-
label="Fruit"
|
|
398
|
-
options={[
|
|
399
|
-
{value: 'a', label: 'Apple'},
|
|
400
|
-
{value: 'b', label: 'Banana'},
|
|
401
|
-
]}
|
|
402
|
-
value={['a', 'b']}
|
|
403
|
-
onChange={() => {}}
|
|
404
|
-
formatValue={formatValue}
|
|
405
|
-
/>,
|
|
406
|
-
);
|
|
407
|
-
expect(formatValue).toHaveBeenCalledWith([
|
|
408
|
-
{value: 'a', label: 'Apple'},
|
|
409
|
-
{value: 'b', label: 'Banana'},
|
|
410
|
-
]);
|
|
411
|
-
expect(screen.getByText('Apple and 1 more')).toBeInTheDocument();
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it('formats the labels display with formatValue', () => {
|
|
415
|
-
render(
|
|
416
|
-
<MultiSelector
|
|
417
|
-
label="Fruit"
|
|
418
|
-
options={defaultOptions}
|
|
419
|
-
value={['Apple', 'Banana', 'Orange']}
|
|
420
|
-
onChange={() => {}}
|
|
421
|
-
triggerDisplay="labels"
|
|
422
|
-
formatValue={items => items.map(item => item.label).join(' & ')}
|
|
423
|
-
/>,
|
|
424
|
-
);
|
|
425
|
-
expect(screen.getByText('Apple & Banana & Orange')).toBeInTheDocument();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('ignores formatValue in badges display', () => {
|
|
429
|
-
render(
|
|
430
|
-
<MultiSelector
|
|
431
|
-
label="Fruit"
|
|
432
|
-
options={defaultOptions}
|
|
433
|
-
value={['Apple', 'Banana']}
|
|
434
|
-
onChange={() => {}}
|
|
435
|
-
triggerDisplay="badges"
|
|
436
|
-
formatValue={() => 'formatted'}
|
|
437
|
-
/>,
|
|
438
|
-
);
|
|
439
|
-
expect(screen.queryByText('formatted')).not.toBeInTheDocument();
|
|
440
|
-
const trigger = within(screen.getByRole('combobox'));
|
|
441
|
-
expect(trigger.getByText('Apple')).toBeInTheDocument();
|
|
442
|
-
expect(trigger.getByText('Banana')).toBeInTheDocument();
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('shows the placeholder rather than calling formatValue when empty', () => {
|
|
446
|
-
const formatValue = vi.fn(() => 'formatted');
|
|
447
|
-
render(
|
|
448
|
-
<MultiSelector
|
|
449
|
-
label="Fruit"
|
|
450
|
-
options={defaultOptions}
|
|
451
|
-
value={[]}
|
|
452
|
-
onChange={() => {}}
|
|
453
|
-
placeholder="Pick fruits..."
|
|
454
|
-
formatValue={formatValue}
|
|
455
|
-
/>,
|
|
456
|
-
);
|
|
457
|
-
expect(screen.getByText('Pick fruits...')).toBeInTheDocument();
|
|
458
|
-
expect(formatValue).not.toHaveBeenCalled();
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it('shows labels display', () => {
|
|
462
|
-
render(
|
|
463
|
-
<MultiSelector
|
|
464
|
-
label="Fruit"
|
|
465
|
-
options={defaultOptions}
|
|
466
|
-
value={['Apple', 'Banana']}
|
|
467
|
-
onChange={() => {}}
|
|
468
|
-
triggerDisplay="labels"
|
|
469
|
-
/>,
|
|
470
|
-
);
|
|
471
|
-
expect(screen.getByText('Apple, Banana')).toBeInTheDocument();
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('shows labels display with overflow', () => {
|
|
475
|
-
render(
|
|
476
|
-
<MultiSelector
|
|
477
|
-
label="Fruit"
|
|
478
|
-
options={['Apple', 'Banana', 'Orange', 'Mango', 'Pineapple']}
|
|
479
|
-
value={['Apple', 'Banana', 'Orange', 'Mango', 'Pineapple']}
|
|
480
|
-
onChange={() => {}}
|
|
481
|
-
triggerDisplay="labels"
|
|
482
|
-
/>,
|
|
483
|
-
);
|
|
484
|
-
expect(screen.getByText('Apple, Banana, Orange, +2')).toBeInTheDocument();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it('opens dropdown on click', async () => {
|
|
488
|
-
const user = userEvent.setup();
|
|
489
|
-
render(
|
|
490
|
-
<MultiSelector
|
|
491
|
-
label="Fruit"
|
|
492
|
-
options={defaultOptions}
|
|
493
|
-
value={[]}
|
|
494
|
-
onChange={() => {}}
|
|
495
|
-
/>,
|
|
496
|
-
);
|
|
497
|
-
const trigger = screen.getByRole('combobox');
|
|
498
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
499
|
-
|
|
500
|
-
await user.click(trigger);
|
|
501
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
it('toggles item on click without closing dropdown', async () => {
|
|
505
|
-
const user = userEvent.setup();
|
|
506
|
-
const onChange = vi.fn();
|
|
507
|
-
render(
|
|
508
|
-
<MultiSelector
|
|
509
|
-
label="Fruit"
|
|
510
|
-
options={defaultOptions}
|
|
511
|
-
value={[]}
|
|
512
|
-
onChange={onChange}
|
|
513
|
-
/>,
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
await user.click(screen.getByRole('combobox'));
|
|
517
|
-
|
|
518
|
-
const options = screen.getAllByRole('option', h);
|
|
519
|
-
await user.click(options[0]);
|
|
520
|
-
|
|
521
|
-
expect(onChange).toHaveBeenCalledWith(['Apple']);
|
|
522
|
-
// Dropdown should still be open
|
|
523
|
-
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
524
|
-
'aria-expanded',
|
|
525
|
-
'true',
|
|
526
|
-
);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it('deselects item when clicking selected item', async () => {
|
|
530
|
-
const user = userEvent.setup();
|
|
531
|
-
const onChange = vi.fn();
|
|
532
|
-
render(
|
|
533
|
-
<MultiSelector
|
|
534
|
-
label="Fruit"
|
|
535
|
-
options={defaultOptions}
|
|
536
|
-
value={['Apple', 'Banana']}
|
|
537
|
-
onChange={onChange}
|
|
538
|
-
/>,
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
await user.click(screen.getByRole('combobox'));
|
|
542
|
-
const options = screen.getAllByRole('option', h);
|
|
543
|
-
await user.click(options[0]); // Click Apple to deselect
|
|
544
|
-
|
|
545
|
-
expect(onChange).toHaveBeenCalledWith(['Banana']);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it('does not toggle disabled items', async () => {
|
|
549
|
-
const user = userEvent.setup();
|
|
550
|
-
const onChange = vi.fn();
|
|
551
|
-
render(
|
|
552
|
-
<MultiSelector
|
|
553
|
-
label="Fruit"
|
|
554
|
-
options={[
|
|
555
|
-
{value: 'apple', label: 'Apple', disabled: true},
|
|
556
|
-
{value: 'banana', label: 'Banana'},
|
|
557
|
-
]}
|
|
558
|
-
value={[]}
|
|
559
|
-
onChange={onChange}
|
|
560
|
-
/>,
|
|
561
|
-
);
|
|
562
|
-
|
|
563
|
-
await user.click(screen.getByRole('combobox'));
|
|
564
|
-
const options = screen.getAllByRole('option', h);
|
|
565
|
-
await user.click(options[0]); // Click disabled Apple
|
|
566
|
-
|
|
567
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('renders disabled state', () => {
|
|
571
|
-
render(
|
|
572
|
-
<MultiSelector
|
|
573
|
-
label="Fruit"
|
|
574
|
-
options={defaultOptions}
|
|
575
|
-
value={[]}
|
|
576
|
-
onChange={() => {}}
|
|
577
|
-
isDisabled
|
|
578
|
-
/>,
|
|
579
|
-
);
|
|
580
|
-
const trigger = screen.getByRole('combobox');
|
|
581
|
-
expect(trigger).not.toHaveAttribute('disabled');
|
|
582
|
-
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it('has correct ARIA attributes', () => {
|
|
586
|
-
render(
|
|
587
|
-
<MultiSelector
|
|
588
|
-
label="Fruit"
|
|
589
|
-
options={defaultOptions}
|
|
590
|
-
value={[]}
|
|
591
|
-
onChange={() => {}}
|
|
592
|
-
isRequired
|
|
593
|
-
/>,
|
|
594
|
-
);
|
|
595
|
-
const trigger = screen.getByRole('combobox');
|
|
596
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
|
|
597
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
598
|
-
expect(trigger).toHaveAttribute('aria-required', 'true');
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it('shows error status with aria-invalid', () => {
|
|
602
|
-
render(
|
|
603
|
-
<MultiSelector
|
|
604
|
-
label="Fruit"
|
|
605
|
-
options={defaultOptions}
|
|
606
|
-
value={[]}
|
|
607
|
-
onChange={() => {}}
|
|
608
|
-
status={{type: 'error', message: 'Required'}}
|
|
609
|
-
/>,
|
|
610
|
-
);
|
|
611
|
-
const trigger = screen.getByRole('combobox');
|
|
612
|
-
expect(trigger).toHaveAttribute('aria-invalid', 'true');
|
|
613
|
-
expect(screen.getByText('Required')).toBeInTheDocument();
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it('closes on Escape', async () => {
|
|
617
|
-
const user = userEvent.setup();
|
|
618
|
-
render(
|
|
619
|
-
<MultiSelector
|
|
620
|
-
label="Fruit"
|
|
621
|
-
options={defaultOptions}
|
|
622
|
-
value={[]}
|
|
623
|
-
onChange={() => {}}
|
|
624
|
-
/>,
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
const trigger = screen.getByRole('combobox');
|
|
628
|
-
await user.click(trigger);
|
|
629
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
630
|
-
|
|
631
|
-
await user.keyboard('{Escape}');
|
|
632
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it('closes combobox on Tab and moves focus to next element', async () => {
|
|
636
|
-
const user = userEvent.setup();
|
|
637
|
-
render(
|
|
638
|
-
<>
|
|
639
|
-
<MultiSelector
|
|
640
|
-
label="Fruit"
|
|
641
|
-
options={defaultOptions}
|
|
642
|
-
value={[]}
|
|
643
|
-
onChange={() => {}}
|
|
644
|
-
/>
|
|
645
|
-
<button type="button">Next</button>
|
|
646
|
-
</>,
|
|
647
|
-
);
|
|
648
|
-
|
|
649
|
-
const trigger = screen.getByRole('combobox');
|
|
650
|
-
await user.click(trigger);
|
|
651
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
652
|
-
|
|
653
|
-
await user.keyboard('{Tab}');
|
|
654
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
655
|
-
expect(screen.getByRole('button', {name: 'Next'})).toHaveFocus();
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
it('supports keyboard navigation with ArrowDown/ArrowUp', async () => {
|
|
659
|
-
const user = userEvent.setup();
|
|
660
|
-
render(
|
|
661
|
-
<MultiSelector
|
|
662
|
-
label="Fruit"
|
|
663
|
-
options={defaultOptions}
|
|
664
|
-
value={[]}
|
|
665
|
-
onChange={() => {}}
|
|
666
|
-
/>,
|
|
667
|
-
);
|
|
668
|
-
|
|
669
|
-
const trigger = screen.getByRole('combobox');
|
|
670
|
-
await user.click(trigger);
|
|
671
|
-
|
|
672
|
-
await user.keyboard('{ArrowDown}');
|
|
673
|
-
const activeId = trigger.getAttribute('aria-activedescendant');
|
|
674
|
-
expect(activeId).toBeTruthy();
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
it('End/Home jump the highlight to the last/first option (non-search)', async () => {
|
|
678
|
-
const user = userEvent.setup();
|
|
679
|
-
render(
|
|
680
|
-
<MultiSelector
|
|
681
|
-
label="Fruit"
|
|
682
|
-
options={defaultOptions}
|
|
683
|
-
value={[]}
|
|
684
|
-
onChange={() => {}}
|
|
685
|
-
/>,
|
|
686
|
-
);
|
|
687
|
-
|
|
688
|
-
const trigger = screen.getByRole('combobox');
|
|
689
|
-
await user.click(trigger);
|
|
690
|
-
const options = screen.getAllByRole('option', h);
|
|
691
|
-
|
|
692
|
-
await user.keyboard('{End}');
|
|
693
|
-
expect(trigger).toHaveAttribute(
|
|
694
|
-
'aria-activedescendant',
|
|
695
|
-
options[options.length - 1].id,
|
|
696
|
-
);
|
|
697
|
-
await user.keyboard('{Home}');
|
|
698
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it('toggles item with Enter key', async () => {
|
|
702
|
-
const user = userEvent.setup();
|
|
703
|
-
const onChange = vi.fn();
|
|
704
|
-
render(
|
|
705
|
-
<MultiSelector
|
|
706
|
-
label="Fruit"
|
|
707
|
-
options={defaultOptions}
|
|
708
|
-
value={[]}
|
|
709
|
-
onChange={onChange}
|
|
710
|
-
/>,
|
|
711
|
-
);
|
|
712
|
-
|
|
713
|
-
const trigger = screen.getByRole('combobox');
|
|
714
|
-
await user.click(trigger);
|
|
715
|
-
await user.keyboard('{Enter}');
|
|
716
|
-
expect(onChange).toHaveBeenCalledWith(['Apple']);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it('does not toggle the highlighted option on a composing Enter (IME)', async () => {
|
|
720
|
-
const user = userEvent.setup();
|
|
721
|
-
const onChange = vi.fn();
|
|
722
|
-
render(
|
|
723
|
-
<MultiSelector
|
|
724
|
-
label="Fruit"
|
|
725
|
-
options={defaultOptions}
|
|
726
|
-
value={[]}
|
|
727
|
-
onChange={onChange}
|
|
728
|
-
hasSearch
|
|
729
|
-
/>,
|
|
730
|
-
);
|
|
731
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
732
|
-
const search = screen.getByRole('combobox', h);
|
|
733
|
-
// Filter to Banana and highlight it so an unguarded Enter would toggle it.
|
|
734
|
-
await user.type(search, 'ban');
|
|
735
|
-
await user.keyboard('{ArrowDown}');
|
|
736
|
-
expect(search).toHaveAttribute('aria-activedescendant');
|
|
737
|
-
|
|
738
|
-
// The composing keydown (isComposing / legacy keyCode 229) that commits an
|
|
739
|
-
// IME candidate must not be read as "toggle the highlighted option".
|
|
740
|
-
fireEvent.keyDown(search, {key: 'Enter', isComposing: true});
|
|
741
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
742
|
-
fireEvent.keyDown(search, {key: 'Enter', keyCode: 229});
|
|
743
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
744
|
-
|
|
745
|
-
// A real, non-composing Enter still toggles the highlighted option.
|
|
746
|
-
fireEvent.keyDown(search, {key: 'Enter'});
|
|
747
|
-
expect(onChange).toHaveBeenCalledWith(['Banana']);
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
it('toggles the correct item when selected items are sorted to top', async () => {
|
|
751
|
-
const user = userEvent.setup();
|
|
752
|
-
const onChange = vi.fn();
|
|
753
|
-
// Orange is selected, so sorted order is: Orange, Apple, Banana
|
|
754
|
-
render(
|
|
755
|
-
<MultiSelector
|
|
756
|
-
label="Fruit"
|
|
757
|
-
options={defaultOptions}
|
|
758
|
-
value={['Orange']}
|
|
759
|
-
onChange={onChange}
|
|
760
|
-
/>,
|
|
761
|
-
);
|
|
762
|
-
|
|
763
|
-
const trigger = screen.getByRole('combobox');
|
|
764
|
-
await user.click(trigger);
|
|
765
|
-
// highlightedIndex starts at 0 which is Orange (sorted first)
|
|
766
|
-
await user.keyboard('{ArrowDown}');
|
|
767
|
-
// Now at index 1 which should be Apple
|
|
768
|
-
await user.keyboard('{Enter}');
|
|
769
|
-
expect(onChange).toHaveBeenCalledWith(['Orange', 'Apple']);
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it('renders select-all checkbox when hasSelectAll', async () => {
|
|
773
|
-
const user = userEvent.setup();
|
|
774
|
-
render(
|
|
775
|
-
<MultiSelector
|
|
776
|
-
label="Fruit"
|
|
777
|
-
options={defaultOptions}
|
|
778
|
-
value={[]}
|
|
779
|
-
onChange={() => {}}
|
|
780
|
-
hasSelectAll
|
|
781
|
-
/>,
|
|
782
|
-
);
|
|
783
|
-
|
|
784
|
-
await user.click(screen.getByRole('combobox'));
|
|
785
|
-
expect(screen.getByText('Select all')).toBeInTheDocument();
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
it('select-all selects all enabled items', async () => {
|
|
789
|
-
const user = userEvent.setup();
|
|
790
|
-
const onChange = vi.fn();
|
|
791
|
-
render(
|
|
792
|
-
<MultiSelector
|
|
793
|
-
label="Fruit"
|
|
794
|
-
options={[
|
|
795
|
-
{value: 'apple', label: 'Apple'},
|
|
796
|
-
{value: 'banana', label: 'Banana', disabled: true},
|
|
797
|
-
{value: 'orange', label: 'Orange'},
|
|
798
|
-
]}
|
|
799
|
-
value={[]}
|
|
800
|
-
onChange={onChange}
|
|
801
|
-
hasSelectAll
|
|
802
|
-
/>,
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
await user.click(screen.getByRole('combobox'));
|
|
806
|
-
const selectAll = screen.getByText('Select all');
|
|
807
|
-
await user.click(selectAll);
|
|
808
|
-
|
|
809
|
-
expect(onChange).toHaveBeenCalledWith(['apple', 'orange']);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it('select-all deselects all when all are selected', async () => {
|
|
813
|
-
const user = userEvent.setup();
|
|
814
|
-
const onChange = vi.fn();
|
|
815
|
-
render(
|
|
816
|
-
<MultiSelector
|
|
817
|
-
label="Fruit"
|
|
818
|
-
options={defaultOptions}
|
|
819
|
-
value={['Apple', 'Banana', 'Orange']}
|
|
820
|
-
onChange={onChange}
|
|
821
|
-
hasSelectAll
|
|
822
|
-
/>,
|
|
823
|
-
);
|
|
824
|
-
|
|
825
|
-
await user.click(screen.getByRole('combobox'));
|
|
826
|
-
const selectAll = screen.getByText('Select all');
|
|
827
|
-
await user.click(selectAll);
|
|
828
|
-
|
|
829
|
-
expect(onChange).toHaveBeenCalledWith([]);
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
it('select-all is a role="option" in the listbox', async () => {
|
|
833
|
-
const user = userEvent.setup();
|
|
834
|
-
render(
|
|
835
|
-
<MultiSelector
|
|
836
|
-
label="Fruit"
|
|
837
|
-
options={defaultOptions}
|
|
838
|
-
value={[]}
|
|
839
|
-
onChange={() => {}}
|
|
840
|
-
hasSelectAll
|
|
841
|
-
/>,
|
|
842
|
-
);
|
|
843
|
-
|
|
844
|
-
await user.click(screen.getByRole('combobox'));
|
|
845
|
-
const options = screen.getAllByRole('option', h);
|
|
846
|
-
expect(options[0]).toHaveTextContent('Select all');
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
it('select-all accessible name reflects none/partial/all selection', async () => {
|
|
850
|
-
const user = userEvent.setup();
|
|
851
|
-
const options = [
|
|
852
|
-
{value: 'apple', label: 'Apple'},
|
|
853
|
-
{value: 'banana', label: 'Banana'},
|
|
854
|
-
];
|
|
855
|
-
const {rerender} = render(
|
|
856
|
-
<MultiSelector
|
|
857
|
-
label="Fruit"
|
|
858
|
-
options={options}
|
|
859
|
-
value={[]}
|
|
860
|
-
onChange={() => {}}
|
|
861
|
-
hasSelectAll
|
|
862
|
-
/>,
|
|
863
|
-
);
|
|
864
|
-
|
|
865
|
-
await user.click(screen.getByRole('combobox'));
|
|
866
|
-
|
|
867
|
-
// None selected: plain name, not selected
|
|
868
|
-
let selectAll = screen.getAllByRole('option', h)[0];
|
|
869
|
-
expect(selectAll).not.toHaveAccessibleName(/partially selected/);
|
|
870
|
-
expect(selectAll).toHaveAttribute('aria-selected', 'false');
|
|
871
|
-
|
|
872
|
-
// Partial: aria-selected="mixed" is invalid on role="option", so the
|
|
873
|
-
// indeterminate state must be conveyed through the accessible name.
|
|
874
|
-
rerender(
|
|
875
|
-
<MultiSelector
|
|
876
|
-
label="Fruit"
|
|
877
|
-
options={options}
|
|
878
|
-
value={['apple']}
|
|
879
|
-
onChange={() => {}}
|
|
880
|
-
hasSelectAll
|
|
881
|
-
/>,
|
|
882
|
-
);
|
|
883
|
-
selectAll = screen.getAllByRole('option', h)[0];
|
|
884
|
-
expect(selectAll).toHaveAccessibleName('Select all, partially selected');
|
|
885
|
-
expect(selectAll).toHaveAttribute('aria-selected', 'false');
|
|
886
|
-
|
|
887
|
-
// All selected: plain name again, selected
|
|
888
|
-
rerender(
|
|
889
|
-
<MultiSelector
|
|
890
|
-
label="Fruit"
|
|
891
|
-
options={options}
|
|
892
|
-
value={['apple', 'banana']}
|
|
893
|
-
onChange={() => {}}
|
|
894
|
-
hasSelectAll
|
|
895
|
-
/>,
|
|
896
|
-
);
|
|
897
|
-
selectAll = screen.getAllByRole('option', h)[0];
|
|
898
|
-
expect(selectAll).not.toHaveAccessibleName(/partially selected/);
|
|
899
|
-
expect(selectAll).toHaveAttribute('aria-selected', 'true');
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
it('select-all toggles via keyboard Enter', async () => {
|
|
903
|
-
const user = userEvent.setup();
|
|
904
|
-
const onChange = vi.fn();
|
|
905
|
-
render(
|
|
906
|
-
<MultiSelector
|
|
907
|
-
label="Fruit"
|
|
908
|
-
options={defaultOptions}
|
|
909
|
-
value={[]}
|
|
910
|
-
onChange={onChange}
|
|
911
|
-
hasSelectAll
|
|
912
|
-
/>,
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
const trigger = screen.getByRole('combobox');
|
|
916
|
-
await user.click(trigger);
|
|
917
|
-
// highlightedIndex starts at 0 which is select-all
|
|
918
|
-
await user.keyboard('{Enter}');
|
|
919
|
-
expect(onChange).toHaveBeenCalledWith(['Apple', 'Banana', 'Orange']);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
it('renders search input when hasSearch', async () => {
|
|
923
|
-
const user = userEvent.setup();
|
|
924
|
-
render(
|
|
925
|
-
<MultiSelector
|
|
926
|
-
label="Fruit"
|
|
927
|
-
options={defaultOptions}
|
|
928
|
-
value={[]}
|
|
929
|
-
onChange={() => {}}
|
|
930
|
-
hasSearch
|
|
931
|
-
/>,
|
|
932
|
-
);
|
|
933
|
-
|
|
934
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
935
|
-
const searchInput = screen.getByRole('combobox', h);
|
|
936
|
-
expect(searchInput).toBeInTheDocument();
|
|
937
|
-
expect(searchInput).toHaveAttribute('aria-autocomplete', 'list');
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
it('filters options when searching', async () => {
|
|
941
|
-
const user = userEvent.setup();
|
|
942
|
-
render(
|
|
943
|
-
<MultiSelector
|
|
944
|
-
label="Fruit"
|
|
945
|
-
options={defaultOptions}
|
|
946
|
-
value={[]}
|
|
947
|
-
onChange={() => {}}
|
|
948
|
-
hasSearch
|
|
949
|
-
/>,
|
|
950
|
-
);
|
|
951
|
-
|
|
952
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
953
|
-
const searchInput = screen.getByRole('combobox', h);
|
|
954
|
-
await user.type(searchInput, 'app');
|
|
955
|
-
|
|
956
|
-
const options = screen.getAllByRole('option', h);
|
|
957
|
-
expect(options).toHaveLength(1);
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
describe('grouped search', () => {
|
|
961
|
-
const GROUPED = [
|
|
962
|
-
{
|
|
963
|
-
type: 'section' as const,
|
|
964
|
-
title: 'Citrus',
|
|
965
|
-
options: [
|
|
966
|
-
{value: 'orange', label: 'Orange'},
|
|
967
|
-
{value: 'lemon', label: 'Lemon'},
|
|
968
|
-
],
|
|
969
|
-
},
|
|
970
|
-
{
|
|
971
|
-
type: 'section' as const,
|
|
972
|
-
title: 'Berries',
|
|
973
|
-
options: [
|
|
974
|
-
{value: 'strawberry', label: 'Strawberry'},
|
|
975
|
-
{value: 'blueberry', label: 'Blueberry'},
|
|
976
|
-
],
|
|
977
|
-
},
|
|
978
|
-
];
|
|
979
|
-
|
|
980
|
-
it('keeps the group header above matching items while searching', async () => {
|
|
981
|
-
const user = userEvent.setup();
|
|
982
|
-
render(
|
|
983
|
-
<MultiSelector
|
|
984
|
-
label="Fruit"
|
|
985
|
-
options={GROUPED}
|
|
986
|
-
value={[]}
|
|
987
|
-
onChange={() => {}}
|
|
988
|
-
hasSearch
|
|
989
|
-
/>,
|
|
990
|
-
);
|
|
991
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
992
|
-
await user.type(screen.getByRole('combobox', h), 'orange');
|
|
993
|
-
|
|
994
|
-
expect(
|
|
995
|
-
screen.getByRole('group', {name: 'Citrus', ...h}),
|
|
996
|
-
).toBeInTheDocument();
|
|
997
|
-
const options = screen.getAllByRole('option', h);
|
|
998
|
-
expect(options).toHaveLength(1);
|
|
999
|
-
expect(options[0]).toHaveTextContent('Orange');
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
it('hides a group whose items have no match', async () => {
|
|
1003
|
-
const user = userEvent.setup();
|
|
1004
|
-
render(
|
|
1005
|
-
<MultiSelector
|
|
1006
|
-
label="Fruit"
|
|
1007
|
-
options={GROUPED}
|
|
1008
|
-
value={[]}
|
|
1009
|
-
onChange={() => {}}
|
|
1010
|
-
hasSearch
|
|
1011
|
-
/>,
|
|
1012
|
-
);
|
|
1013
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1014
|
-
await user.type(screen.getByRole('combobox', h), 'berry');
|
|
1015
|
-
|
|
1016
|
-
expect(
|
|
1017
|
-
screen.getByRole('group', {name: 'Berries', ...h}),
|
|
1018
|
-
).toBeInTheDocument();
|
|
1019
|
-
expect(
|
|
1020
|
-
screen.queryByRole('group', {name: 'Citrus', ...h}),
|
|
1021
|
-
).not.toBeInTheDocument();
|
|
1022
|
-
expect(screen.getAllByRole('option', h)).toHaveLength(2);
|
|
1023
|
-
});
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
it('PageDown/PageUp jump the highlight to the last/first filtered option', async () => {
|
|
1027
|
-
const user = userEvent.setup();
|
|
1028
|
-
render(
|
|
1029
|
-
<MultiSelector
|
|
1030
|
-
label="Fruit"
|
|
1031
|
-
options={defaultOptions}
|
|
1032
|
-
value={[]}
|
|
1033
|
-
onChange={() => {}}
|
|
1034
|
-
hasSearch
|
|
1035
|
-
/>,
|
|
1036
|
-
);
|
|
1037
|
-
|
|
1038
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1039
|
-
const searchInput = screen.getByRole('combobox', h);
|
|
1040
|
-
// Filter to Banana and Orange so "last" means last *visible* option.
|
|
1041
|
-
await user.type(searchInput, 'an');
|
|
1042
|
-
const options = screen.getAllByRole('option', h);
|
|
1043
|
-
expect(options).toHaveLength(2);
|
|
1044
|
-
|
|
1045
|
-
await user.keyboard('{PageDown}');
|
|
1046
|
-
expect(searchInput).toHaveAttribute(
|
|
1047
|
-
'aria-activedescendant',
|
|
1048
|
-
options[options.length - 1].id,
|
|
1049
|
-
);
|
|
1050
|
-
await user.keyboard('{PageUp}');
|
|
1051
|
-
expect(searchInput).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
it('Home/End move the search caret, not the option highlight', async () => {
|
|
1055
|
-
const user = userEvent.setup();
|
|
1056
|
-
render(
|
|
1057
|
-
<MultiSelector
|
|
1058
|
-
label="Fruit"
|
|
1059
|
-
options={defaultOptions}
|
|
1060
|
-
value={[]}
|
|
1061
|
-
onChange={() => {}}
|
|
1062
|
-
hasSearch
|
|
1063
|
-
/>,
|
|
1064
|
-
);
|
|
1065
|
-
|
|
1066
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1067
|
-
const searchInput = screen.getByRole<HTMLInputElement>('combobox', h);
|
|
1068
|
-
await user.type(searchInput, 'an');
|
|
1069
|
-
expect(searchInput.selectionStart).toBe(2);
|
|
1070
|
-
const activeBefore = searchInput.getAttribute('aria-activedescendant');
|
|
1071
|
-
// Home/End stay on the input for caret movement (APG editable combobox);
|
|
1072
|
-
// the option highlight must not move.
|
|
1073
|
-
await user.keyboard('{Home}');
|
|
1074
|
-
expect(searchInput.selectionStart).toBe(0);
|
|
1075
|
-
expect(searchInput.getAttribute('aria-activedescendant')).toBe(
|
|
1076
|
-
activeBefore,
|
|
1077
|
-
);
|
|
1078
|
-
await user.keyboard('{End}');
|
|
1079
|
-
expect(searchInput.selectionStart).toBe(2);
|
|
1080
|
-
expect(searchInput.getAttribute('aria-activedescendant')).toBe(
|
|
1081
|
-
activeBefore,
|
|
1082
|
-
);
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
it('shows empty state when search has no results', async () => {
|
|
1086
|
-
const user = userEvent.setup();
|
|
1087
|
-
render(
|
|
1088
|
-
<MultiSelector
|
|
1089
|
-
label="Fruit"
|
|
1090
|
-
options={defaultOptions}
|
|
1091
|
-
value={[]}
|
|
1092
|
-
onChange={() => {}}
|
|
1093
|
-
hasSearch
|
|
1094
|
-
/>,
|
|
1095
|
-
);
|
|
1096
|
-
|
|
1097
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1098
|
-
const searchInput = screen.getByRole('combobox', h);
|
|
1099
|
-
await user.type(searchInput, 'xyz');
|
|
1100
|
-
|
|
1101
|
-
expect(
|
|
1102
|
-
within(screen.getByRole('listbox', h)).getByText('No results found'),
|
|
1103
|
-
).toBeInTheDocument();
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
it('empty-state message is not exposed as a listbox child', async () => {
|
|
1107
|
-
const user = userEvent.setup();
|
|
1108
|
-
render(
|
|
1109
|
-
<MultiSelector
|
|
1110
|
-
label="Fruit"
|
|
1111
|
-
options={defaultOptions}
|
|
1112
|
-
value={[]}
|
|
1113
|
-
onChange={() => {}}
|
|
1114
|
-
hasSearch
|
|
1115
|
-
/>,
|
|
1116
|
-
);
|
|
1117
|
-
|
|
1118
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1119
|
-
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
1120
|
-
|
|
1121
|
-
// role="listbox" only permits option/group children — the visual
|
|
1122
|
-
// empty-state message must be presentational (it is announced through
|
|
1123
|
-
// the result-count live region instead).
|
|
1124
|
-
const listbox = screen.getByRole('listbox', h);
|
|
1125
|
-
const empty = within(listbox).getByText('No results found');
|
|
1126
|
-
expect(empty).toHaveAttribute('role', 'presentation');
|
|
1127
|
-
});
|
|
1128
|
-
|
|
1129
|
-
it('renders emptySearchText in place of the default message', async () => {
|
|
1130
|
-
const user = userEvent.setup();
|
|
1131
|
-
render(
|
|
1132
|
-
<MultiSelector
|
|
1133
|
-
label="Fruit"
|
|
1134
|
-
options={defaultOptions}
|
|
1135
|
-
value={[]}
|
|
1136
|
-
onChange={() => {}}
|
|
1137
|
-
hasSearch
|
|
1138
|
-
emptySearchText="Nothing like that here"
|
|
1139
|
-
/>,
|
|
1140
|
-
);
|
|
1141
|
-
|
|
1142
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1143
|
-
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
1144
|
-
|
|
1145
|
-
const listbox = screen.getByRole('listbox', h);
|
|
1146
|
-
expect(
|
|
1147
|
-
within(listbox).getByText('Nothing like that here'),
|
|
1148
|
-
).toBeInTheDocument();
|
|
1149
|
-
expect(
|
|
1150
|
-
within(listbox).queryByText('No results found'),
|
|
1151
|
-
).not.toBeInTheDocument();
|
|
1152
|
-
});
|
|
1153
|
-
|
|
1154
|
-
it('renders emptyText, not emptySearchText, with no options and no query', async () => {
|
|
1155
|
-
const user = userEvent.setup();
|
|
1156
|
-
render(
|
|
1157
|
-
<MultiSelector
|
|
1158
|
-
label="Fruit"
|
|
1159
|
-
options={[]}
|
|
1160
|
-
value={[]}
|
|
1161
|
-
onChange={() => {}}
|
|
1162
|
-
hasSearch
|
|
1163
|
-
emptySearchText="Nothing like that here"
|
|
1164
|
-
/>,
|
|
1165
|
-
);
|
|
1166
|
-
|
|
1167
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1168
|
-
|
|
1169
|
-
const listbox = screen.getByRole('listbox', h);
|
|
1170
|
-
expect(within(listbox).getByText('No options')).toBeInTheDocument();
|
|
1171
|
-
expect(
|
|
1172
|
-
within(listbox).queryByText('Nothing like that here'),
|
|
1173
|
-
).not.toBeInTheDocument();
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
it('announces emptySearchText, not the catalog default', async () => {
|
|
1177
|
-
const user = userEvent.setup();
|
|
1178
|
-
render(
|
|
1179
|
-
<MultiSelector
|
|
1180
|
-
label="Fruit"
|
|
1181
|
-
options={defaultOptions}
|
|
1182
|
-
value={[]}
|
|
1183
|
-
onChange={() => {}}
|
|
1184
|
-
hasSearch
|
|
1185
|
-
emptySearchText="Nothing like that here"
|
|
1186
|
-
/>,
|
|
1187
|
-
);
|
|
1188
|
-
|
|
1189
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1190
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
|
|
1191
|
-
|
|
1192
|
-
// The panel message is role="presentation", so the live region is the
|
|
1193
|
-
// only thing a screen reader gets — it has to say the same words.
|
|
1194
|
-
await waitFor(() =>
|
|
1195
|
-
expect(politeRegion()?.textContent).toBe('Nothing like that here'),
|
|
1196
|
-
);
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
// `emptySearchText` is typed ReactNode, which invites an element — a
|
|
1200
|
-
// "no results, create one" row is exactly what a dead end is for. The
|
|
1201
|
-
// announcement used to take the prop only when it was a string and
|
|
1202
|
-
// announce the catalog default otherwise, so a screen-reader user was told
|
|
1203
|
-
// something the sighted user was not reading (`spec:AST-056` AR1).
|
|
1204
|
-
it('announces the text of an element emptySearchText', async () => {
|
|
1205
|
-
const user = userEvent.setup();
|
|
1206
|
-
render(
|
|
1207
|
-
<MultiSelector
|
|
1208
|
-
label="Fruit"
|
|
1209
|
-
options={defaultOptions}
|
|
1210
|
-
value={[]}
|
|
1211
|
-
onChange={() => {}}
|
|
1212
|
-
hasSearch
|
|
1213
|
-
emptySearchText={
|
|
1214
|
-
<span>
|
|
1215
|
-
Nothing like that here. <a href="/new">Add a fruit</a>
|
|
1216
|
-
</span>
|
|
1217
|
-
}
|
|
1218
|
-
/>,
|
|
1219
|
-
);
|
|
1220
|
-
|
|
1221
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1222
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
|
|
1223
|
-
|
|
1224
|
-
await waitFor(() =>
|
|
1225
|
-
expect(politeRegion()?.textContent).toBe(
|
|
1226
|
-
'Nothing like that here. Add a fruit',
|
|
1227
|
-
),
|
|
1228
|
-
);
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
it('announces the text of an element emptyText', async () => {
|
|
1232
|
-
const user = userEvent.setup();
|
|
1233
|
-
render(
|
|
1234
|
-
<MultiSelector
|
|
1235
|
-
label="Fruit"
|
|
1236
|
-
options={[]}
|
|
1237
|
-
value={[]}
|
|
1238
|
-
onChange={() => {}}
|
|
1239
|
-
emptyText={
|
|
1240
|
-
<span>
|
|
1241
|
-
Add a fruit first<span aria-hidden="true"> →</span>
|
|
1242
|
-
</span>
|
|
1243
|
-
}
|
|
1244
|
-
/>,
|
|
1245
|
-
);
|
|
1246
|
-
|
|
1247
|
-
await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
|
|
1248
|
-
|
|
1249
|
-
// The decorative arrow is hidden from the accessibility tree on screen,
|
|
1250
|
-
// so it stays out of the announcement too.
|
|
1251
|
-
await waitFor(() =>
|
|
1252
|
-
expect(politeRegion()?.textContent).toBe('Add a fruit first'),
|
|
1253
|
-
);
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
it('announces text a child component renders, which is only in the DOM', async () => {
|
|
1257
|
-
const user = userEvent.setup();
|
|
1258
|
-
// The words exist nowhere in the node the caller passed — they are
|
|
1259
|
-
// produced inside this component's own render. Reading the DOM after
|
|
1260
|
-
// render is what makes them announceable.
|
|
1261
|
-
function Message() {
|
|
1262
|
-
return <span>Nothing like that here</span>;
|
|
1263
|
-
}
|
|
1264
|
-
render(
|
|
1265
|
-
<MultiSelector
|
|
1266
|
-
label="Fruit"
|
|
1267
|
-
options={defaultOptions}
|
|
1268
|
-
value={[]}
|
|
1269
|
-
onChange={() => {}}
|
|
1270
|
-
hasSearch
|
|
1271
|
-
emptySearchText={<Message />}
|
|
1272
|
-
/>,
|
|
1273
|
-
);
|
|
1274
|
-
|
|
1275
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1276
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
|
|
1277
|
-
|
|
1278
|
-
await waitFor(() =>
|
|
1279
|
-
expect(politeRegion()?.textContent).toBe('Nothing like that here'),
|
|
1280
|
-
);
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
it('announces the empty state when opened with no options', async () => {
|
|
1284
|
-
const user = userEvent.setup();
|
|
1285
|
-
render(
|
|
1286
|
-
<MultiSelector
|
|
1287
|
-
label="Fruit"
|
|
1288
|
-
options={[]}
|
|
1289
|
-
value={[]}
|
|
1290
|
-
onChange={() => {}}
|
|
1291
|
-
emptyText="Add a fruit first"
|
|
1292
|
-
/>,
|
|
1293
|
-
);
|
|
1294
|
-
|
|
1295
|
-
await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
|
|
1296
|
-
|
|
1297
|
-
await waitFor(() =>
|
|
1298
|
-
expect(politeRegion()?.textContent).toBe('Add a fruit first'),
|
|
1299
|
-
);
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
it('shows and announces nothing while isLoading', async () => {
|
|
1303
|
-
const user = userEvent.setup();
|
|
1304
|
-
render(
|
|
1305
|
-
<MultiSelector
|
|
1306
|
-
label="Fruit"
|
|
1307
|
-
options={[]}
|
|
1308
|
-
value={[]}
|
|
1309
|
-
onChange={() => {}}
|
|
1310
|
-
isLoading
|
|
1311
|
-
/>,
|
|
1312
|
-
);
|
|
1313
|
-
|
|
1314
|
-
const trigger = screen.getByRole('combobox', {name: 'Fruit'});
|
|
1315
|
-
await user.click(trigger);
|
|
1316
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1317
|
-
|
|
1318
|
-
// useAnnounce writes on the next animation frame, so an immediate read
|
|
1319
|
-
// would pass whether or not anything was announced.
|
|
1320
|
-
await act(
|
|
1321
|
-
async () =>
|
|
1322
|
-
void (await new Promise(resolve => requestAnimationFrame(resolve))),
|
|
1323
|
-
);
|
|
1324
|
-
|
|
1325
|
-
// The options have not arrived, so "No options" would be a claim the
|
|
1326
|
-
// component cannot make; the trigger's spinner carries the state.
|
|
1327
|
-
const listbox = screen.getByRole('listbox', h);
|
|
1328
|
-
expect(within(listbox).queryByText('No options')).not.toBeInTheDocument();
|
|
1329
|
-
// Only the busy start is spoken (spec:AST-086 FR8), never an empty or
|
|
1330
|
-
// no-results message the panel is suppressing.
|
|
1331
|
-
// (The channel clears its text on a timer, so it may already be empty.)
|
|
1332
|
-
expect(['', 'Loading']).toContain(politeRegion()?.textContent ?? '');
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
it('announces nothing while isLoading, matching the silent panel', async () => {
|
|
1336
|
-
const user = userEvent.setup();
|
|
1337
|
-
render(
|
|
1338
|
-
<MultiSelector
|
|
1339
|
-
label="Fruit"
|
|
1340
|
-
options={defaultOptions}
|
|
1341
|
-
value={[]}
|
|
1342
|
-
onChange={() => {}}
|
|
1343
|
-
hasSearch
|
|
1344
|
-
isLoading
|
|
1345
|
-
emptySearchText="Nothing like that here"
|
|
1346
|
-
/>,
|
|
1347
|
-
);
|
|
1348
|
-
|
|
1349
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1350
|
-
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
1351
|
-
|
|
1352
|
-
// useAnnounce writes on the next animation frame, so an immediate read
|
|
1353
|
-
// would pass whether or not anything was announced.
|
|
1354
|
-
await act(
|
|
1355
|
-
async () =>
|
|
1356
|
-
void (await new Promise(resolve => requestAnimationFrame(resolve))),
|
|
1357
|
-
);
|
|
1358
|
-
|
|
1359
|
-
// The panel is deliberately blank while loading; the live region is the
|
|
1360
|
-
// only channel left, so a result there would be a claim the screen
|
|
1361
|
-
// refuses to make.
|
|
1362
|
-
const listbox = screen.getByRole('listbox', h);
|
|
1363
|
-
expect(
|
|
1364
|
-
within(listbox).queryByText('Nothing like that here'),
|
|
1365
|
-
).not.toBeInTheDocument();
|
|
1366
|
-
// Only the busy start is spoken (spec:AST-086 FR8), never an empty or
|
|
1367
|
-
// no-results message the panel is suppressing.
|
|
1368
|
-
// (The channel clears its text on a timer, so it may already be empty.)
|
|
1369
|
-
expect(['', 'Loading']).toContain(politeRegion()?.textContent ?? '');
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
it('renders emptyText when there are no options and no search input', async () => {
|
|
1373
|
-
const user = userEvent.setup();
|
|
1374
|
-
render(
|
|
1375
|
-
<MultiSelector
|
|
1376
|
-
label="Fruit"
|
|
1377
|
-
options={[]}
|
|
1378
|
-
value={[]}
|
|
1379
|
-
onChange={() => {}}
|
|
1380
|
-
emptyText="Add a fruit first"
|
|
1381
|
-
/>,
|
|
1382
|
-
);
|
|
1383
|
-
|
|
1384
|
-
// Without hasSearch the trigger itself is the combobox.
|
|
1385
|
-
await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
|
|
1386
|
-
|
|
1387
|
-
const listbox = screen.getByRole('listbox', h);
|
|
1388
|
-
expect(within(listbox).getByText('Add a fruit first')).toBeInTheDocument();
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
describe('result announcements', () => {
|
|
1392
|
-
it('announces the match count politely while searching', async () => {
|
|
1393
|
-
const user = userEvent.setup();
|
|
1394
|
-
render(
|
|
1395
|
-
<InternationalizationProvider
|
|
1396
|
-
locale="fr"
|
|
1397
|
-
overrides={{
|
|
1398
|
-
fr: {
|
|
1399
|
-
'@astryx.multiSelector.resultCount':
|
|
1400
|
-
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
1401
|
-
},
|
|
1402
|
-
}}>
|
|
1403
|
-
<MultiSelector
|
|
1404
|
-
label="Fruit"
|
|
1405
|
-
options={defaultOptions}
|
|
1406
|
-
value={EMPTY_VALUE}
|
|
1407
|
-
onChange={() => {}}
|
|
1408
|
-
hasSearch
|
|
1409
|
-
/>
|
|
1410
|
-
</InternationalizationProvider>,
|
|
1411
|
-
);
|
|
1412
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1413
|
-
// "an" matches Banana and Orange.
|
|
1414
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'an');
|
|
1415
|
-
await waitFor(() => {
|
|
1416
|
-
// The plural branch of a message no catalog supplies.
|
|
1417
|
-
expect(politeRegion()?.textContent).toBe('2 résultats');
|
|
1418
|
-
});
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
it('announces the singular form when one option matches', async () => {
|
|
1422
|
-
const user = userEvent.setup();
|
|
1423
|
-
render(
|
|
1424
|
-
<InternationalizationProvider
|
|
1425
|
-
locale="fr"
|
|
1426
|
-
overrides={{
|
|
1427
|
-
fr: {
|
|
1428
|
-
'@astryx.multiSelector.resultCount':
|
|
1429
|
-
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
1430
|
-
},
|
|
1431
|
-
}}>
|
|
1432
|
-
<MultiSelector
|
|
1433
|
-
label="Fruit"
|
|
1434
|
-
options={defaultOptions}
|
|
1435
|
-
value={EMPTY_VALUE}
|
|
1436
|
-
onChange={() => {}}
|
|
1437
|
-
hasSearch
|
|
1438
|
-
/>
|
|
1439
|
-
</InternationalizationProvider>,
|
|
1440
|
-
);
|
|
1441
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1442
|
-
// "app" matches only Apple. Exact match, so "1 résultats" would fail.
|
|
1443
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'app');
|
|
1444
|
-
await waitFor(() => {
|
|
1445
|
-
expect(politeRegion()?.textContent).toBe('1 résultat');
|
|
1446
|
-
});
|
|
1447
|
-
});
|
|
1448
|
-
|
|
1449
|
-
it('announces the empty-results message when nothing matches', async () => {
|
|
1450
|
-
const user = userEvent.setup();
|
|
1451
|
-
render(
|
|
1452
|
-
<InternationalizationProvider
|
|
1453
|
-
locale="fr"
|
|
1454
|
-
overrides={{
|
|
1455
|
-
fr: {'@astryx.multiSelector.emptySearchResults': 'Aucun résultat'},
|
|
1456
|
-
}}>
|
|
1457
|
-
<MultiSelector
|
|
1458
|
-
label="Fruit"
|
|
1459
|
-
options={defaultOptions}
|
|
1460
|
-
value={EMPTY_VALUE}
|
|
1461
|
-
onChange={() => {}}
|
|
1462
|
-
hasSearch
|
|
1463
|
-
/>
|
|
1464
|
-
</InternationalizationProvider>,
|
|
1465
|
-
);
|
|
1466
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1467
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
|
|
1468
|
-
await waitFor(() => {
|
|
1469
|
-
expect(politeRegion()?.textContent).toBe('Aucun résultat');
|
|
1470
|
-
});
|
|
1471
|
-
});
|
|
1472
|
-
|
|
1473
|
-
it('speaks the result count from a provider catalog (plural)', async () => {
|
|
1474
|
-
const user = userEvent.setup();
|
|
1475
|
-
render(
|
|
1476
|
-
<InternationalizationProvider
|
|
1477
|
-
locale="fr"
|
|
1478
|
-
messages={{
|
|
1479
|
-
fr: {
|
|
1480
|
-
'@astryx.multiSelector.resultCount': {
|
|
1481
|
-
defaultMessage:
|
|
1482
|
-
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
1483
|
-
},
|
|
1484
|
-
},
|
|
1485
|
-
}}>
|
|
1486
|
-
<MultiSelector
|
|
1487
|
-
label="Fruit"
|
|
1488
|
-
options={defaultOptions}
|
|
1489
|
-
value={EMPTY_VALUE}
|
|
1490
|
-
onChange={() => {}}
|
|
1491
|
-
hasSearch
|
|
1492
|
-
/>
|
|
1493
|
-
</InternationalizationProvider>,
|
|
1494
|
-
);
|
|
1495
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1496
|
-
// "an" matches Banana and Orange.
|
|
1497
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'an');
|
|
1498
|
-
await waitFor(() => {
|
|
1499
|
-
// Same key through the catalog path rather than `overrides`.
|
|
1500
|
-
expect(politeRegion()?.textContent).toBe('2 résultats');
|
|
1501
|
-
});
|
|
1502
|
-
});
|
|
1503
|
-
|
|
1504
|
-
it('does not announce results until the user searches', async () => {
|
|
1505
|
-
const user = userEvent.setup();
|
|
1506
|
-
render(
|
|
1507
|
-
<MultiSelector
|
|
1508
|
-
label="Fruit"
|
|
1509
|
-
options={defaultOptions}
|
|
1510
|
-
value={EMPTY_VALUE}
|
|
1511
|
-
onChange={() => {}}
|
|
1512
|
-
hasSearch
|
|
1513
|
-
/>,
|
|
1514
|
-
);
|
|
1515
|
-
// Popover closed: nothing announced.
|
|
1516
|
-
expect(politeRegion()?.textContent ?? '').toBe('');
|
|
1517
|
-
// Open with an empty query: still nothing announced.
|
|
1518
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1519
|
-
expect(politeRegion()?.textContent ?? '').toBe('');
|
|
1520
|
-
});
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
it('renders with description', () => {
|
|
1524
|
-
render(
|
|
1525
|
-
<MultiSelector
|
|
1526
|
-
label="Fruit"
|
|
1527
|
-
description="Choose your fruits"
|
|
1528
|
-
options={defaultOptions}
|
|
1529
|
-
value={[]}
|
|
1530
|
-
onChange={() => {}}
|
|
1531
|
-
/>,
|
|
1532
|
-
);
|
|
1533
|
-
expect(screen.getByText('Choose your fruits')).toBeInTheDocument();
|
|
1534
|
-
});
|
|
1535
|
-
|
|
1536
|
-
it('supports data-testid', () => {
|
|
1537
|
-
render(
|
|
1538
|
-
<MultiSelector
|
|
1539
|
-
label="Fruit"
|
|
1540
|
-
options={defaultOptions}
|
|
1541
|
-
value={[]}
|
|
1542
|
-
onChange={() => {}}
|
|
1543
|
-
data-testid="fruit-selector"
|
|
1544
|
-
/>,
|
|
1545
|
-
);
|
|
1546
|
-
expect(screen.getByTestId('fruit-selector')).toBeInTheDocument();
|
|
1547
|
-
});
|
|
1548
|
-
|
|
1549
|
-
it('renders sections with dividers', async () => {
|
|
1550
|
-
const user = userEvent.setup();
|
|
1551
|
-
render(
|
|
1552
|
-
<MultiSelector
|
|
1553
|
-
label="Fruit"
|
|
1554
|
-
options={[
|
|
1555
|
-
{value: 'apple', label: 'Apple'},
|
|
1556
|
-
{
|
|
1557
|
-
type: 'section',
|
|
1558
|
-
title: 'Citrus',
|
|
1559
|
-
options: [
|
|
1560
|
-
{value: 'orange', label: 'Orange'},
|
|
1561
|
-
{value: 'lemon', label: 'Lemon'},
|
|
1562
|
-
],
|
|
1563
|
-
},
|
|
1564
|
-
]}
|
|
1565
|
-
value={[]}
|
|
1566
|
-
onChange={() => {}}
|
|
1567
|
-
/>,
|
|
1568
|
-
);
|
|
1569
|
-
|
|
1570
|
-
await user.click(screen.getByRole('combobox'));
|
|
1571
|
-
const options = screen.getAllByRole('option', h);
|
|
1572
|
-
expect(options).toHaveLength(3);
|
|
1573
|
-
expect(screen.getByText('Citrus')).toBeInTheDocument();
|
|
1574
|
-
});
|
|
1575
|
-
|
|
1576
|
-
it('shows loading state with a spinner and aria-busy', () => {
|
|
1577
|
-
render(
|
|
1578
|
-
<MultiSelector
|
|
1579
|
-
label="Fruit"
|
|
1580
|
-
options={defaultOptions}
|
|
1581
|
-
value={[]}
|
|
1582
|
-
onChange={() => {}}
|
|
1583
|
-
isLoading
|
|
1584
|
-
/>,
|
|
1585
|
-
);
|
|
1586
|
-
expect(screen.getByRole('combobox')).toHaveAttribute('aria-busy', 'true');
|
|
1587
|
-
expectSilentBusySpinner();
|
|
1588
|
-
});
|
|
1589
|
-
|
|
1590
|
-
it('renders with custom selectAllLabel', async () => {
|
|
1591
|
-
const user = userEvent.setup();
|
|
1592
|
-
render(
|
|
1593
|
-
<MultiSelector
|
|
1594
|
-
label="Fruit"
|
|
1595
|
-
options={defaultOptions}
|
|
1596
|
-
value={[]}
|
|
1597
|
-
onChange={() => {}}
|
|
1598
|
-
hasSelectAll
|
|
1599
|
-
selectAllLabel="Check all"
|
|
1600
|
-
/>,
|
|
1601
|
-
);
|
|
1602
|
-
|
|
1603
|
-
await user.click(screen.getByRole('combobox'));
|
|
1604
|
-
expect(screen.getByText('Check all')).toBeInTheDocument();
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
it('sorts selected items to top', async () => {
|
|
1608
|
-
const user = userEvent.setup();
|
|
1609
|
-
render(
|
|
1610
|
-
<MultiSelector
|
|
1611
|
-
label="Fruit"
|
|
1612
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
1613
|
-
value={['Orange']}
|
|
1614
|
-
onChange={() => {}}
|
|
1615
|
-
/>,
|
|
1616
|
-
);
|
|
1617
|
-
|
|
1618
|
-
await user.click(screen.getByRole('combobox'));
|
|
1619
|
-
const options = screen.getAllByRole('option', h);
|
|
1620
|
-
// Orange is selected so it should appear first
|
|
1621
|
-
expect(options[0]).toHaveAttribute('aria-selected', 'true');
|
|
1622
|
-
expect(options[0]).toHaveTextContent('Orange');
|
|
1623
|
-
expect(options[1]).toHaveTextContent('Apple');
|
|
1624
|
-
expect(options[2]).toHaveTextContent('Banana');
|
|
1625
|
-
});
|
|
1626
|
-
|
|
1627
|
-
it('sorts selected items to top within sections', async () => {
|
|
1628
|
-
const user = userEvent.setup();
|
|
1629
|
-
render(
|
|
1630
|
-
<MultiSelector
|
|
1631
|
-
label="Fruit"
|
|
1632
|
-
options={[
|
|
1633
|
-
{
|
|
1634
|
-
type: 'section',
|
|
1635
|
-
title: 'Citrus',
|
|
1636
|
-
options: [
|
|
1637
|
-
{value: 'orange', label: 'Orange'},
|
|
1638
|
-
{value: 'lemon', label: 'Lemon'},
|
|
1639
|
-
{value: 'lime', label: 'Lime'},
|
|
1640
|
-
],
|
|
1641
|
-
},
|
|
1642
|
-
]}
|
|
1643
|
-
value={['lime']}
|
|
1644
|
-
onChange={() => {}}
|
|
1645
|
-
/>,
|
|
1646
|
-
);
|
|
1647
|
-
|
|
1648
|
-
await user.click(screen.getByRole('combobox'));
|
|
1649
|
-
const options = screen.getAllByRole('option', h);
|
|
1650
|
-
// Lime is selected so it should appear first within the section
|
|
1651
|
-
expect(options[0]).toHaveTextContent('Lime');
|
|
1652
|
-
expect(options[1]).toHaveTextContent('Orange');
|
|
1653
|
-
expect(options[2]).toHaveTextContent('Lemon');
|
|
1654
|
-
});
|
|
1655
|
-
|
|
1656
|
-
it('has displayName', () => {
|
|
1657
|
-
expect(MultiSelector.displayName).toBe('MultiSelector');
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
describe('keyboard accessibility', () => {
|
|
1661
|
-
it('trigger is focusable via Tab when enabled', async () => {
|
|
1662
|
-
const user = userEvent.setup();
|
|
1663
|
-
render(
|
|
1664
|
-
<MultiSelector
|
|
1665
|
-
label="Fruit"
|
|
1666
|
-
options={defaultOptions}
|
|
1667
|
-
value={[]}
|
|
1668
|
-
onChange={() => {}}
|
|
1669
|
-
/>,
|
|
1670
|
-
);
|
|
1671
|
-
await user.tab();
|
|
1672
|
-
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
1673
|
-
});
|
|
1674
|
-
|
|
1675
|
-
it('trigger keeps its tab stop when disabled (spec:AST-085 FR1)', () => {
|
|
1676
|
-
render(
|
|
1677
|
-
<MultiSelector
|
|
1678
|
-
label="Fruit"
|
|
1679
|
-
options={defaultOptions}
|
|
1680
|
-
value={[]}
|
|
1681
|
-
onChange={() => {}}
|
|
1682
|
-
isDisabled
|
|
1683
|
-
/>,
|
|
1684
|
-
);
|
|
1685
|
-
expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '0');
|
|
1686
|
-
});
|
|
1687
|
-
|
|
1688
|
-
it('opens the listbox with ArrowDown from a focused trigger', async () => {
|
|
1689
|
-
const user = userEvent.setup();
|
|
1690
|
-
render(
|
|
1691
|
-
<MultiSelector
|
|
1692
|
-
label="Fruit"
|
|
1693
|
-
options={defaultOptions}
|
|
1694
|
-
value={[]}
|
|
1695
|
-
onChange={() => {}}
|
|
1696
|
-
/>,
|
|
1697
|
-
);
|
|
1698
|
-
const trigger = screen.getByRole('combobox');
|
|
1699
|
-
await user.tab();
|
|
1700
|
-
expect(trigger).toHaveFocus();
|
|
1701
|
-
await user.keyboard('{ArrowDown}');
|
|
1702
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
it('clear button is reachable by keyboard', () => {
|
|
1706
|
-
render(
|
|
1707
|
-
<MultiSelector
|
|
1708
|
-
label="Fruit"
|
|
1709
|
-
options={defaultOptions}
|
|
1710
|
-
value={['Apple', 'Banana']}
|
|
1711
|
-
onChange={() => {}}
|
|
1712
|
-
hasClear
|
|
1713
|
-
/>,
|
|
1714
|
-
);
|
|
1715
|
-
const clear = screen.getByRole('button', {name: 'Clear all Fruit'});
|
|
1716
|
-
expect(clear).not.toHaveAttribute('tabIndex', '-1');
|
|
1717
|
-
});
|
|
1718
|
-
|
|
1719
|
-
it('returns focus to the trigger when the clear button is activated by keyboard', async () => {
|
|
1720
|
-
function Controlled() {
|
|
1721
|
-
const [value, setValue] = useState(['Apple', 'Banana']);
|
|
1722
|
-
return (
|
|
1723
|
-
<MultiSelector
|
|
1724
|
-
label="Fruit"
|
|
1725
|
-
options={defaultOptions}
|
|
1726
|
-
value={value}
|
|
1727
|
-
onChange={setValue}
|
|
1728
|
-
hasClear
|
|
1729
|
-
/>
|
|
1730
|
-
);
|
|
1731
|
-
}
|
|
1732
|
-
const user = userEvent.setup();
|
|
1733
|
-
render(<Controlled />);
|
|
1734
|
-
screen.getByRole('button', {name: 'Clear all Fruit'}).focus();
|
|
1735
|
-
await user.keyboard('{Enter}');
|
|
1736
|
-
expect(
|
|
1737
|
-
screen.queryByRole('button', {name: 'Clear all Fruit'}),
|
|
1738
|
-
).not.toBeInTheDocument();
|
|
1739
|
-
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
1740
|
-
});
|
|
1741
|
-
|
|
1742
|
-
it('leaves focus in the search input when the clear button is clicked with the list open', async () => {
|
|
1743
|
-
function Controlled() {
|
|
1744
|
-
const [value, setValue] = useState(['Apple', 'Banana']);
|
|
1745
|
-
return (
|
|
1746
|
-
<MultiSelector
|
|
1747
|
-
label="Fruit"
|
|
1748
|
-
options={defaultOptions}
|
|
1749
|
-
value={value}
|
|
1750
|
-
onChange={setValue}
|
|
1751
|
-
hasClear
|
|
1752
|
-
hasSearch
|
|
1753
|
-
/>
|
|
1754
|
-
);
|
|
1755
|
-
}
|
|
1756
|
-
const user = userEvent.setup();
|
|
1757
|
-
const {container} = render(<Controlled />);
|
|
1758
|
-
await user.click(container.querySelector('[aria-haspopup]') as Element);
|
|
1759
|
-
const search = screen.getByRole('combobox', h);
|
|
1760
|
-
search.focus();
|
|
1761
|
-
await user.click(screen.getByRole('button', {name: 'Clear all Fruit'}));
|
|
1762
|
-
expect(
|
|
1763
|
-
screen.queryByRole('button', {name: 'Clear all Fruit'}),
|
|
1764
|
-
).not.toBeInTheDocument();
|
|
1765
|
-
expect(search).toHaveFocus();
|
|
1766
|
-
});
|
|
1767
|
-
|
|
1768
|
-
it('scrolls the highlighted option into view during arrow navigation', async () => {
|
|
1769
|
-
const scrollIntoView = vi.fn();
|
|
1770
|
-
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
1771
|
-
configurable: true,
|
|
1772
|
-
value: scrollIntoView,
|
|
1773
|
-
});
|
|
1774
|
-
try {
|
|
1775
|
-
const user = userEvent.setup();
|
|
1776
|
-
const longOptions = Array.from(
|
|
1777
|
-
{length: 20},
|
|
1778
|
-
(_, i) => `Option ${i + 1}`,
|
|
1779
|
-
);
|
|
1780
|
-
render(
|
|
1781
|
-
<MultiSelector
|
|
1782
|
-
label="Fruit"
|
|
1783
|
-
options={longOptions}
|
|
1784
|
-
value={[]}
|
|
1785
|
-
onChange={() => {}}
|
|
1786
|
-
/>,
|
|
1787
|
-
);
|
|
1788
|
-
|
|
1789
|
-
const trigger = screen.getByRole('combobox');
|
|
1790
|
-
await user.click(trigger);
|
|
1791
|
-
scrollIntoView.mockClear();
|
|
1792
|
-
await user.keyboard('{ArrowDown}');
|
|
1793
|
-
await user.keyboard('{ArrowDown}');
|
|
1794
|
-
|
|
1795
|
-
expect(scrollIntoView).toHaveBeenCalledWith({block: 'nearest'});
|
|
1796
|
-
} finally {
|
|
1797
|
-
delete (HTMLElement.prototype as unknown as {scrollIntoView?: unknown})
|
|
1798
|
-
.scrollIntoView;
|
|
1799
|
-
}
|
|
1800
|
-
});
|
|
1801
|
-
|
|
1802
|
-
it('highlights on hover without scrolling, keyboard still scrolls (#6077)', async () => {
|
|
1803
|
-
// Hover must highlight only: scrollIntoView under a stationary pointer
|
|
1804
|
-
// moves the next option under it, re-highlighting and scrolling again —
|
|
1805
|
-
// a runaway auto-scroll loop with no user input.
|
|
1806
|
-
const scrollIntoView = vi.fn();
|
|
1807
|
-
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
1808
|
-
configurable: true,
|
|
1809
|
-
value: scrollIntoView,
|
|
1810
|
-
});
|
|
1811
|
-
try {
|
|
1812
|
-
const user = userEvent.setup();
|
|
1813
|
-
const longOptions = Array.from(
|
|
1814
|
-
{length: 20},
|
|
1815
|
-
(_, i) => `Option ${i + 1}`,
|
|
1816
|
-
);
|
|
1817
|
-
render(
|
|
1818
|
-
<MultiSelector
|
|
1819
|
-
label="Fruit"
|
|
1820
|
-
options={longOptions}
|
|
1821
|
-
value={[]}
|
|
1822
|
-
onChange={() => {}}
|
|
1823
|
-
/>,
|
|
1824
|
-
);
|
|
1825
|
-
|
|
1826
|
-
const trigger = screen.getByRole('combobox');
|
|
1827
|
-
await user.click(trigger);
|
|
1828
|
-
scrollIntoView.mockClear();
|
|
1829
|
-
|
|
1830
|
-
const options = screen.getAllByRole('option', {hidden: true});
|
|
1831
|
-
fireEvent.mouseEnter(options[3]);
|
|
1832
|
-
|
|
1833
|
-
expect(trigger.getAttribute('aria-activedescendant')).toBe(
|
|
1834
|
-
options[3].id,
|
|
1835
|
-
);
|
|
1836
|
-
expect(scrollIntoView).not.toHaveBeenCalled();
|
|
1837
|
-
|
|
1838
|
-
await user.keyboard('{ArrowDown}');
|
|
1839
|
-
expect(scrollIntoView).toHaveBeenCalledWith({block: 'nearest'});
|
|
1840
|
-
} finally {
|
|
1841
|
-
delete (HTMLElement.prototype as unknown as {scrollIntoView?: unknown})
|
|
1842
|
-
.scrollIntoView;
|
|
1843
|
-
}
|
|
1844
|
-
});
|
|
1845
|
-
|
|
1846
|
-
it('clears all values via Delete on the focused trigger', async () => {
|
|
1847
|
-
const user = userEvent.setup();
|
|
1848
|
-
const onChange = vi.fn();
|
|
1849
|
-
render(
|
|
1850
|
-
<MultiSelector
|
|
1851
|
-
label="Fruit"
|
|
1852
|
-
options={defaultOptions}
|
|
1853
|
-
value={['Apple', 'Banana']}
|
|
1854
|
-
onChange={onChange}
|
|
1855
|
-
hasClear
|
|
1856
|
-
/>,
|
|
1857
|
-
);
|
|
1858
|
-
const trigger = screen.getByRole('combobox');
|
|
1859
|
-
trigger.focus();
|
|
1860
|
-
await user.keyboard('{Delete}');
|
|
1861
|
-
expect(onChange).toHaveBeenCalledWith([]);
|
|
1862
|
-
});
|
|
1863
|
-
|
|
1864
|
-
it('clears all values via Backspace on the focused trigger', async () => {
|
|
1865
|
-
const user = userEvent.setup();
|
|
1866
|
-
const onChange = vi.fn();
|
|
1867
|
-
render(
|
|
1868
|
-
<MultiSelector
|
|
1869
|
-
label="Fruit"
|
|
1870
|
-
options={defaultOptions}
|
|
1871
|
-
value={['Apple', 'Banana']}
|
|
1872
|
-
onChange={onChange}
|
|
1873
|
-
hasClear
|
|
1874
|
-
/>,
|
|
1875
|
-
);
|
|
1876
|
-
const trigger = screen.getByRole('combobox');
|
|
1877
|
-
trigger.focus();
|
|
1878
|
-
await user.keyboard('{Backspace}');
|
|
1879
|
-
expect(onChange).toHaveBeenCalledWith([]);
|
|
1880
|
-
});
|
|
1881
|
-
|
|
1882
|
-
it('does not clear via Delete when nothing is selected', async () => {
|
|
1883
|
-
const user = userEvent.setup();
|
|
1884
|
-
const onChange = vi.fn();
|
|
1885
|
-
render(
|
|
1886
|
-
<MultiSelector
|
|
1887
|
-
label="Fruit"
|
|
1888
|
-
options={defaultOptions}
|
|
1889
|
-
value={[]}
|
|
1890
|
-
onChange={onChange}
|
|
1891
|
-
hasClear
|
|
1892
|
-
/>,
|
|
1893
|
-
);
|
|
1894
|
-
const trigger = screen.getByRole('combobox');
|
|
1895
|
-
trigger.focus();
|
|
1896
|
-
await user.keyboard('{Delete}');
|
|
1897
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1898
|
-
});
|
|
1899
|
-
});
|
|
1900
|
-
|
|
1901
|
-
describe('announcements', () => {
|
|
1902
|
-
it('announces the selection count politely when toggling an option', async () => {
|
|
1903
|
-
const user = userEvent.setup();
|
|
1904
|
-
render(
|
|
1905
|
-
<InternationalizationProvider
|
|
1906
|
-
locale="fr"
|
|
1907
|
-
overrides={{
|
|
1908
|
-
fr: {
|
|
1909
|
-
'@astryx.multiSelector.selectionCount':
|
|
1910
|
-
'{count, number} sur {total, number} sélectionnés',
|
|
1911
|
-
},
|
|
1912
|
-
}}>
|
|
1913
|
-
<MultiSelector
|
|
1914
|
-
label="Fruit"
|
|
1915
|
-
options={[...ANNOUNCE_OPTIONS]}
|
|
1916
|
-
value={EMPTY_VALUE}
|
|
1917
|
-
onChange={() => {}}
|
|
1918
|
-
/>
|
|
1919
|
-
</InternationalizationProvider>,
|
|
1920
|
-
);
|
|
1921
|
-
await user.click(screen.getByRole('combobox'));
|
|
1922
|
-
const options = screen.getAllByRole('option', {hidden: true});
|
|
1923
|
-
await user.click(options[0]);
|
|
1924
|
-
await waitFor(() => {
|
|
1925
|
-
// Both arguments land, in the order the message asks for them.
|
|
1926
|
-
expect(politeRegion()?.textContent).toBe('1 sur 3 sélectionnés');
|
|
1927
|
-
});
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
it('announces the all-selected message when select-all selects everything', async () => {
|
|
1931
|
-
const user = userEvent.setup();
|
|
1932
|
-
render(
|
|
1933
|
-
<InternationalizationProvider
|
|
1934
|
-
locale="fr"
|
|
1935
|
-
overrides={{
|
|
1936
|
-
fr: {'@astryx.multiSelector.allSelected': 'Tout est là'},
|
|
1937
|
-
}}>
|
|
1938
|
-
<MultiSelector
|
|
1939
|
-
label="Fruit"
|
|
1940
|
-
options={[...ANNOUNCE_OPTIONS]}
|
|
1941
|
-
value={EMPTY_VALUE}
|
|
1942
|
-
onChange={() => {}}
|
|
1943
|
-
hasSelectAll
|
|
1944
|
-
/>
|
|
1945
|
-
</InternationalizationProvider>,
|
|
1946
|
-
);
|
|
1947
|
-
await user.click(screen.getByRole('combobox'));
|
|
1948
|
-
await user.click(screen.getByText('Select all'));
|
|
1949
|
-
await waitFor(() => {
|
|
1950
|
-
expect(politeRegion()?.textContent).toBe('Tout est là');
|
|
1951
|
-
});
|
|
1952
|
-
});
|
|
1953
|
-
|
|
1954
|
-
it('announces the selection-cleared message when clearing', async () => {
|
|
1955
|
-
const user = userEvent.setup();
|
|
1956
|
-
render(
|
|
1957
|
-
<InternationalizationProvider
|
|
1958
|
-
locale="fr"
|
|
1959
|
-
overrides={{
|
|
1960
|
-
fr: {'@astryx.multiSelector.selectionCleared': 'Plus rien'},
|
|
1961
|
-
}}>
|
|
1962
|
-
<MultiSelector
|
|
1963
|
-
label="Fruit"
|
|
1964
|
-
options={[...ANNOUNCE_OPTIONS]}
|
|
1965
|
-
value={['Apple', 'Banana']}
|
|
1966
|
-
onChange={() => {}}
|
|
1967
|
-
hasClear
|
|
1968
|
-
/>
|
|
1969
|
-
</InternationalizationProvider>,
|
|
1970
|
-
);
|
|
1971
|
-
await user.click(screen.getByRole('button', {name: 'Clear all Fruit'}));
|
|
1972
|
-
await waitFor(() => {
|
|
1973
|
-
expect(politeRegion()?.textContent).toBe('Plus rien');
|
|
1974
|
-
});
|
|
1975
|
-
});
|
|
1976
|
-
|
|
1977
|
-
it('prefers a provider override over the provider catalog', async () => {
|
|
1978
|
-
const user = userEvent.setup();
|
|
1979
|
-
render(
|
|
1980
|
-
<InternationalizationProvider
|
|
1981
|
-
locale="fr"
|
|
1982
|
-
messages={{
|
|
1983
|
-
fr: {
|
|
1984
|
-
'@astryx.multiSelector.selectionCount': {
|
|
1985
|
-
defaultMessage: '{count, number} du catalogue',
|
|
1986
|
-
},
|
|
1987
|
-
},
|
|
1988
|
-
}}
|
|
1989
|
-
overrides={{
|
|
1990
|
-
fr: {
|
|
1991
|
-
'@astryx.multiSelector.selectionCount':
|
|
1992
|
-
'{count, number} remplacé',
|
|
1993
|
-
},
|
|
1994
|
-
}}>
|
|
1995
|
-
<MultiSelector
|
|
1996
|
-
label="Fruit"
|
|
1997
|
-
options={[...ANNOUNCE_OPTIONS]}
|
|
1998
|
-
value={EMPTY_VALUE}
|
|
1999
|
-
onChange={() => {}}
|
|
2000
|
-
/>
|
|
2001
|
-
</InternationalizationProvider>,
|
|
2002
|
-
);
|
|
2003
|
-
await user.click(screen.getByRole('combobox'));
|
|
2004
|
-
const options = screen.getAllByRole('option', {hidden: true});
|
|
2005
|
-
await user.click(options[0]);
|
|
2006
|
-
await waitFor(() => {
|
|
2007
|
-
expect(politeRegion()?.textContent).toBe('1 remplacé');
|
|
2008
|
-
});
|
|
2009
|
-
});
|
|
2010
|
-
});
|
|
2011
|
-
|
|
2012
|
-
describe('disabledMessage', () => {
|
|
2013
|
-
it('shows the reason tooltip on hover when disabled with a reason', async () => {
|
|
2014
|
-
render(
|
|
2015
|
-
<MultiSelector
|
|
2016
|
-
label="Fruit"
|
|
2017
|
-
options={defaultOptions}
|
|
2018
|
-
value={[]}
|
|
2019
|
-
onChange={() => {}}
|
|
2020
|
-
isDisabled
|
|
2021
|
-
disabledMessage="Select a table first"
|
|
2022
|
-
data-testid="fruit-multi-selector"
|
|
2023
|
-
/>,
|
|
2024
|
-
);
|
|
2025
|
-
|
|
2026
|
-
// data-testid lands on the trigger; the reason tooltip anchors to the
|
|
2027
|
-
// control box around it.
|
|
2028
|
-
const container = screen
|
|
2029
|
-
.getByTestId('fruit-multi-selector')
|
|
2030
|
-
.closest('.astryx-multi-selector') as HTMLElement;
|
|
2031
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
2032
|
-
expect(tooltip).toHaveTextContent('Select a table first');
|
|
2033
|
-
|
|
2034
|
-
fireEvent.mouseEnter(container);
|
|
2035
|
-
await waitFor(() => {
|
|
2036
|
-
expect(tooltip).toHaveAttribute('popover-open');
|
|
2037
|
-
});
|
|
2038
|
-
|
|
2039
|
-
fireEvent.mouseLeave(container);
|
|
2040
|
-
await waitFor(() => {
|
|
2041
|
-
expect(tooltip).not.toHaveAttribute('popover-open');
|
|
2042
|
-
});
|
|
2043
|
-
});
|
|
2044
|
-
|
|
2045
|
-
it('shows the reason tooltip on keyboard focus', async () => {
|
|
2046
|
-
const user = userEvent.setup();
|
|
2047
|
-
render(
|
|
2048
|
-
<MultiSelector
|
|
2049
|
-
label="Fruit"
|
|
2050
|
-
options={defaultOptions}
|
|
2051
|
-
value={[]}
|
|
2052
|
-
onChange={() => {}}
|
|
2053
|
-
isDisabled
|
|
2054
|
-
disabledMessage="Select a table first"
|
|
2055
|
-
/>,
|
|
2056
|
-
);
|
|
2057
|
-
|
|
2058
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
2059
|
-
await user.tab();
|
|
2060
|
-
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
2061
|
-
await waitFor(() => {
|
|
2062
|
-
expect(tooltip).toHaveAttribute('popover-open');
|
|
2063
|
-
});
|
|
2064
|
-
});
|
|
2065
|
-
|
|
2066
|
-
it('does not render a tooltip when not disabled', () => {
|
|
2067
|
-
render(
|
|
2068
|
-
<MultiSelector
|
|
2069
|
-
label="Fruit"
|
|
2070
|
-
options={defaultOptions}
|
|
2071
|
-
value={[]}
|
|
2072
|
-
onChange={() => {}}
|
|
2073
|
-
disabledMessage="Select a table first"
|
|
2074
|
-
/>,
|
|
2075
|
-
);
|
|
2076
|
-
expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
|
|
2077
|
-
});
|
|
2078
|
-
|
|
2079
|
-
it('does not render a tooltip when disabled without a reason', () => {
|
|
2080
|
-
render(
|
|
2081
|
-
<MultiSelector
|
|
2082
|
-
label="Fruit"
|
|
2083
|
-
options={defaultOptions}
|
|
2084
|
-
value={[]}
|
|
2085
|
-
onChange={() => {}}
|
|
2086
|
-
isDisabled
|
|
2087
|
-
/>,
|
|
2088
|
-
);
|
|
2089
|
-
expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
it('keeps the trigger focusable via aria-disabled when a reason is provided', () => {
|
|
2093
|
-
render(
|
|
2094
|
-
<MultiSelector
|
|
2095
|
-
label="Fruit"
|
|
2096
|
-
options={defaultOptions}
|
|
2097
|
-
value={[]}
|
|
2098
|
-
onChange={() => {}}
|
|
2099
|
-
isDisabled
|
|
2100
|
-
disabledMessage="Select a table first"
|
|
2101
|
-
/>,
|
|
2102
|
-
);
|
|
2103
|
-
const trigger = screen.getByRole('combobox');
|
|
2104
|
-
expect(trigger).not.toBeDisabled();
|
|
2105
|
-
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
2106
|
-
expect(trigger).toHaveAttribute('tabIndex', '0');
|
|
2107
|
-
});
|
|
2108
|
-
|
|
2109
|
-
it('links the reason tooltip from the trigger via aria-describedby', () => {
|
|
2110
|
-
render(
|
|
2111
|
-
<MultiSelector
|
|
2112
|
-
label="Fruit"
|
|
2113
|
-
options={defaultOptions}
|
|
2114
|
-
value={[]}
|
|
2115
|
-
onChange={() => {}}
|
|
2116
|
-
isDisabled
|
|
2117
|
-
disabledMessage="Select a table first"
|
|
2118
|
-
/>,
|
|
2119
|
-
);
|
|
2120
|
-
const trigger = screen.getByRole('combobox');
|
|
2121
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
2122
|
-
expect(trigger.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
2123
|
-
});
|
|
2124
|
-
|
|
2125
|
-
it('blocks activation while focusable-disabled', async () => {
|
|
2126
|
-
const user = userEvent.setup();
|
|
2127
|
-
const onChange = vi.fn();
|
|
2128
|
-
render(
|
|
2129
|
-
<MultiSelector
|
|
2130
|
-
label="Fruit"
|
|
2131
|
-
options={defaultOptions}
|
|
2132
|
-
value={[]}
|
|
2133
|
-
onChange={onChange}
|
|
2134
|
-
isDisabled
|
|
2135
|
-
disabledMessage="Select a table first"
|
|
2136
|
-
/>,
|
|
2137
|
-
);
|
|
2138
|
-
|
|
2139
|
-
const trigger = screen.getByRole('combobox');
|
|
2140
|
-
await user.click(trigger);
|
|
2141
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2142
|
-
|
|
2143
|
-
await user.keyboard('{Enter}');
|
|
2144
|
-
await user.keyboard('{ArrowDown}');
|
|
2145
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2146
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
2147
|
-
});
|
|
2148
|
-
|
|
2149
|
-
it('stays focusable when disabled without a reason (spec:AST-085 FR1)', () => {
|
|
2150
|
-
render(
|
|
2151
|
-
<MultiSelector
|
|
2152
|
-
label="Fruit"
|
|
2153
|
-
options={defaultOptions}
|
|
2154
|
-
value={[]}
|
|
2155
|
-
onChange={() => {}}
|
|
2156
|
-
isDisabled
|
|
2157
|
-
/>,
|
|
2158
|
-
);
|
|
2159
|
-
const trigger = screen.getByRole('combobox');
|
|
2160
|
-
expect(trigger).not.toHaveAttribute('disabled');
|
|
2161
|
-
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
2162
|
-
expect(trigger).toHaveAttribute('tabIndex', '0');
|
|
2163
|
-
});
|
|
2164
|
-
});
|
|
2165
|
-
describe('isReadOnly', () => {
|
|
2166
|
-
const readOnlyCases = [
|
|
2167
|
-
{presentation: 'popover', hasSearch: false},
|
|
2168
|
-
{presentation: 'popover', hasSearch: true},
|
|
2169
|
-
{presentation: 'bottom-sheet', hasSearch: false},
|
|
2170
|
-
{presentation: 'bottom-sheet', hasSearch: true},
|
|
2171
|
-
{presentation: 'adaptive', hasSearch: false},
|
|
2172
|
-
{presentation: 'adaptive', hasSearch: true},
|
|
2173
|
-
] as const;
|
|
2174
|
-
|
|
2175
|
-
const sheetTransitionCases = [
|
|
2176
|
-
{presentation: 'bottom-sheet', hasSearch: false},
|
|
2177
|
-
{presentation: 'bottom-sheet', hasSearch: true},
|
|
2178
|
-
{presentation: 'adaptive', hasSearch: false},
|
|
2179
|
-
{presentation: 'adaptive', hasSearch: true},
|
|
2180
|
-
] as const;
|
|
2181
|
-
|
|
2182
|
-
const disabledPrecedenceCases = (
|
|
2183
|
-
['popover', 'bottom-sheet', 'adaptive'] as const
|
|
2184
|
-
).flatMap(presentation =>
|
|
2185
|
-
[false, true].flatMap(hasSearch =>
|
|
2186
|
-
[false, true].map(hasDisabledMessage => ({
|
|
2187
|
-
presentation,
|
|
2188
|
-
hasSearch,
|
|
2189
|
-
hasDisabledMessage,
|
|
2190
|
-
})),
|
|
2191
|
-
),
|
|
2192
|
-
);
|
|
2193
|
-
|
|
2194
|
-
function stubCompactTouch(presentation: 'bottom-sheet' | 'adaptive') {
|
|
2195
|
-
if (presentation !== 'adaptive') {
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
vi.mocked(matchMedia).mockImplementation(
|
|
2199
|
-
(query: string) =>
|
|
2200
|
-
({
|
|
2201
|
-
matches: query === COMPACT_TOUCH_PRESENTATION_QUERY,
|
|
2202
|
-
media: query,
|
|
2203
|
-
onchange: null,
|
|
2204
|
-
addEventListener: vi.fn(),
|
|
2205
|
-
removeEventListener: vi.fn(),
|
|
2206
|
-
addListener: vi.fn(),
|
|
2207
|
-
removeListener: vi.fn(),
|
|
2208
|
-
dispatchEvent: vi.fn(),
|
|
2209
|
-
}) as MediaQueryList,
|
|
2210
|
-
);
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
it.each(readOnlyCases)(
|
|
2214
|
-
'uses a read-only combobox with no rendered popup ($presentation, search=$hasSearch)',
|
|
2215
|
-
async ({presentation, hasSearch}) => {
|
|
2216
|
-
const user = userEvent.setup();
|
|
2217
|
-
const {container} = render(
|
|
2218
|
-
<form>
|
|
2219
|
-
<MultiSelector
|
|
2220
|
-
label="Fruit"
|
|
2221
|
-
htmlName="fruit"
|
|
2222
|
-
options={defaultOptions}
|
|
2223
|
-
value={['Apple', 'Banana']}
|
|
2224
|
-
onChange={() => {}}
|
|
2225
|
-
hasClear
|
|
2226
|
-
hasSearch={hasSearch}
|
|
2227
|
-
presentation={presentation}
|
|
2228
|
-
isDefaultOpen
|
|
2229
|
-
triggerDisplay="labels"
|
|
2230
|
-
isReadOnly
|
|
2231
|
-
/>
|
|
2232
|
-
</form>,
|
|
2233
|
-
);
|
|
2234
|
-
|
|
2235
|
-
const trigger = screen.getByRole('combobox', {name: 'Fruit'});
|
|
2236
|
-
expect(trigger.tagName).toBe('BUTTON');
|
|
2237
|
-
expect(trigger).toHaveTextContent('Apple, Banana');
|
|
2238
|
-
expect(trigger).toHaveAttribute('aria-readonly', 'true');
|
|
2239
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2240
|
-
const readOnlyDescription = screen.getByText('Read only');
|
|
2241
|
-
expect(trigger.getAttribute('aria-describedby')).toContain(
|
|
2242
|
-
readOnlyDescription.id,
|
|
2243
|
-
);
|
|
2244
|
-
expect(trigger).not.toHaveAttribute('aria-controls');
|
|
2245
|
-
expect(trigger).not.toHaveAttribute('aria-haspopup');
|
|
2246
|
-
expect(screen.getAllByRole('combobox', h)).toHaveLength(1);
|
|
2247
|
-
expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
|
|
2248
|
-
expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
|
|
2249
|
-
expect(
|
|
2250
|
-
screen.queryByRole('button', {name: 'Clear all Fruit'}),
|
|
2251
|
-
).not.toBeInTheDocument();
|
|
2252
|
-
expect(
|
|
2253
|
-
container.querySelector('.astryx-multi-selector-indicator-icon'),
|
|
2254
|
-
).toBeNull();
|
|
2255
|
-
expect(
|
|
2256
|
-
container.querySelector('.astryx-multi-selector'),
|
|
2257
|
-
).toHaveAttribute('data-readonly', 'readonly');
|
|
2258
|
-
expect(
|
|
2259
|
-
new FormData(container.querySelector('form')!).getAll('fruit'),
|
|
2260
|
-
).toEqual(['Apple', 'Banana']);
|
|
2261
|
-
|
|
2262
|
-
await user.tab();
|
|
2263
|
-
expect(trigger).toHaveFocus();
|
|
2264
|
-
},
|
|
2265
|
-
);
|
|
2266
|
-
|
|
2267
|
-
it('keeps loading feedback while blocking the selection surface', async () => {
|
|
2268
|
-
const user = userEvent.setup();
|
|
2269
|
-
render(
|
|
2270
|
-
<MultiSelector
|
|
2271
|
-
label="Fruit"
|
|
2272
|
-
options={defaultOptions}
|
|
2273
|
-
value={['Apple']}
|
|
2274
|
-
onChange={() => {}}
|
|
2275
|
-
isLoading
|
|
2276
|
-
isReadOnly
|
|
2277
|
-
/>,
|
|
2278
|
-
);
|
|
2279
|
-
|
|
2280
|
-
const trigger = screen.getByRole('combobox', {name: 'Fruit'});
|
|
2281
|
-
expect(trigger).toHaveAttribute('aria-busy', 'true');
|
|
2282
|
-
expectSilentBusySpinner();
|
|
2283
|
-
|
|
2284
|
-
await user.click(trigger);
|
|
2285
|
-
expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
|
|
2286
|
-
});
|
|
2287
|
-
|
|
2288
|
-
it('blocks pointer, keyboard, and typeahead changes', async () => {
|
|
2289
|
-
const user = userEvent.setup();
|
|
2290
|
-
const onChange = vi.fn();
|
|
2291
|
-
const changeAction = vi.fn();
|
|
2292
|
-
render(
|
|
2293
|
-
<MultiSelector
|
|
2294
|
-
label="Fruit"
|
|
2295
|
-
options={defaultOptions}
|
|
2296
|
-
value={['Apple']}
|
|
2297
|
-
onChange={onChange}
|
|
2298
|
-
changeAction={changeAction}
|
|
2299
|
-
isReadOnly
|
|
2300
|
-
/>,
|
|
2301
|
-
);
|
|
2302
|
-
|
|
2303
|
-
const trigger = screen.getByRole('combobox');
|
|
2304
|
-
await user.click(trigger);
|
|
2305
|
-
trigger.focus();
|
|
2306
|
-
await user.keyboard('{Enter}{ArrowDown}b{Backspace}');
|
|
2307
|
-
|
|
2308
|
-
expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
|
|
2309
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
2310
|
-
expect(changeAction).not.toHaveBeenCalled();
|
|
2311
|
-
});
|
|
2312
|
-
|
|
2313
|
-
it.each([false, true])(
|
|
2314
|
-
'restores popover focus when an open selector becomes read-only (search=%s)',
|
|
2315
|
-
async hasSearch => {
|
|
2316
|
-
const user = userEvent.setup();
|
|
2317
|
-
const selector = (isReadOnly: boolean) => (
|
|
2318
|
-
<MultiSelector
|
|
2319
|
-
label="Fruit"
|
|
2320
|
-
options={defaultOptions}
|
|
2321
|
-
value={['Apple']}
|
|
2322
|
-
onChange={() => {}}
|
|
2323
|
-
hasSearch={hasSearch}
|
|
2324
|
-
isReadOnly={isReadOnly}
|
|
2325
|
-
/>
|
|
2326
|
-
);
|
|
2327
|
-
const {rerender} = render(selector(false));
|
|
2328
|
-
const editableTrigger = screen.getByRole(
|
|
2329
|
-
hasSearch ? 'button' : 'combobox',
|
|
2330
|
-
{name: 'Fruit'},
|
|
2331
|
-
);
|
|
2332
|
-
await user.click(editableTrigger);
|
|
2333
|
-
if (hasSearch) {
|
|
2334
|
-
await waitFor(() =>
|
|
2335
|
-
expect(screen.getByRole('combobox', h)).toHaveFocus(),
|
|
2336
|
-
);
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
rerender(selector(true));
|
|
2340
|
-
|
|
2341
|
-
const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
|
|
2342
|
-
await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
|
|
2343
|
-
expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
|
|
2344
|
-
},
|
|
2345
|
-
);
|
|
2346
|
-
|
|
2347
|
-
it.each(sheetTransitionCases)(
|
|
2348
|
-
'finishes $presentation close and restores focus when read-only changes (search=$hasSearch)',
|
|
2349
|
-
async ({presentation, hasSearch}) => {
|
|
2350
|
-
stubCompactTouch(presentation);
|
|
2351
|
-
const user = userEvent.setup();
|
|
2352
|
-
const selector = (isReadOnly: boolean) => (
|
|
2353
|
-
<MultiSelector
|
|
2354
|
-
label="Fruit"
|
|
2355
|
-
options={defaultOptions}
|
|
2356
|
-
value={['Apple']}
|
|
2357
|
-
onChange={() => {}}
|
|
2358
|
-
hasSearch={hasSearch}
|
|
2359
|
-
presentation={presentation}
|
|
2360
|
-
isReadOnly={isReadOnly}
|
|
2361
|
-
/>
|
|
2362
|
-
);
|
|
2363
|
-
const {rerender} = render(selector(false));
|
|
2364
|
-
const editableTrigger = screen.getByRole(
|
|
2365
|
-
hasSearch ? 'button' : 'combobox',
|
|
2366
|
-
{name: 'Fruit'},
|
|
2367
|
-
);
|
|
2368
|
-
await user.click(editableTrigger);
|
|
2369
|
-
const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
|
|
2370
|
-
const panel = dialog.querySelector<HTMLElement>('.astryx-bottom-sheet');
|
|
2371
|
-
expect(panel).not.toBeNull();
|
|
2372
|
-
await waitFor(() =>
|
|
2373
|
-
expect(
|
|
2374
|
-
hasSearch
|
|
2375
|
-
? screen.getByRole('combobox', h)
|
|
2376
|
-
: screen.getByRole('listbox'),
|
|
2377
|
-
).toHaveFocus(),
|
|
2378
|
-
);
|
|
2379
|
-
|
|
2380
|
-
rerender(selector(true));
|
|
2381
|
-
|
|
2382
|
-
await waitFor(() => expect(dialog).toHaveAttribute('inert'));
|
|
2383
|
-
fireEvent.transitionEnd(panel!, {propertyName: 'transform'});
|
|
2384
|
-
const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
|
|
2385
|
-
await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
|
|
2386
|
-
expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
|
|
2387
|
-
expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
|
|
2388
|
-
},
|
|
2389
|
-
);
|
|
2390
|
-
|
|
2391
|
-
it.each(disabledPrecedenceCases)(
|
|
2392
|
-
'matches disabled-only semantics for $presentation (search=$hasSearch, reason=$hasDisabledMessage)',
|
|
2393
|
-
async ({presentation, hasSearch, hasDisabledMessage}) => {
|
|
2394
|
-
if (presentation === 'adaptive') {
|
|
2395
|
-
stubCompactTouch(presentation);
|
|
2396
|
-
}
|
|
2397
|
-
const user = userEvent.setup();
|
|
2398
|
-
const onChange = vi.fn();
|
|
2399
|
-
const selector = (isReadOnly: boolean) => (
|
|
2400
|
-
<form>
|
|
2401
|
-
<MultiSelector
|
|
2402
|
-
label="Fruit"
|
|
2403
|
-
htmlName="fruit"
|
|
2404
|
-
options={defaultOptions}
|
|
2405
|
-
value={['Apple', 'Banana']}
|
|
2406
|
-
onChange={onChange}
|
|
2407
|
-
hasClear
|
|
2408
|
-
hasSearch={hasSearch}
|
|
2409
|
-
presentation={presentation}
|
|
2410
|
-
isDisabled
|
|
2411
|
-
isReadOnly={isReadOnly}
|
|
2412
|
-
disabledMessage={
|
|
2413
|
-
hasDisabledMessage ? 'Locked by policy' : undefined
|
|
2414
|
-
}
|
|
2415
|
-
/>
|
|
2416
|
-
</form>
|
|
2417
|
-
);
|
|
2418
|
-
const {container, rerender} = render(selector(false));
|
|
2419
|
-
const getTrigger = () =>
|
|
2420
|
-
screen.getByRole(hasSearch ? 'button' : 'combobox', {name: 'Fruit'});
|
|
2421
|
-
const snapshot = () => {
|
|
2422
|
-
const trigger = getTrigger();
|
|
2423
|
-
const root = container.querySelector('.astryx-multi-selector');
|
|
2424
|
-
return {
|
|
2425
|
-
tagName: trigger.tagName,
|
|
2426
|
-
role: trigger.getAttribute('role'),
|
|
2427
|
-
hasPopup: trigger.getAttribute('aria-haspopup'),
|
|
2428
|
-
expanded: trigger.getAttribute('aria-expanded'),
|
|
2429
|
-
controls: trigger.getAttribute('aria-controls'),
|
|
2430
|
-
readOnly: trigger.getAttribute('aria-readonly'),
|
|
2431
|
-
disabled: trigger.matches(':disabled'),
|
|
2432
|
-
ariaDisabled: trigger.getAttribute('aria-disabled'),
|
|
2433
|
-
tabIndex: trigger.tabIndex,
|
|
2434
|
-
rootClassName: root?.className,
|
|
2435
|
-
rootDisabled: root?.getAttribute('data-disabled'),
|
|
2436
|
-
rootReadOnly: root?.getAttribute('data-readonly'),
|
|
2437
|
-
hasIndicator:
|
|
2438
|
-
root?.querySelector('.astryx-multi-selector-indicator-icon') !=
|
|
2439
|
-
null,
|
|
2440
|
-
listboxes: screen.queryAllByRole('listbox', h).length,
|
|
2441
|
-
dialogs: screen.queryAllByRole('dialog', h).length,
|
|
2442
|
-
formEntries: [
|
|
2443
|
-
...new FormData(container.querySelector('form')!).entries(),
|
|
2444
|
-
],
|
|
2445
|
-
};
|
|
2446
|
-
};
|
|
2447
|
-
const disabledOnly = snapshot();
|
|
2448
|
-
|
|
2449
|
-
rerender(selector(true));
|
|
2450
|
-
|
|
2451
|
-
expect(snapshot()).toEqual(disabledOnly);
|
|
2452
|
-
expect(disabledOnly.rootDisabled).toBe('disabled');
|
|
2453
|
-
expect(disabledOnly.rootReadOnly).toBeNull();
|
|
2454
|
-
expect(disabledOnly.readOnly).toBeNull();
|
|
2455
|
-
expect(disabledOnly.formEntries).toEqual([]);
|
|
2456
|
-
const trigger = getTrigger();
|
|
2457
|
-
await user.click(trigger);
|
|
2458
|
-
trigger.focus();
|
|
2459
|
-
await user.keyboard('{Enter}{ArrowDown}b{Backspace}');
|
|
2460
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
2461
|
-
},
|
|
2462
|
-
);
|
|
2463
|
-
});
|
|
2464
|
-
|
|
2465
|
-
describe('form participation', () => {
|
|
2466
|
-
it('submits one entry per selected value under htmlName', () => {
|
|
2467
|
-
const {container} = render(
|
|
2468
|
-
<form>
|
|
2469
|
-
<MultiSelector
|
|
2470
|
-
label="Fruit"
|
|
2471
|
-
htmlName="fruit"
|
|
2472
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
2473
|
-
value={['Apple', 'Orange']}
|
|
2474
|
-
onChange={() => {}}
|
|
2475
|
-
/>
|
|
2476
|
-
</form>,
|
|
2477
|
-
);
|
|
2478
|
-
const data = new FormData(container.querySelector('form')!);
|
|
2479
|
-
expect(data.getAll('fruit')).toEqual(['Apple', 'Orange']);
|
|
2480
|
-
});
|
|
2481
|
-
|
|
2482
|
-
it('is excluded from form data when disabled', () => {
|
|
2483
|
-
const {container} = render(
|
|
2484
|
-
<form>
|
|
2485
|
-
<MultiSelector
|
|
2486
|
-
label="Fruit"
|
|
2487
|
-
htmlName="fruit"
|
|
2488
|
-
options={['Apple']}
|
|
2489
|
-
value={['Apple']}
|
|
2490
|
-
onChange={() => {}}
|
|
2491
|
-
isDisabled
|
|
2492
|
-
/>
|
|
2493
|
-
</form>,
|
|
2494
|
-
);
|
|
2495
|
-
expect([
|
|
2496
|
-
...new FormData(container.querySelector('form')!).keys(),
|
|
2497
|
-
]).toEqual([]);
|
|
2498
|
-
});
|
|
2499
|
-
});
|
|
2500
|
-
});
|
|
2501
|
-
|
|
2502
|
-
describe('MultiSelector statusVariant forwarding', () => {
|
|
2503
|
-
it('defaults to attached (status renders with data-variant="attached")', () => {
|
|
2504
|
-
const {container} = render(
|
|
2505
|
-
<MultiSelector
|
|
2506
|
-
label="Fruit"
|
|
2507
|
-
options={['Apple', 'Banana']}
|
|
2508
|
-
value={[]}
|
|
2509
|
-
onChange={() => {}}
|
|
2510
|
-
status={{type: 'error', message: 'Required'}}
|
|
2511
|
-
/>,
|
|
2512
|
-
);
|
|
2513
|
-
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
2514
|
-
'data-variant',
|
|
2515
|
-
'attached',
|
|
2516
|
-
);
|
|
2517
|
-
});
|
|
2518
|
-
|
|
2519
|
-
it('forwards statusVariant="detached" to the underlying Field status', () => {
|
|
2520
|
-
const {container} = render(
|
|
2521
|
-
<MultiSelector
|
|
2522
|
-
label="Fruit"
|
|
2523
|
-
options={['Apple', 'Banana']}
|
|
2524
|
-
value={[]}
|
|
2525
|
-
onChange={() => {}}
|
|
2526
|
-
status={{type: 'error', message: 'Required'}}
|
|
2527
|
-
statusVariant="detached"
|
|
2528
|
-
/>,
|
|
2529
|
-
);
|
|
2530
|
-
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
2531
|
-
'data-variant',
|
|
2532
|
-
'detached',
|
|
2533
|
-
);
|
|
2534
|
-
});
|
|
2535
|
-
|
|
2536
|
-
it('keeps the on-field status icon for the attached variant', () => {
|
|
2537
|
-
const {container} = render(
|
|
2538
|
-
<MultiSelector
|
|
2539
|
-
label="Fruit"
|
|
2540
|
-
options={['Apple', 'Banana']}
|
|
2541
|
-
value={[]}
|
|
2542
|
-
onChange={() => {}}
|
|
2543
|
-
status={{type: 'error', message: 'Required'}}
|
|
2544
|
-
/>,
|
|
2545
|
-
);
|
|
2546
|
-
// Attached: the status glyph replaces the chevron indicator on the field.
|
|
2547
|
-
expect(
|
|
2548
|
-
container.querySelector('.astryx-multi-selector-indicator-icon'),
|
|
2549
|
-
).toBeNull();
|
|
2550
|
-
});
|
|
2551
|
-
|
|
2552
|
-
it('suppresses the on-field status icon for the detached variant', () => {
|
|
2553
|
-
const {container} = render(
|
|
2554
|
-
<MultiSelector
|
|
2555
|
-
label="Fruit"
|
|
2556
|
-
options={['Apple', 'Banana']}
|
|
2557
|
-
value={[]}
|
|
2558
|
-
onChange={() => {}}
|
|
2559
|
-
status={{type: 'error', message: 'Required'}}
|
|
2560
|
-
statusVariant="detached"
|
|
2561
|
-
/>,
|
|
2562
|
-
);
|
|
2563
|
-
// Detached: the message box below carries its own leading icon, so the
|
|
2564
|
-
// field keeps its chevron indicator rather than duplicating the glyph.
|
|
2565
|
-
expect(
|
|
2566
|
-
container.querySelector('.astryx-multi-selector-indicator-icon'),
|
|
2567
|
-
).not.toBeNull();
|
|
2568
|
-
});
|
|
2569
|
-
|
|
2570
|
-
it.each(['secondary', 'ghost'] as const)(
|
|
2571
|
-
'detaches attached status by default for the %s variant',
|
|
2572
|
-
variant => {
|
|
2573
|
-
const {container} = render(
|
|
2574
|
-
<MultiSelector
|
|
2575
|
-
label="Fruit"
|
|
2576
|
-
options={['Apple', 'Banana']}
|
|
2577
|
-
value={[]}
|
|
2578
|
-
onChange={() => {}}
|
|
2579
|
-
variant={variant}
|
|
2580
|
-
status={{type: 'error', message: 'Required'}}
|
|
2581
|
-
/>,
|
|
2582
|
-
);
|
|
2583
|
-
expect(container.querySelector('.astryx-multi-selector')).toHaveAttribute(
|
|
2584
|
-
'data-variant',
|
|
2585
|
-
variant,
|
|
2586
|
-
);
|
|
2587
|
-
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
2588
|
-
'data-variant',
|
|
2589
|
-
'detached',
|
|
2590
|
-
);
|
|
2591
|
-
},
|
|
2592
|
-
);
|
|
2593
|
-
|
|
2594
|
-
it('uses a status tooltip for ghost multi-selectors when requested', () => {
|
|
2595
|
-
const {container} = render(
|
|
2596
|
-
<MultiSelector
|
|
2597
|
-
label="Fruit"
|
|
2598
|
-
options={['Apple', 'Banana']}
|
|
2599
|
-
value={[]}
|
|
2600
|
-
onChange={() => {}}
|
|
2601
|
-
variant="ghost"
|
|
2602
|
-
status={{type: 'warning', message: 'Some rows are hidden'}}
|
|
2603
|
-
statusVariant="tooltip"
|
|
2604
|
-
/>,
|
|
2605
|
-
);
|
|
2606
|
-
expect(container.querySelector('.astryx-field-status')).toBeNull();
|
|
2607
|
-
const statusButton = screen.getByRole('button', {
|
|
2608
|
-
name: /warning details/i,
|
|
2609
|
-
});
|
|
2610
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
2611
|
-
expect(tooltip).toHaveTextContent('Some rows are hidden');
|
|
2612
|
-
expect(statusButton.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
2613
|
-
expect(
|
|
2614
|
-
screen.getByRole('combobox').getAttribute('aria-describedby'),
|
|
2615
|
-
).toContain(tooltip.id);
|
|
2616
|
-
});
|
|
2617
|
-
});
|
|
2618
|
-
|
|
2619
|
-
describe('MultiSelector empty-state theme target', () => {
|
|
2620
|
-
const OPTIONS = ['Apple', 'Banana', 'Cherry'];
|
|
2621
|
-
|
|
2622
|
-
it('renders the astryx-multi-selector-empty-state target on the "No results found" element', async () => {
|
|
2623
|
-
const user = userEvent.setup();
|
|
2624
|
-
const {container} = render(
|
|
2625
|
-
<MultiSelector
|
|
2626
|
-
label="Fruit"
|
|
2627
|
-
options={OPTIONS}
|
|
2628
|
-
value={[]}
|
|
2629
|
-
onChange={() => {}}
|
|
2630
|
-
hasSearch
|
|
2631
|
-
/>,
|
|
2632
|
-
);
|
|
2633
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2634
|
-
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
2635
|
-
|
|
2636
|
-
const empty = container.querySelector('.astryx-multi-selector-empty-state');
|
|
2637
|
-
expect(empty).not.toBeNull();
|
|
2638
|
-
expect(empty).toHaveTextContent('No results found');
|
|
2639
|
-
});
|
|
2640
|
-
});
|
|
2641
|
-
|
|
2642
|
-
describe('MultiSelector clear icon theme target', () => {
|
|
2643
|
-
const ICON_OPTIONS = ['Apple', 'Banana', 'Orange'];
|
|
2644
|
-
|
|
2645
|
-
// Resolve the clear glyph span (the astryx-icon element inside the clear
|
|
2646
|
-
// button), independent of the theme target class.
|
|
2647
|
-
const getClearIcon = (): HTMLElement => {
|
|
2648
|
-
const button = screen.getByRole('button', {name: 'Clear all Fruit'});
|
|
2649
|
-
const icon = button.querySelector('.astryx-icon');
|
|
2650
|
-
if (icon == null) {
|
|
2651
|
-
throw new Error('clear icon not found');
|
|
2652
|
-
}
|
|
2653
|
-
return icon as HTMLElement;
|
|
2654
|
-
};
|
|
2655
|
-
|
|
2656
|
-
it('renders only the astryx-input-clear-icon target on the clear glyph', () => {
|
|
2657
|
-
render(
|
|
2658
|
-
<MultiSelector
|
|
2659
|
-
label="Fruit"
|
|
2660
|
-
options={ICON_OPTIONS}
|
|
2661
|
-
value={['Banana']}
|
|
2662
|
-
onChange={() => {}}
|
|
2663
|
-
hasClear
|
|
2664
|
-
/>,
|
|
2665
|
-
);
|
|
2666
|
-
// The canonical target lands on the icon element itself (not the button),
|
|
2667
|
-
// so a theme can restyle just this glyph (color, size, hover) via
|
|
2668
|
-
// `defineTheme` — a button-level target could not reach the icon's own
|
|
2669
|
-
// color/size.
|
|
2670
|
-
const icon = getClearIcon();
|
|
2671
|
-
expect(icon).toHaveClass('astryx-input-clear-icon');
|
|
2672
|
-
expect(icon).not.toHaveClass('astryx-multi-selector-clear-icon');
|
|
2673
|
-
expect(icon).toHaveClass('astryx-icon');
|
|
2674
|
-
});
|
|
2675
|
-
|
|
2676
|
-
it('keeps the clear button functional alongside the target', () => {
|
|
2677
|
-
const onChange = vi.fn();
|
|
2678
|
-
render(
|
|
2679
|
-
<MultiSelector
|
|
2680
|
-
label="Fruit"
|
|
2681
|
-
options={ICON_OPTIONS}
|
|
2682
|
-
value={['Banana']}
|
|
2683
|
-
onChange={onChange}
|
|
2684
|
-
hasClear
|
|
2685
|
-
/>,
|
|
2686
|
-
);
|
|
2687
|
-
const clear = screen.getByRole('button', {name: 'Clear all Fruit'});
|
|
2688
|
-
expect(clear.tagName).toBe('BUTTON');
|
|
2689
|
-
fireEvent.click(clear);
|
|
2690
|
-
expect(onChange).toHaveBeenCalledWith([]);
|
|
2691
|
-
});
|
|
2692
|
-
|
|
2693
|
-
it('routes the clear glyph through the shared clear button', () => {
|
|
2694
|
-
// The clear affordance composes the shared InputClearButton (a ghost Button
|
|
2695
|
-
// with a secondary/sm glyph), so the icon carries the canonical
|
|
2696
|
-
// `astryx-input-clear-icon` target. Aside from that target class it matches
|
|
2697
|
-
// the shared button's own `close`/`sm`/`secondary` glyph exactly, so the
|
|
2698
|
-
// default look is defined in one place.
|
|
2699
|
-
render(
|
|
2700
|
-
<MultiSelector
|
|
2701
|
-
label="Fruit"
|
|
2702
|
-
options={ICON_OPTIONS}
|
|
2703
|
-
value={['Banana']}
|
|
2704
|
-
onChange={() => {}}
|
|
2705
|
-
hasClear
|
|
2706
|
-
/>,
|
|
2707
|
-
);
|
|
2708
|
-
const icon = getClearIcon();
|
|
2709
|
-
expect(icon).toHaveClass('astryx-input-clear-icon');
|
|
2710
|
-
expect(icon).not.toHaveClass('astryx-multi-selector-clear-icon');
|
|
2711
|
-
|
|
2712
|
-
const {container: refContainer} = render(
|
|
2713
|
-
<Icon icon="close" size="sm" color="secondary" />,
|
|
2714
|
-
);
|
|
2715
|
-
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
2716
|
-
|
|
2717
|
-
const styleClasses = (el: HTMLElement) =>
|
|
2718
|
-
el.className
|
|
2719
|
-
.split(' ')
|
|
2720
|
-
.filter(c => c !== 'astryx-input-clear-icon')
|
|
2721
|
-
.sort();
|
|
2722
|
-
|
|
2723
|
-
expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
|
|
2724
|
-
});
|
|
2725
|
-
|
|
2726
|
-
it('uses input-clear-icon so a theme reaches the icon color, size, and hover', () => {
|
|
2727
|
-
// jsdom cannot resolve the @layer cascade, so the DOM-class assertion above
|
|
2728
|
-
// (target lands on the icon element) plus this generation assertion (the
|
|
2729
|
-
// theme emits same-element icon rules in @layer astryx-theme) together
|
|
2730
|
-
// prove the seam: a same-element theme rule wins over the icon's own
|
|
2731
|
-
// base-layer color/size.
|
|
2732
|
-
const theme = defineTheme({
|
|
2733
|
-
name: 'multi-selector-shared-clear-icon-test',
|
|
2734
|
-
components: {
|
|
2735
|
-
'input-clear-icon': {
|
|
2736
|
-
base: {
|
|
2737
|
-
width: '12px',
|
|
2738
|
-
height: '12px',
|
|
2739
|
-
fontSize: '12px',
|
|
2740
|
-
color: 'var(--color-icon-secondary)',
|
|
2741
|
-
':hover': {color: 'var(--color-icon-primary)'},
|
|
2742
|
-
},
|
|
2743
|
-
},
|
|
2744
|
-
},
|
|
2745
|
-
});
|
|
2746
|
-
const css = generateThemeTestCSS(theme);
|
|
2747
|
-
expect(css).toContain('.astryx-input-clear-icon {');
|
|
2748
|
-
expect(css).toContain('width: 12px');
|
|
2749
|
-
expect(css).toContain('height: 12px');
|
|
2750
|
-
expect(css).toContain('.astryx-input-clear-icon:hover');
|
|
2751
|
-
expect(css).toContain('color: var(--color-icon-primary)');
|
|
2752
|
-
});
|
|
2753
|
-
});
|
|
2754
|
-
|
|
2755
|
-
describe('MultiSelector indicator (chevron) icon theme target', () => {
|
|
2756
|
-
const ICON_OPTIONS = ['Apple', 'Banana', 'Orange'];
|
|
2757
|
-
|
|
2758
|
-
const getIndicatorIcon = (container: HTMLElement): HTMLElement => {
|
|
2759
|
-
// The chevron is the only glyph carrying the indicator target class.
|
|
2760
|
-
const icon = container.querySelector(
|
|
2761
|
-
'.astryx-multi-selector-indicator-icon',
|
|
2762
|
-
);
|
|
2763
|
-
if (icon == null) {
|
|
2764
|
-
throw new Error('indicator icon not found');
|
|
2765
|
-
}
|
|
2766
|
-
return icon as HTMLElement;
|
|
2767
|
-
};
|
|
2768
|
-
|
|
2769
|
-
it('renders the astryx-multi-selector-indicator-icon target on the chevron glyph', () => {
|
|
2770
|
-
const {container} = render(
|
|
2771
|
-
<MultiSelector
|
|
2772
|
-
label="Fruit"
|
|
2773
|
-
options={ICON_OPTIONS}
|
|
2774
|
-
value={[]}
|
|
2775
|
-
onChange={() => {}}
|
|
2776
|
-
/>,
|
|
2777
|
-
);
|
|
2778
|
-
// The stable theme target lands on the icon element itself (not the trigger
|
|
2779
|
-
// button), so a theme can restyle just this glyph (color, size, hover) —
|
|
2780
|
-
// and each open/closed state — via `defineTheme`. A button-level target
|
|
2781
|
-
// could not reach the icon's own color/size.
|
|
2782
|
-
const icon = getIndicatorIcon(container);
|
|
2783
|
-
expect(icon).toHaveClass('astryx-multi-selector-indicator-icon');
|
|
2784
|
-
expect(icon).toHaveClass('astryx-icon');
|
|
2785
|
-
// Open/closed state is reflected so a theme can target each state alone.
|
|
2786
|
-
expect(icon).toHaveAttribute('data-state', 'collapsed');
|
|
2787
|
-
});
|
|
2788
|
-
|
|
2789
|
-
it('reflects the expanded state on the chevron when the popover is open', async () => {
|
|
2790
|
-
const user = userEvent.setup();
|
|
2791
|
-
const {container} = render(
|
|
2792
|
-
<MultiSelector
|
|
2793
|
-
label="Fruit"
|
|
2794
|
-
options={ICON_OPTIONS}
|
|
2795
|
-
value={[]}
|
|
2796
|
-
onChange={() => {}}
|
|
2797
|
-
/>,
|
|
2798
|
-
);
|
|
2799
|
-
await user.click(screen.getByRole('combobox'));
|
|
2800
|
-
await waitFor(() => {
|
|
2801
|
-
expect(getIndicatorIcon(container)).toHaveAttribute(
|
|
2802
|
-
'data-state',
|
|
2803
|
-
'expanded',
|
|
2804
|
-
);
|
|
2805
|
-
});
|
|
2806
|
-
});
|
|
2807
|
-
|
|
2808
|
-
it('renders the default icon (secondary color, sm size) byte-identically', () => {
|
|
2809
|
-
// Pixel-identical default guard: the chevron glyph must carry the exact
|
|
2810
|
-
// same StyleX color/size classes as a standalone secondary/sm icon. The
|
|
2811
|
-
// glyph now sets --color-icon-secondary itself rather than inheriting it
|
|
2812
|
-
// from a wrapper span that set the same token, so the rendered color is
|
|
2813
|
-
// unchanged. The added
|
|
2814
|
-
// target class + data-state are purely additive — they change nothing until
|
|
2815
|
-
// a theme targets them.
|
|
2816
|
-
const {container} = render(
|
|
2817
|
-
<MultiSelector
|
|
2818
|
-
label="Fruit"
|
|
2819
|
-
options={ICON_OPTIONS}
|
|
2820
|
-
value={[]}
|
|
2821
|
-
onChange={() => {}}
|
|
2822
|
-
/>,
|
|
2823
|
-
);
|
|
2824
|
-
const icon = getIndicatorIcon(container);
|
|
2825
|
-
|
|
2826
|
-
const {container: refContainer} = render(
|
|
2827
|
-
<Icon icon="chevronDown" size="sm" color="secondary" />,
|
|
2828
|
-
);
|
|
2829
|
-
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
2830
|
-
|
|
2831
|
-
// Exclude the additive theme-target classes (the stable target + its
|
|
2832
|
-
// reflected state class) so only StyleX classes remain.
|
|
2833
|
-
const themeTargetClasses = new Set([
|
|
2834
|
-
'astryx-multi-selector-indicator-icon',
|
|
2835
|
-
'collapsed',
|
|
2836
|
-
'expanded',
|
|
2837
|
-
]);
|
|
2838
|
-
const styleClasses = (el: HTMLElement) =>
|
|
2839
|
-
el.className
|
|
2840
|
-
.split(' ')
|
|
2841
|
-
.filter(c => !themeTargetClasses.has(c))
|
|
2842
|
-
.sort();
|
|
2843
|
-
|
|
2844
|
-
// A superset, not an exact match: the chevron additionally carries the
|
|
2845
|
-
// rotation styles, which live on the glyph precisely so a theme can reach
|
|
2846
|
-
// the transform through the same selector as the color. The guard that
|
|
2847
|
-
// matters is that every color/size class of a standalone icon is still
|
|
2848
|
-
// present — i.e. the default look has not drifted.
|
|
2849
|
-
expect(styleClasses(icon)).toEqual(
|
|
2850
|
-
expect.arrayContaining(styleClasses(refIcon)),
|
|
2851
|
-
);
|
|
2852
|
-
});
|
|
2853
|
-
|
|
2854
|
-
it('exposes multi-selector-indicator-icon so a theme reaches the icon size and per-state color', () => {
|
|
2855
|
-
// jsdom cannot resolve the @layer cascade, so the DOM-class assertions
|
|
2856
|
-
// above (target lands on the icon element) plus this generation assertion
|
|
2857
|
-
// (the theme emits same-element icon rules in @layer astryx-theme) together
|
|
2858
|
-
// prove the seam: a same-element theme rule wins over the icon's own
|
|
2859
|
-
// base-layer color/size.
|
|
2860
|
-
const theme = defineTheme({
|
|
2861
|
-
name: 'multi-selector-indicator-icon-test',
|
|
2862
|
-
components: {
|
|
2863
|
-
'multi-selector-indicator-icon': {
|
|
2864
|
-
base: {width: '14px', height: '14px', fontSize: '14px'},
|
|
2865
|
-
'state:expanded': {color: 'var(--color-icon-primary)'},
|
|
2866
|
-
},
|
|
2867
|
-
},
|
|
2868
|
-
});
|
|
2869
|
-
const css = generateThemeTestCSS(theme);
|
|
2870
|
-
expect(css).toContain('.astryx-multi-selector-indicator-icon {');
|
|
2871
|
-
expect(css).toContain('width: 14px');
|
|
2872
|
-
expect(css).toContain('height: 14px');
|
|
2873
|
-
expect(css).toContain(
|
|
2874
|
-
'.astryx-multi-selector-indicator-icon[data-state="expanded"]',
|
|
2875
|
-
);
|
|
2876
|
-
expect(css).toContain('color: var(--color-icon-primary)');
|
|
2877
|
-
});
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
|
-
describe('MultiSelector list structure', () => {
|
|
2881
|
-
it('does not draw a divider under select-all', async () => {
|
|
2882
|
-
const user = userEvent.setup();
|
|
2883
|
-
render(
|
|
2884
|
-
<MultiSelector
|
|
2885
|
-
label="Fruit"
|
|
2886
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
2887
|
-
value={[]}
|
|
2888
|
-
onChange={() => {}}
|
|
2889
|
-
hasSelectAll
|
|
2890
|
-
/>,
|
|
2891
|
-
);
|
|
2892
|
-
await user.click(screen.getByRole('combobox'));
|
|
2893
|
-
// Select-all is the first row of the list, not a section of its own. No
|
|
2894
|
-
// option here declares a divider and there is no search row, so the panel
|
|
2895
|
-
// should contain no rule at all.
|
|
2896
|
-
expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
2897
|
-
const [first] = screen.getAllByRole('option', h);
|
|
2898
|
-
expect(first).toHaveTextContent('Select all');
|
|
2899
|
-
});
|
|
2900
|
-
|
|
2901
|
-
it('renders a section title as a plain heading inside the group, not a divider', async () => {
|
|
2902
|
-
const user = userEvent.setup();
|
|
2903
|
-
render(
|
|
2904
|
-
<MultiSelector
|
|
2905
|
-
label="Fruit"
|
|
2906
|
-
options={[
|
|
2907
|
-
{
|
|
2908
|
-
type: 'section',
|
|
2909
|
-
title: 'Citrus',
|
|
2910
|
-
options: [
|
|
2911
|
-
{value: 'orange', label: 'Orange'},
|
|
2912
|
-
{value: 'lemon', label: 'Lemon'},
|
|
2913
|
-
],
|
|
2914
|
-
},
|
|
2915
|
-
]}
|
|
2916
|
-
value={[]}
|
|
2917
|
-
onChange={() => {}}
|
|
2918
|
-
/>,
|
|
2919
|
-
);
|
|
2920
|
-
await user.click(screen.getByRole('combobox'));
|
|
2921
|
-
|
|
2922
|
-
// A labeled Divider used to stand in for the heading; it rendered a
|
|
2923
|
-
// role="separator" as a direct child of the listbox and stacked a second
|
|
2924
|
-
// rule under the search row's own.
|
|
2925
|
-
expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
2926
|
-
|
|
2927
|
-
const group = screen.getByRole('group', {name: 'Citrus', ...h});
|
|
2928
|
-
const heading = group.querySelector(
|
|
2929
|
-
'.astryx-multi-selector-section-heading',
|
|
2930
|
-
);
|
|
2931
|
-
expect(heading).toBeTruthy();
|
|
2932
|
-
expect(heading).toHaveTextContent('Citrus');
|
|
2933
|
-
// The group already carries the title as its accessible name, so the
|
|
2934
|
-
// visible heading must not announce it a second time.
|
|
2935
|
-
expect(heading).toHaveAttribute('aria-hidden', 'true');
|
|
2936
|
-
// ...and it precedes the options it heads.
|
|
2937
|
-
const [firstOption] = within(group).getAllByRole('option', h);
|
|
2938
|
-
expect(
|
|
2939
|
-
heading!.compareDocumentPosition(firstOption) &
|
|
2940
|
-
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2941
|
-
).toBeTruthy();
|
|
2942
|
-
});
|
|
2943
|
-
|
|
2944
|
-
it('hides a standalone divider from the accessibility tree (#4994)', async () => {
|
|
2945
|
-
const user = userEvent.setup();
|
|
2946
|
-
render(
|
|
2947
|
-
<MultiSelector
|
|
2948
|
-
label="Fruit"
|
|
2949
|
-
options={['Apple', {type: 'divider'}, 'Banana']}
|
|
2950
|
-
value={[]}
|
|
2951
|
-
onChange={() => {}}
|
|
2952
|
-
/>,
|
|
2953
|
-
);
|
|
2954
|
-
await user.click(screen.getByRole('combobox'));
|
|
2955
|
-
|
|
2956
|
-
// role="listbox" only permits option/group children. The divider still
|
|
2957
|
-
// renders role="separator" (unchanged visual/DOM), but must be excluded
|
|
2958
|
-
// from the accessibility tree so it never reaches the listbox's exposed
|
|
2959
|
-
// children (axe aria-required-children).
|
|
2960
|
-
const divider = document.querySelector('[role="separator"]');
|
|
2961
|
-
expect(divider).toBeTruthy();
|
|
2962
|
-
expect(divider).toHaveAttribute('aria-hidden', 'true');
|
|
2963
|
-
});
|
|
2964
|
-
});
|
|
2965
|
-
|
|
2966
|
-
describe('MultiSelector search affordances', () => {
|
|
2967
|
-
it('renders the search row seamlessly — no nested input box, a divider under it', async () => {
|
|
2968
|
-
const user = userEvent.setup();
|
|
2969
|
-
render(
|
|
2970
|
-
<MultiSelector
|
|
2971
|
-
label="Fruit"
|
|
2972
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
2973
|
-
value={[]}
|
|
2974
|
-
onChange={() => {}}
|
|
2975
|
-
hasSearch
|
|
2976
|
-
/>,
|
|
2977
|
-
);
|
|
2978
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2979
|
-
|
|
2980
|
-
const search = screen.getByRole('combobox', h);
|
|
2981
|
-
// The row is the outer gutter; the input sits inside the rounded field.
|
|
2982
|
-
const row = search.closest('.astryx-multi-selector-search');
|
|
2983
|
-
const field = search.parentElement;
|
|
2984
|
-
if (!row || !field) {
|
|
2985
|
-
throw new Error('search row not found');
|
|
2986
|
-
}
|
|
2987
|
-
// The panel is already a bordered surface: the field inside it must not be
|
|
2988
|
-
// a second bordered box (this used to render a TextInput).
|
|
2989
|
-
expect(row).not.toHaveClass('astryx-text-input');
|
|
2990
|
-
expect(search.closest('.astryx-text-input')).toBeNull();
|
|
2991
|
-
// The field is a rounded box inset from the panel edge, shaped like the
|
|
2992
|
-
// option rows under it — not a full-bleed header strip.
|
|
2993
|
-
expect(field).not.toBe(row);
|
|
2994
|
-
// ...and a divider separates it from the options.
|
|
2995
|
-
const separator = document.querySelector('[role="separator"]');
|
|
2996
|
-
if (!separator) {
|
|
2997
|
-
throw new Error('divider not found');
|
|
2998
|
-
}
|
|
2999
|
-
// Order: row, then divider, then the listbox.
|
|
3000
|
-
expect(
|
|
3001
|
-
row.compareDocumentPosition(separator) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
3002
|
-
).toBeTruthy();
|
|
3003
|
-
const listbox = screen.getByRole('listbox', h);
|
|
3004
|
-
expect(
|
|
3005
|
-
separator.compareDocumentPosition(listbox) &
|
|
3006
|
-
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
3007
|
-
).toBeTruthy();
|
|
3008
|
-
});
|
|
3009
|
-
|
|
3010
|
-
it('keeps the search row outside the scrolling option list', async () => {
|
|
3011
|
-
const user = userEvent.setup();
|
|
3012
|
-
render(
|
|
3013
|
-
<MultiSelector
|
|
3014
|
-
label="Fruit"
|
|
3015
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3016
|
-
value={[]}
|
|
3017
|
-
onChange={() => {}}
|
|
3018
|
-
hasSearch
|
|
3019
|
-
/>,
|
|
3020
|
-
);
|
|
3021
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
3022
|
-
// The options scroll under the header rather than carrying it away, so the
|
|
3023
|
-
// field stays reachable in a long list.
|
|
3024
|
-
const search = screen.getByRole('combobox', h);
|
|
3025
|
-
const listbox = screen.getByRole('listbox', h);
|
|
3026
|
-
expect(listbox.contains(search)).toBe(false);
|
|
3027
|
-
});
|
|
3028
|
-
|
|
3029
|
-
it('renders a decorative (aria-hidden) magnifier icon whenever hasSearch is on', async () => {
|
|
3030
|
-
const user = userEvent.setup();
|
|
3031
|
-
render(
|
|
3032
|
-
<MultiSelector
|
|
3033
|
-
label="Fruit"
|
|
3034
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3035
|
-
value={[]}
|
|
3036
|
-
onChange={() => {}}
|
|
3037
|
-
hasSearch
|
|
3038
|
-
/>,
|
|
3039
|
-
);
|
|
3040
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
3041
|
-
const search = screen.getByRole('combobox', h);
|
|
3042
|
-
// The magnifier leads the search row, as a sibling of the <input>.
|
|
3043
|
-
const wrapper = search.parentElement;
|
|
3044
|
-
const magnifier = wrapper?.querySelector('.astryx-icon');
|
|
3045
|
-
expect(magnifier).toBeTruthy();
|
|
3046
|
-
expect(magnifier?.getAttribute('aria-hidden')).toBe('true');
|
|
3047
|
-
expect(magnifier?.getAttribute('aria-label')).toBeNull();
|
|
3048
|
-
});
|
|
3049
|
-
|
|
3050
|
-
it('renders the clear button once a query is typed and clears + refocuses on click', async () => {
|
|
3051
|
-
const user = userEvent.setup();
|
|
3052
|
-
render(
|
|
3053
|
-
<MultiSelector
|
|
3054
|
-
label="Fruit"
|
|
3055
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3056
|
-
value={[]}
|
|
3057
|
-
onChange={() => {}}
|
|
3058
|
-
hasSearch
|
|
3059
|
-
/>,
|
|
3060
|
-
);
|
|
3061
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
3062
|
-
const search = screen.getByRole('combobox', h);
|
|
3063
|
-
await user.type(search, 'ap');
|
|
3064
|
-
expect(search).toHaveValue('ap');
|
|
3065
|
-
|
|
3066
|
-
// The clear button is TextInput's built-in hasClear affordance; its name is
|
|
3067
|
-
// derived from the field label ("Search options").
|
|
3068
|
-
const clear = screen.getByRole('button', {
|
|
3069
|
-
name: 'Clear Search options',
|
|
3070
|
-
hidden: true,
|
|
3071
|
-
});
|
|
3072
|
-
|
|
3073
|
-
await user.click(clear);
|
|
3074
|
-
expect(search).toHaveValue('');
|
|
3075
|
-
expect(search).toHaveFocus();
|
|
3076
|
-
});
|
|
3077
|
-
|
|
3078
|
-
it('does not render the clear button when the query is empty', async () => {
|
|
3079
|
-
const user = userEvent.setup();
|
|
3080
|
-
render(
|
|
3081
|
-
<MultiSelector
|
|
3082
|
-
label="Fruit"
|
|
3083
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3084
|
-
value={[]}
|
|
3085
|
-
onChange={() => {}}
|
|
3086
|
-
hasSearch
|
|
3087
|
-
/>,
|
|
3088
|
-
);
|
|
3089
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
3090
|
-
expect(
|
|
3091
|
-
screen.queryByRole('button', {
|
|
3092
|
-
name: 'Clear Search options',
|
|
3093
|
-
hidden: true,
|
|
3094
|
-
}),
|
|
3095
|
-
).not.toBeInTheDocument();
|
|
3096
|
-
});
|
|
3097
|
-
|
|
3098
|
-
it('keeps the combobox contract on the input, not the affordances', async () => {
|
|
3099
|
-
const user = userEvent.setup();
|
|
3100
|
-
render(
|
|
3101
|
-
<MultiSelector
|
|
3102
|
-
label="Fruit"
|
|
3103
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3104
|
-
value={[]}
|
|
3105
|
-
onChange={() => {}}
|
|
3106
|
-
hasSearch
|
|
3107
|
-
/>,
|
|
3108
|
-
);
|
|
3109
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
3110
|
-
const search = screen.getByRole('combobox', h);
|
|
3111
|
-
expect(search.tagName).toBe('INPUT');
|
|
3112
|
-
expect(search).toHaveAttribute('aria-autocomplete', 'list');
|
|
3113
|
-
});
|
|
3114
|
-
|
|
3115
|
-
it('tabs from the search input to the clear button (keeping the popup open) when a query is showing it', async () => {
|
|
3116
|
-
const user = userEvent.setup();
|
|
3117
|
-
render(
|
|
3118
|
-
<MultiSelector
|
|
3119
|
-
label="Fruit"
|
|
3120
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3121
|
-
value={[]}
|
|
3122
|
-
onChange={() => {}}
|
|
3123
|
-
hasSearch
|
|
3124
|
-
/>,
|
|
3125
|
-
);
|
|
3126
|
-
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
3127
|
-
const trigger = screen.getByRole('button', {name: 'Fruit'});
|
|
3128
|
-
const search = screen.getByRole('combobox', h);
|
|
3129
|
-
await user.type(search, 'ap');
|
|
3130
|
-
expect(search).toHaveFocus();
|
|
3131
|
-
|
|
3132
|
-
// Forward-tab lands on the clear (✕) button and the popup stays open, so
|
|
3133
|
-
// the affordance is keyboard-reachable rather than being skipped when the
|
|
3134
|
-
// input's Tab dismisses the popup.
|
|
3135
|
-
await user.tab();
|
|
3136
|
-
const clear = screen.getByRole('button', {
|
|
3137
|
-
name: 'Clear Search options',
|
|
3138
|
-
hidden: true,
|
|
3139
|
-
});
|
|
3140
|
-
expect(clear).toHaveFocus();
|
|
3141
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
3142
|
-
});
|
|
3143
|
-
|
|
3144
|
-
it('dismisses on Tab from the search input when there is no query (no clear button)', async () => {
|
|
3145
|
-
const user = userEvent.setup();
|
|
3146
|
-
render(
|
|
3147
|
-
<MultiSelector
|
|
3148
|
-
label="Fruit"
|
|
3149
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3150
|
-
value={[]}
|
|
3151
|
-
onChange={() => {}}
|
|
3152
|
-
hasSearch
|
|
3153
|
-
/>,
|
|
3154
|
-
);
|
|
3155
|
-
const trigger = screen.getByRole('button', {name: 'Fruit'});
|
|
3156
|
-
await user.click(trigger);
|
|
3157
|
-
const search = screen.getByRole('combobox', h);
|
|
3158
|
-
// Focus moves into the search input on open (via rAF).
|
|
3159
|
-
await waitFor(() => expect(search).toHaveFocus());
|
|
3160
|
-
|
|
3161
|
-
// With no query there is no clear button, so Tab dismisses the popup as a
|
|
3162
|
-
// plain combobox does.
|
|
3163
|
-
await user.tab();
|
|
3164
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
3165
|
-
});
|
|
3166
|
-
});
|
|
3167
|
-
|
|
3168
|
-
describe('MultiSelector disabled state theme target', () => {
|
|
3169
|
-
const getSelectorRoot = (container: HTMLElement): HTMLElement => {
|
|
3170
|
-
const root = container.querySelector('.astryx-multi-selector');
|
|
3171
|
-
if (root == null) {
|
|
3172
|
-
throw new Error('multi-selector root not found');
|
|
3173
|
-
}
|
|
3174
|
-
return root as HTMLElement;
|
|
3175
|
-
};
|
|
3176
|
-
|
|
3177
|
-
it('reflects data-state="disabled" on the root when disabled', () => {
|
|
3178
|
-
const {container} = render(
|
|
3179
|
-
<MultiSelector
|
|
3180
|
-
label="Fruit"
|
|
3181
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3182
|
-
value={[]}
|
|
3183
|
-
onChange={() => {}}
|
|
3184
|
-
isDisabled
|
|
3185
|
-
/>,
|
|
3186
|
-
);
|
|
3187
|
-
expect(getSelectorRoot(container)).toHaveAttribute(
|
|
3188
|
-
'data-disabled',
|
|
3189
|
-
'disabled',
|
|
3190
|
-
);
|
|
3191
|
-
});
|
|
3192
|
-
|
|
3193
|
-
it('omits the disabled class/attribute when enabled', () => {
|
|
3194
|
-
const {container} = render(
|
|
3195
|
-
<MultiSelector
|
|
3196
|
-
label="Fruit"
|
|
3197
|
-
options={['Apple', 'Banana', 'Orange']}
|
|
3198
|
-
value={[]}
|
|
3199
|
-
onChange={() => {}}
|
|
3200
|
-
/>,
|
|
3201
|
-
);
|
|
3202
|
-
const root = getSelectorRoot(container);
|
|
3203
|
-
expect(root).not.toHaveAttribute('data-disabled');
|
|
3204
|
-
expect(root).not.toHaveAttribute('data-disabled');
|
|
3205
|
-
});
|
|
3206
|
-
|
|
3207
|
-
it('exposes the disabled state so a theme can key on it', () => {
|
|
3208
|
-
const theme = defineTheme({
|
|
3209
|
-
name: 'multi-selector-disabled-state-test',
|
|
3210
|
-
components: {
|
|
3211
|
-
'multi-selector': {
|
|
3212
|
-
'disabled:disabled': {opacity: '0.4'},
|
|
3213
|
-
},
|
|
3214
|
-
},
|
|
3215
|
-
});
|
|
3216
|
-
const css = generateThemeTestCSS(theme);
|
|
3217
|
-
expect(css).toContain('.astryx-multi-selector[data-disabled="disabled"]');
|
|
3218
|
-
expect(css).toContain('opacity: 0.4');
|
|
3219
|
-
});
|
|
3220
|
-
});
|
|
3221
|
-
|
|
3222
|
-
describe('MultiSelector dropdown option theme target', () => {
|
|
3223
|
-
const ROW_OPTIONS = ['Apple', 'Banana', 'Orange'];
|
|
3224
|
-
|
|
3225
|
-
it('renders astryx-multi-selector-option, with its size, on every dropdown row', async () => {
|
|
3226
|
-
const user = userEvent.setup();
|
|
3227
|
-
render(
|
|
3228
|
-
<MultiSelector
|
|
3229
|
-
label="Fruit"
|
|
3230
|
-
options={ROW_OPTIONS}
|
|
3231
|
-
value={[]}
|
|
3232
|
-
onChange={() => {}}
|
|
3233
|
-
size="lg"
|
|
3234
|
-
/>,
|
|
3235
|
-
);
|
|
3236
|
-
await user.click(screen.getByRole('combobox'));
|
|
3237
|
-
const options = screen.getAllByRole('option', h);
|
|
3238
|
-
expect(options).toHaveLength(3);
|
|
3239
|
-
for (const option of options) {
|
|
3240
|
-
expect(option).toHaveClass('astryx-multi-selector-option');
|
|
3241
|
-
expect(option).toHaveAttribute('data-size', 'lg');
|
|
3242
|
-
expect(option).toHaveAttribute('data-size', 'lg');
|
|
3243
|
-
}
|
|
3244
|
-
});
|
|
3245
|
-
|
|
3246
|
-
it('carries the selected and disabled states a theme keys on', async () => {
|
|
3247
|
-
const user = userEvent.setup();
|
|
3248
|
-
render(
|
|
3249
|
-
<MultiSelector
|
|
3250
|
-
label="Fruit"
|
|
3251
|
-
options={[
|
|
3252
|
-
{value: 'apple', label: 'Apple'},
|
|
3253
|
-
{value: 'banana', label: 'Banana'},
|
|
3254
|
-
{value: 'orange', label: 'Orange', disabled: true},
|
|
3255
|
-
]}
|
|
3256
|
-
value={['apple']}
|
|
3257
|
-
onChange={() => {}}
|
|
3258
|
-
/>,
|
|
3259
|
-
);
|
|
3260
|
-
await user.click(screen.getByRole('combobox'));
|
|
3261
|
-
const [selected, plain, disabled] = screen.getAllByRole('option', h);
|
|
3262
|
-
|
|
3263
|
-
expect(selected).toHaveAttribute('data-selected', 'selected');
|
|
3264
|
-
expect(selected).toHaveAttribute('data-selected', 'selected');
|
|
3265
|
-
expect(plain).not.toHaveAttribute('data-selected');
|
|
3266
|
-
expect(plain).not.toHaveAttribute('data-selected');
|
|
3267
|
-
|
|
3268
|
-
expect(disabled).toHaveAttribute('data-disabled', 'disabled');
|
|
3269
|
-
expect(disabled).toHaveAttribute('data-disabled', 'disabled');
|
|
3270
|
-
expect(plain).not.toHaveAttribute('data-disabled');
|
|
3271
|
-
});
|
|
3272
|
-
|
|
3273
|
-
it('marks the Select All row with the select-all state, not a separate target', async () => {
|
|
3274
|
-
const user = userEvent.setup();
|
|
3275
|
-
render(
|
|
3276
|
-
<MultiSelector
|
|
3277
|
-
label="Fruit"
|
|
3278
|
-
options={ROW_OPTIONS}
|
|
3279
|
-
value={[]}
|
|
3280
|
-
onChange={() => {}}
|
|
3281
|
-
hasSelectAll
|
|
3282
|
-
/>,
|
|
3283
|
-
);
|
|
3284
|
-
await user.click(screen.getByRole('combobox'));
|
|
3285
|
-
|
|
3286
|
-
const [selectAllRow, ...regularRows] = screen.getAllByRole('option', h);
|
|
3287
|
-
expect(selectAllRow).toHaveTextContent('Select all');
|
|
3288
|
-
expect(selectAllRow).toHaveClass('astryx-multi-selector-option');
|
|
3289
|
-
expect(selectAllRow).toHaveAttribute('data-select-all', 'select-all');
|
|
3290
|
-
expect(selectAllRow).toHaveAttribute('data-select-all', 'select-all');
|
|
3291
|
-
|
|
3292
|
-
for (const row of regularRows) {
|
|
3293
|
-
expect(row).toHaveClass('astryx-multi-selector-option');
|
|
3294
|
-
expect(row).not.toHaveClass('select-all');
|
|
3295
|
-
expect(row).not.toHaveAttribute('data-select-all');
|
|
3296
|
-
}
|
|
3297
|
-
});
|
|
3298
|
-
|
|
3299
|
-
it('keeps the row targetable when renderOption replaces the label', async () => {
|
|
3300
|
-
const user = userEvent.setup();
|
|
3301
|
-
render(
|
|
3302
|
-
<MultiSelector
|
|
3303
|
-
label="Fruit"
|
|
3304
|
-
options={[{value: 'apple', label: 'Apple'}]}
|
|
3305
|
-
value={[]}
|
|
3306
|
-
onChange={() => {}}
|
|
3307
|
-
renderOption={option => (
|
|
3308
|
-
<span data-testid="custom-row">{option.label}</span>
|
|
3309
|
-
)}
|
|
3310
|
-
/>,
|
|
3311
|
-
);
|
|
3312
|
-
await user.click(screen.getByRole('combobox'));
|
|
3313
|
-
const option = screen.getAllByRole('option', h)[0];
|
|
3314
|
-
// The row owns the typography, so custom content inherits the same
|
|
3315
|
-
// treatment the fallback label gets — and one row override reaches both.
|
|
3316
|
-
expect(option).toHaveClass('astryx-multi-selector-option');
|
|
3317
|
-
expect(within(option).getByTestId('custom-row')).toHaveTextContent('Apple');
|
|
3318
|
-
});
|
|
3319
|
-
|
|
3320
|
-
it('exposes the row target, its states and its size to defineTheme', () => {
|
|
3321
|
-
const theme = defineTheme({
|
|
3322
|
-
name: 'multi-selector-option-target-test',
|
|
3323
|
-
components: {
|
|
3324
|
-
'multi-selector-option': {
|
|
3325
|
-
base: {borderRadius: '8px', fontWeight: '600'},
|
|
3326
|
-
selected: {backgroundColor: 'var(--color-background-muted)'},
|
|
3327
|
-
'select-all': {fontWeight: '700'},
|
|
3328
|
-
'size:lg': {borderRadius: '12px'},
|
|
3329
|
-
},
|
|
3330
|
-
},
|
|
3331
|
-
});
|
|
3332
|
-
const css = generateThemeTestCSS(theme);
|
|
3333
|
-
expect(css).toContain('.astryx-multi-selector-option {');
|
|
3334
|
-
expect(css).toContain(
|
|
3335
|
-
'.astryx-multi-selector-option[data-selected="selected"]',
|
|
3336
|
-
);
|
|
3337
|
-
expect(css).toContain(
|
|
3338
|
-
'.astryx-multi-selector-option[data-select-all="select-all"]',
|
|
3339
|
-
);
|
|
3340
|
-
expect(css).toContain('.astryx-multi-selector-option[data-size="lg"]');
|
|
3341
|
-
});
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
describe('MultiSelector popup theme target', () => {
|
|
3345
|
-
it('puts astryx-multi-selector-popup on the surface that paints, not the list inside it', async () => {
|
|
3346
|
-
const user = userEvent.setup();
|
|
3347
|
-
render(
|
|
3348
|
-
<MultiSelector
|
|
3349
|
-
label="Fruit"
|
|
3350
|
-
options={['Apple', 'Banana']}
|
|
3351
|
-
value={[]}
|
|
3352
|
-
onChange={() => {}}
|
|
3353
|
-
/>,
|
|
3354
|
-
);
|
|
3355
|
-
await user.click(screen.getByRole('combobox', {name: /Fruit/}));
|
|
3356
|
-
|
|
3357
|
-
const popup = document.querySelector(
|
|
3358
|
-
'.astryx-multi-selector-popup',
|
|
3359
|
-
) as HTMLElement;
|
|
3360
|
-
expect(popup).not.toBeNull();
|
|
3361
|
-
expect(popup).toHaveClass('astryx-popover');
|
|
3362
|
-
// The scrolling list is a descendant, not the target itself.
|
|
3363
|
-
expect(popup.querySelector('[role="listbox"]')).not.toBeNull();
|
|
3364
|
-
expect(popup.getAttribute('role')).toBeNull();
|
|
3365
|
-
|
|
3366
|
-
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
3367
|
-
expect(popup).not.toBe(layer);
|
|
3368
|
-
expect(layer.contains(popup)).toBe(true);
|
|
3369
|
-
});
|
|
3370
|
-
|
|
3371
|
-
it('stays closed when the trigger click follows its own light dismiss (#5004)', async () => {
|
|
3372
|
-
const user = userEvent.setup();
|
|
3373
|
-
render(
|
|
3374
|
-
<MultiSelector
|
|
3375
|
-
label="Fruit"
|
|
3376
|
-
options={['Apple', 'Banana', 'Cherry']}
|
|
3377
|
-
value={[]}
|
|
3378
|
-
onChange={() => {}}
|
|
3379
|
-
/>,
|
|
3380
|
-
);
|
|
3381
|
-
const trigger = screen.getByRole('combobox');
|
|
3382
|
-
await user.click(trigger);
|
|
3383
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
3384
|
-
|
|
3385
|
-
// The browser dismissed the popup on pointerup and queued the toggle. When
|
|
3386
|
-
// that event lands before the click — WebKit, or any engine under load —
|
|
3387
|
-
// the click used to read a closed popup and reopen it.
|
|
3388
|
-
fireEvent.pointerDown(trigger);
|
|
3389
|
-
const popover = document.querySelector('[popover]') as HTMLElement;
|
|
3390
|
-
act(() => {
|
|
3391
|
-
popover.dispatchEvent(
|
|
3392
|
-
Object.assign(new Event('toggle'), {
|
|
3393
|
-
oldState: 'open',
|
|
3394
|
-
newState: 'closed',
|
|
3395
|
-
}),
|
|
3396
|
-
);
|
|
3397
|
-
});
|
|
3398
|
-
// Synchronously: the click falls inside the one gesture the guard covers,
|
|
3399
|
-
// as it does in a browser a few milliseconds behind the dismissal.
|
|
3400
|
-
fireEvent.click(trigger);
|
|
3401
|
-
|
|
3402
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
3403
|
-
expect(trigger.parentElement).toHaveClass(
|
|
3404
|
-
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
3405
|
-
);
|
|
3406
|
-
});
|
|
3407
|
-
});
|
|
3408
|
-
|
|
3409
|
-
describe('MultiSelector trigger render prop', () => {
|
|
3410
|
-
const OPTIONS = [
|
|
3411
|
-
{value: 'bug', label: 'Bug'},
|
|
3412
|
-
{value: 'feature', label: 'Feature'},
|
|
3413
|
-
{value: 'docs', label: 'Docs'},
|
|
3414
|
-
];
|
|
3415
|
-
|
|
3416
|
-
it('renders the caller control instead of the field and button', () => {
|
|
3417
|
-
render(
|
|
3418
|
-
<MultiSelector
|
|
3419
|
-
label="Labels"
|
|
3420
|
-
options={OPTIONS}
|
|
3421
|
-
value={[]}
|
|
3422
|
-
onChange={() => {}}
|
|
3423
|
-
renderTrigger={props => (
|
|
3424
|
-
<button type="button" {...props}>
|
|
3425
|
-
Pick labels
|
|
3426
|
-
</button>
|
|
3427
|
-
)}
|
|
3428
|
-
/>,
|
|
3429
|
-
);
|
|
3430
|
-
const opener = screen.getByRole('button', {name: 'Pick labels'});
|
|
3431
|
-
expect(opener).toHaveAttribute('aria-haspopup', 'listbox');
|
|
3432
|
-
expect(opener).toHaveAttribute('aria-expanded', 'false');
|
|
3433
|
-
expect(opener).toHaveAttribute('aria-controls');
|
|
3434
|
-
// No field chrome: exactly one button, and no Field label element.
|
|
3435
|
-
expect(screen.getAllByRole('button')).toHaveLength(1);
|
|
3436
|
-
expect(screen.queryByText('Labels')).toBeNull();
|
|
3437
|
-
});
|
|
3438
|
-
|
|
3439
|
-
it('opens against the caller control and names the listbox by label', async () => {
|
|
3440
|
-
const user = userEvent.setup();
|
|
3441
|
-
render(
|
|
3442
|
-
<MultiSelector
|
|
3443
|
-
label="Labels"
|
|
3444
|
-
options={OPTIONS}
|
|
3445
|
-
value={['bug']}
|
|
3446
|
-
onChange={() => {}}
|
|
3447
|
-
renderTrigger={props => (
|
|
3448
|
-
<button type="button" {...props}>
|
|
3449
|
-
Pick labels
|
|
3450
|
-
</button>
|
|
3451
|
-
)}
|
|
3452
|
-
/>,
|
|
3453
|
-
);
|
|
3454
|
-
const opener = screen.getByRole('button', {name: 'Pick labels'});
|
|
3455
|
-
await user.click(opener);
|
|
3456
|
-
|
|
3457
|
-
expect(opener).toHaveAttribute('aria-expanded', 'true');
|
|
3458
|
-
const listbox = screen.getByRole('listbox', {name: 'Labels', ...h});
|
|
3459
|
-
expect(opener.getAttribute('aria-controls')).toBe(listbox.id);
|
|
3460
|
-
expect(screen.getAllByRole('option', h)).toHaveLength(3);
|
|
3461
|
-
expect(screen.getByRole('option', {name: 'Bug', ...h})).toHaveAttribute(
|
|
3462
|
-
'aria-selected',
|
|
3463
|
-
'true',
|
|
3464
|
-
);
|
|
3465
|
-
});
|
|
3466
|
-
|
|
3467
|
-
it('the listbox owns the keyboard once open and toggles on Enter', async () => {
|
|
3468
|
-
const user = userEvent.setup();
|
|
3469
|
-
const onChange = vi.fn();
|
|
3470
|
-
render(
|
|
3471
|
-
<MultiSelector
|
|
3472
|
-
label="Labels"
|
|
3473
|
-
options={OPTIONS}
|
|
3474
|
-
value={[]}
|
|
3475
|
-
onChange={onChange}
|
|
3476
|
-
renderTrigger={props => (
|
|
3477
|
-
<button type="button" {...props}>
|
|
3478
|
-
Pick labels
|
|
3479
|
-
</button>
|
|
3480
|
-
)}
|
|
3481
|
-
/>,
|
|
3482
|
-
);
|
|
3483
|
-
await user.click(screen.getByRole('button', {name: 'Pick labels'}));
|
|
3484
|
-
const listbox = screen.getByRole('listbox', h);
|
|
3485
|
-
expect(listbox).toHaveAttribute('tabindex', '0');
|
|
3486
|
-
await waitFor(() => expect(listbox).toHaveFocus());
|
|
3487
|
-
|
|
3488
|
-
// Opening by click highlights the first option; Enter toggles it.
|
|
3489
|
-
fireEvent.keyDown(listbox, {key: 'ArrowDown'});
|
|
3490
|
-
fireEvent.keyDown(listbox, {key: 'Enter'});
|
|
3491
|
-
expect(onChange).toHaveBeenCalledWith(['feature']);
|
|
3492
|
-
});
|
|
3493
|
-
|
|
3494
|
-
it('handleRef opens and closes the panel and onOpenChange reports it', async () => {
|
|
3495
|
-
const onOpenChange = vi.fn();
|
|
3496
|
-
function Harness() {
|
|
3497
|
-
const handleRef = useRef<MultiSelectorHandle>(null);
|
|
3498
|
-
return (
|
|
3499
|
-
<>
|
|
3500
|
-
<button type="button" onClick={() => handleRef.current?.open()}>
|
|
3501
|
-
open from outside
|
|
3502
|
-
</button>
|
|
3503
|
-
<button type="button" onClick={() => handleRef.current?.close()}>
|
|
3504
|
-
close from outside
|
|
3505
|
-
</button>
|
|
3506
|
-
<MultiSelector
|
|
3507
|
-
label="Labels"
|
|
3508
|
-
options={OPTIONS}
|
|
3509
|
-
value={[]}
|
|
3510
|
-
onChange={() => {}}
|
|
3511
|
-
handleRef={handleRef}
|
|
3512
|
-
onOpenChange={onOpenChange}
|
|
3513
|
-
renderTrigger={props => (
|
|
3514
|
-
<button type="button" {...props}>
|
|
3515
|
-
anchor
|
|
3516
|
-
</button>
|
|
3517
|
-
)}
|
|
3518
|
-
/>
|
|
3519
|
-
</>
|
|
3520
|
-
);
|
|
3521
|
-
}
|
|
3522
|
-
const user = userEvent.setup();
|
|
3523
|
-
render(<Harness />);
|
|
3524
|
-
const anchor = screen.getByRole('button', {name: 'anchor'});
|
|
3525
|
-
expect(anchor).toHaveAttribute('aria-expanded', 'false');
|
|
3526
|
-
expect(onOpenChange).not.toHaveBeenCalled();
|
|
3527
|
-
|
|
3528
|
-
await user.click(screen.getByRole('button', {name: 'open from outside'}));
|
|
3529
|
-
expect(anchor).toHaveAttribute('aria-expanded', 'true');
|
|
3530
|
-
expect(onOpenChange).toHaveBeenLastCalledWith(true);
|
|
3531
|
-
|
|
3532
|
-
await user.click(screen.getByRole('button', {name: 'close from outside'}));
|
|
3533
|
-
await waitFor(() => {
|
|
3534
|
-
expect(anchor).toHaveAttribute('aria-expanded', 'false');
|
|
3535
|
-
});
|
|
3536
|
-
expect(onOpenChange).toHaveBeenLastCalledWith(false);
|
|
3537
|
-
expect(onOpenChange).toHaveBeenCalledTimes(2);
|
|
3538
|
-
});
|
|
3539
|
-
|
|
3540
|
-
it('the handle does not open a disabled selector', () => {
|
|
3541
|
-
const handleRef = {current: null as MultiSelectorHandle | null};
|
|
3542
|
-
render(
|
|
3543
|
-
<MultiSelector
|
|
3544
|
-
label="Labels"
|
|
3545
|
-
options={OPTIONS}
|
|
3546
|
-
value={[]}
|
|
3547
|
-
onChange={() => {}}
|
|
3548
|
-
isDisabled
|
|
3549
|
-
handleRef={handleRef}
|
|
3550
|
-
renderTrigger={props => (
|
|
3551
|
-
<button type="button" {...props}>
|
|
3552
|
-
anchor
|
|
3553
|
-
</button>
|
|
3554
|
-
)}
|
|
3555
|
-
/>,
|
|
3556
|
-
);
|
|
3557
|
-
act(() => {
|
|
3558
|
-
handleRef.current?.open();
|
|
3559
|
-
});
|
|
3560
|
-
expect(handleRef.current?.isOpen()).toBe(false);
|
|
3561
|
-
expect(screen.getByRole('button', {name: 'anchor'})).toHaveAttribute(
|
|
3562
|
-
'aria-expanded',
|
|
3563
|
-
'false',
|
|
3564
|
-
);
|
|
3565
|
-
});
|
|
3566
|
-
});
|
|
3567
|
-
|
|
3568
|
-
describe('MultiSelector renderTrigger — read-only', () => {
|
|
3569
|
-
it("a read-only selector advertises no panel on the caller's control", () => {
|
|
3570
|
-
// spec:AST-011 FR4. A read-only selector has no surface to open, so a
|
|
3571
|
-
// control that says "collapsed, has popup" sends a screen-reader user
|
|
3572
|
-
// to press Enter on something that cannot respond — with nothing to
|
|
3573
|
-
// tell them the value is read-only rather than the control broken.
|
|
3574
|
-
const onOpen = vi.fn();
|
|
3575
|
-
render(
|
|
3576
|
-
<MultiSelector
|
|
3577
|
-
label="Labels"
|
|
3578
|
-
value={['a']}
|
|
3579
|
-
onChange={() => {}}
|
|
3580
|
-
isReadOnly
|
|
3581
|
-
options={[
|
|
3582
|
-
{value: 'a', label: 'Alpha'},
|
|
3583
|
-
{value: 'b', label: 'Beta'},
|
|
3584
|
-
]}
|
|
3585
|
-
renderTrigger={props => (
|
|
3586
|
-
<button type="button" {...props} onClick={onOpen}>
|
|
3587
|
-
Labels
|
|
3588
|
-
</button>
|
|
3589
|
-
)}
|
|
3590
|
-
/>,
|
|
3591
|
-
);
|
|
3592
|
-
|
|
3593
|
-
const trigger = screen.getByRole('button', {name: 'Labels'});
|
|
3594
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
3595
|
-
expect(trigger).not.toHaveAttribute('aria-haspopup');
|
|
3596
|
-
expect(trigger).not.toHaveAttribute('aria-controls');
|
|
3597
|
-
// Withholding the disclosure attributes stops the control lying about a
|
|
3598
|
-
// panel; this says why it does not open, which the field path conveys
|
|
3599
|
-
// through chrome the caller's control does not have.
|
|
3600
|
-
expect(trigger).toHaveAttribute('aria-readonly', 'true');
|
|
3601
|
-
});
|
|
3602
|
-
|
|
3603
|
-
it('still advertises the panel when the selector is not read-only', () => {
|
|
3604
|
-
render(
|
|
3605
|
-
<MultiSelector
|
|
3606
|
-
label="Labels"
|
|
3607
|
-
value={[]}
|
|
3608
|
-
onChange={() => {}}
|
|
3609
|
-
options={[{value: 'a', label: 'Alpha'}]}
|
|
3610
|
-
renderTrigger={props => (
|
|
3611
|
-
<button type="button" {...props}>
|
|
3612
|
-
Labels
|
|
3613
|
-
</button>
|
|
3614
|
-
)}
|
|
3615
|
-
/>,
|
|
3616
|
-
);
|
|
3617
|
-
|
|
3618
|
-
const trigger = screen.getByRole('button', {name: 'Labels'});
|
|
3619
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
|
|
3620
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
3621
|
-
expect(trigger).toHaveAttribute('aria-controls');
|
|
3622
|
-
});
|
|
3623
|
-
});
|
|
3624
|
-
|
|
3625
|
-
describe('MultiSelector renderTrigger — focus return', () => {
|
|
3626
|
-
it('returns focus to a control that takes no focus of its own', async () => {
|
|
3627
|
-
// A glyph is not keyboard-openable, but when it IS opened by pointer the
|
|
3628
|
-
// panel still has to give focus back somewhere the person recognises.
|
|
3629
|
-
// Without the fallback focus lands on the body and they lose their place.
|
|
3630
|
-
const user = userEvent.setup();
|
|
3631
|
-
render(
|
|
3632
|
-
<MultiSelector
|
|
3633
|
-
label="Labels"
|
|
3634
|
-
value={[]}
|
|
3635
|
-
onChange={() => {}}
|
|
3636
|
-
options={[
|
|
3637
|
-
{value: 'a', label: 'Alpha'},
|
|
3638
|
-
{value: 'b', label: 'Beta'},
|
|
3639
|
-
]}
|
|
3640
|
-
renderTrigger={props => (
|
|
3641
|
-
<span {...props} data-testid="glyph">
|
|
3642
|
-
#
|
|
3643
|
-
</span>
|
|
3644
|
-
)}
|
|
3645
|
-
/>,
|
|
3646
|
-
);
|
|
3647
|
-
|
|
3648
|
-
const glyph = screen.getByTestId('glyph');
|
|
3649
|
-
expect(glyph).toHaveAttribute('tabindex', '-1');
|
|
3650
|
-
|
|
3651
|
-
await user.click(glyph);
|
|
3652
|
-
await waitFor(() => expect(glyph).toHaveAttribute('aria-expanded', 'true'));
|
|
3653
|
-
await user.keyboard('{Escape}');
|
|
3654
|
-
await waitFor(() => expect(glyph).toHaveFocus());
|
|
3655
|
-
expect(document.body).not.toHaveFocus();
|
|
3656
|
-
});
|
|
3657
|
-
|
|
3658
|
-
it('leaves a real control\u2019s own focusability alone', () => {
|
|
3659
|
-
render(
|
|
3660
|
-
<MultiSelector
|
|
3661
|
-
label="Labels"
|
|
3662
|
-
value={[]}
|
|
3663
|
-
onChange={() => {}}
|
|
3664
|
-
options={[{value: 'a', label: 'Alpha'}]}
|
|
3665
|
-
renderTrigger={props => (
|
|
3666
|
-
<button type="button" {...props}>
|
|
3667
|
-
Labels
|
|
3668
|
-
</button>
|
|
3669
|
-
)}
|
|
3670
|
-
/>,
|
|
3671
|
-
);
|
|
3672
|
-
|
|
3673
|
-
expect(screen.getByRole('button', {name: 'Labels'})).not.toHaveAttribute(
|
|
3674
|
-
'tabindex',
|
|
3675
|
-
);
|
|
3676
|
-
});
|
|
3677
|
-
});
|
|
3678
|
-
|
|
3679
|
-
describe('MultiSelector hasCreate', () => {
|
|
3680
|
-
const OPTIONS = [
|
|
3681
|
-
{value: 'bug', label: 'Bug'},
|
|
3682
|
-
{value: 'feature', label: 'Feature'},
|
|
3683
|
-
{value: 'cafe', label: 'Café'},
|
|
3684
|
-
];
|
|
3685
|
-
|
|
3686
|
-
it('offers Create "<query>" first and reports the creation through onChange', async () => {
|
|
3687
|
-
const user = userEvent.setup();
|
|
3688
|
-
const onChange =
|
|
3689
|
-
vi.fn<(value: string[], change?: MultiSelectorChange) => void>();
|
|
3690
|
-
render(
|
|
3691
|
-
<MultiSelector
|
|
3692
|
-
label="Labels"
|
|
3693
|
-
options={OPTIONS}
|
|
3694
|
-
value={['bug']}
|
|
3695
|
-
onChange={onChange}
|
|
3696
|
-
hasSearch
|
|
3697
|
-
hasCreate
|
|
3698
|
-
/>,
|
|
3699
|
-
);
|
|
3700
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3701
|
-
const search = screen.getByRole('combobox', h);
|
|
3702
|
-
await user.type(search, 'Urgent');
|
|
3703
|
-
|
|
3704
|
-
const options = screen.getAllByRole('option', h);
|
|
3705
|
-
expect(options).toHaveLength(1);
|
|
3706
|
-
expect(options[0]).toHaveTextContent('Create "Urgent"');
|
|
3707
|
-
expect(options[0]).toHaveAttribute('aria-selected', 'false');
|
|
3708
|
-
expect(options[0].querySelector('input[type="checkbox"]')).toBeNull();
|
|
3709
|
-
expect(screen.queryByText('No results found')).toBeNull();
|
|
3710
|
-
|
|
3711
|
-
await user.click(options[0]);
|
|
3712
|
-
// One update: the query joins the value and the change says it is a
|
|
3713
|
-
// creation, so the caller adds the option and accepts the value together.
|
|
3714
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
3715
|
-
expect(onChange).toHaveBeenCalledWith(['bug', 'Urgent'], {
|
|
3716
|
-
type: 'create',
|
|
3717
|
-
query: 'Urgent',
|
|
3718
|
-
});
|
|
3719
|
-
// The filter is cleared so the option the caller adds is visible.
|
|
3720
|
-
expect(search).toHaveValue('');
|
|
3721
|
-
});
|
|
3722
|
-
|
|
3723
|
-
it('a plain toggle passes no descriptor, so a one-argument handler is unchanged', async () => {
|
|
3724
|
-
const user = userEvent.setup();
|
|
3725
|
-
const onChange = vi.fn();
|
|
3726
|
-
render(
|
|
3727
|
-
<MultiSelector
|
|
3728
|
-
label="Labels"
|
|
3729
|
-
options={OPTIONS}
|
|
3730
|
-
value={[]}
|
|
3731
|
-
onChange={onChange}
|
|
3732
|
-
hasSearch
|
|
3733
|
-
hasCreate
|
|
3734
|
-
/>,
|
|
3735
|
-
);
|
|
3736
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3737
|
-
await user.click(screen.getByRole('option', {name: 'Bug', ...h}));
|
|
3738
|
-
expect(onChange).toHaveBeenCalledWith(['bug']);
|
|
3739
|
-
expect(onChange.mock.calls[0]).toHaveLength(1);
|
|
3740
|
-
});
|
|
3741
|
-
|
|
3742
|
-
it('sits above partial matches and is skipped for a label the filter matches exactly', async () => {
|
|
3743
|
-
const user = userEvent.setup();
|
|
3744
|
-
render(
|
|
3745
|
-
<MultiSelector
|
|
3746
|
-
label="Labels"
|
|
3747
|
-
options={OPTIONS}
|
|
3748
|
-
value={[]}
|
|
3749
|
-
onChange={() => {}}
|
|
3750
|
-
hasSearch
|
|
3751
|
-
hasCreate
|
|
3752
|
-
/>,
|
|
3753
|
-
);
|
|
3754
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3755
|
-
const search = screen.getByRole('combobox', h);
|
|
3756
|
-
|
|
3757
|
-
await user.type(search, 'Bu');
|
|
3758
|
-
expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
|
|
3759
|
-
'Create "Bu"',
|
|
3760
|
-
'Bug',
|
|
3761
|
-
]);
|
|
3762
|
-
|
|
3763
|
-
await user.type(search, 'g');
|
|
3764
|
-
expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
|
|
3765
|
-
'Bug',
|
|
3766
|
-
]);
|
|
3767
|
-
|
|
3768
|
-
await user.clear(search);
|
|
3769
|
-
await user.type(search, 'bug');
|
|
3770
|
-
// Case-insensitive, like the filter: "bug" is Bug, so nothing to create.
|
|
3771
|
-
expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
|
|
3772
|
-
'Bug',
|
|
3773
|
-
]);
|
|
3774
|
-
|
|
3775
|
-
await user.clear(search);
|
|
3776
|
-
await user.type(search, 'CAFE');
|
|
3777
|
-
// The duplicate rule is the filter's rule: the filter cannot surface
|
|
3778
|
-
// Café for CAFE, so refusing to create would be a dead end.
|
|
3779
|
-
expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
|
|
3780
|
-
'Create "CAFE"',
|
|
3781
|
-
]);
|
|
3782
|
-
});
|
|
3783
|
-
|
|
3784
|
-
it('leads the select-all row and is not handed to renderOption', async () => {
|
|
3785
|
-
const user = userEvent.setup();
|
|
3786
|
-
const renderOption = vi.fn((option: {label?: string}) => (
|
|
3787
|
-
<span>{`row:${option.label}`}</span>
|
|
3788
|
-
));
|
|
3789
|
-
render(
|
|
3790
|
-
<MultiSelector
|
|
3791
|
-
label="Labels"
|
|
3792
|
-
options={OPTIONS}
|
|
3793
|
-
value={[]}
|
|
3794
|
-
onChange={() => {}}
|
|
3795
|
-
hasSearch
|
|
3796
|
-
hasSelectAll
|
|
3797
|
-
hasCreate
|
|
3798
|
-
renderOption={renderOption}
|
|
3799
|
-
/>,
|
|
3800
|
-
);
|
|
3801
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3802
|
-
await user.type(screen.getByRole('combobox', h), 'Bu');
|
|
3803
|
-
const options = screen.getAllByRole('option', h);
|
|
3804
|
-
expect(options[0]).toHaveTextContent('Create "Bu"');
|
|
3805
|
-
expect(options[1]).toHaveTextContent('Select all');
|
|
3806
|
-
expect(options[2]).toHaveTextContent('row:Bug');
|
|
3807
|
-
expect(
|
|
3808
|
-
renderOption.mock.calls.some(([option]) =>
|
|
3809
|
-
(option.label ?? '').startsWith('Create'),
|
|
3810
|
-
),
|
|
3811
|
-
).toBe(false);
|
|
3812
|
-
});
|
|
3813
|
-
|
|
3814
|
-
it('Enter with nothing highlighted commits the create row; ArrowDown reaches it', async () => {
|
|
3815
|
-
const user = userEvent.setup();
|
|
3816
|
-
const onChange = vi.fn();
|
|
3817
|
-
render(
|
|
3818
|
-
<MultiSelector
|
|
3819
|
-
label="Labels"
|
|
3820
|
-
options={OPTIONS}
|
|
3821
|
-
value={[]}
|
|
3822
|
-
onChange={onChange}
|
|
3823
|
-
hasSearch
|
|
3824
|
-
hasCreate
|
|
3825
|
-
/>,
|
|
3826
|
-
);
|
|
3827
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3828
|
-
const search = screen.getByRole('combobox', h);
|
|
3829
|
-
await user.type(search, 'Urgent');
|
|
3830
|
-
await user.keyboard('{Enter}');
|
|
3831
|
-
expect(onChange).toHaveBeenLastCalledWith(['Urgent'], {
|
|
3832
|
-
type: 'create',
|
|
3833
|
-
query: 'Urgent',
|
|
3834
|
-
});
|
|
3835
|
-
|
|
3836
|
-
await user.type(search, 'Later');
|
|
3837
|
-
fireEvent.keyDown(search, {key: 'ArrowDown'});
|
|
3838
|
-
const createRow = screen.getByRole('option', {
|
|
3839
|
-
name: 'Create "Later"',
|
|
3840
|
-
...h,
|
|
3841
|
-
});
|
|
3842
|
-
expect(search).toHaveAttribute('aria-activedescendant', createRow.id);
|
|
3843
|
-
fireEvent.keyDown(search, {key: 'Enter'});
|
|
3844
|
-
expect(onChange).toHaveBeenLastCalledWith(
|
|
3845
|
-
expect.arrayContaining(['Later']),
|
|
3846
|
-
{type: 'create', query: 'Later'},
|
|
3847
|
-
);
|
|
3848
|
-
});
|
|
3849
|
-
|
|
3850
|
-
it('offers nothing while isLoading, and Enter commits nothing', async () => {
|
|
3851
|
-
const user = userEvent.setup();
|
|
3852
|
-
const onChange = vi.fn();
|
|
3853
|
-
const {rerender} = render(
|
|
3854
|
-
<MultiSelector
|
|
3855
|
-
label="Labels"
|
|
3856
|
-
options={[]}
|
|
3857
|
-
value={[]}
|
|
3858
|
-
onChange={onChange}
|
|
3859
|
-
hasSearch
|
|
3860
|
-
hasCreate
|
|
3861
|
-
isLoading
|
|
3862
|
-
/>,
|
|
3863
|
-
);
|
|
3864
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3865
|
-
const search = screen.getByRole('combobox', h);
|
|
3866
|
-
await user.type(search, 'Bug');
|
|
3867
|
-
expect(screen.queryAllByRole('option', h)).toHaveLength(0);
|
|
3868
|
-
expect(politeRegion()?.textContent ?? '').toBe('');
|
|
3869
|
-
await user.keyboard('{Enter}');
|
|
3870
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
3871
|
-
|
|
3872
|
-
// The options land: Bug exists, so still no create row for "Bug" — the
|
|
3873
|
-
// duplicate the gate was there to prevent.
|
|
3874
|
-
rerender(
|
|
3875
|
-
<MultiSelector
|
|
3876
|
-
label="Labels"
|
|
3877
|
-
options={OPTIONS}
|
|
3878
|
-
value={[]}
|
|
3879
|
-
onChange={onChange}
|
|
3880
|
-
hasSearch
|
|
3881
|
-
hasCreate
|
|
3882
|
-
/>,
|
|
3883
|
-
);
|
|
3884
|
-
expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
|
|
3885
|
-
'Bug',
|
|
3886
|
-
]);
|
|
3887
|
-
});
|
|
3888
|
-
|
|
3889
|
-
it('announces the create row, not "No results found", when it is the only result', async () => {
|
|
3890
|
-
const user = userEvent.setup();
|
|
3891
|
-
render(
|
|
3892
|
-
<MultiSelector
|
|
3893
|
-
label="Labels"
|
|
3894
|
-
options={OPTIONS}
|
|
3895
|
-
value={[]}
|
|
3896
|
-
onChange={() => {}}
|
|
3897
|
-
hasSearch
|
|
3898
|
-
hasCreate
|
|
3899
|
-
/>,
|
|
3900
|
-
);
|
|
3901
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3902
|
-
await enterQuery(user, screen.getByRole('combobox', h), 'Urgent');
|
|
3903
|
-
// The empty-state message is role="presentation", so the live region is
|
|
3904
|
-
// what a screen reader gets — it must say what is on screen: a row.
|
|
3905
|
-
await waitFor(() => {
|
|
3906
|
-
expect(politeRegion()?.textContent).toBe('Create "Urgent"');
|
|
3907
|
-
});
|
|
3908
|
-
});
|
|
3909
|
-
|
|
3910
|
-
it('warns in development and offers nothing when hasCreate is set without hasSearch', async () => {
|
|
3911
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
3912
|
-
try {
|
|
3913
|
-
const user = userEvent.setup();
|
|
3914
|
-
render(
|
|
3915
|
-
<MultiSelector
|
|
3916
|
-
label="Labels"
|
|
3917
|
-
options={OPTIONS}
|
|
3918
|
-
value={[]}
|
|
3919
|
-
onChange={() => {}}
|
|
3920
|
-
hasCreate
|
|
3921
|
-
/>,
|
|
3922
|
-
);
|
|
3923
|
-
expect(warn).toHaveBeenCalledWith(
|
|
3924
|
-
expect.stringMatching(/MultiSelector[\s\S]*hasSearch/),
|
|
3925
|
-
);
|
|
3926
|
-
await user.click(screen.getByRole('combobox', {name: 'Labels'}));
|
|
3927
|
-
// No search input, no query, no create row: the three real options only.
|
|
3928
|
-
expect(
|
|
3929
|
-
screen.queryByRole('combobox', {name: /search/i, ...h}),
|
|
3930
|
-
).toBeNull();
|
|
3931
|
-
expect(screen.getAllByRole('option', h)).toHaveLength(3);
|
|
3932
|
-
} finally {
|
|
3933
|
-
warn.mockRestore();
|
|
3934
|
-
}
|
|
3935
|
-
});
|
|
3936
|
-
|
|
3937
|
-
it('is off by default', async () => {
|
|
3938
|
-
const user = userEvent.setup();
|
|
3939
|
-
render(
|
|
3940
|
-
<MultiSelector
|
|
3941
|
-
label="Labels"
|
|
3942
|
-
options={OPTIONS}
|
|
3943
|
-
value={[]}
|
|
3944
|
-
onChange={() => {}}
|
|
3945
|
-
hasSearch
|
|
3946
|
-
/>,
|
|
3947
|
-
);
|
|
3948
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
3949
|
-
await user.type(screen.getByRole('combobox', h), 'Urgent');
|
|
3950
|
-
expect(screen.queryAllByRole('option', h)).toHaveLength(0);
|
|
3951
|
-
expect(screen.getByText('No results found')).toBeInTheDocument();
|
|
3952
|
-
});
|
|
3953
|
-
});
|
|
3954
|
-
|
|
3955
|
-
describe('MultiSelector option actions (grid)', () => {
|
|
3956
|
-
const edit = vi.fn();
|
|
3957
|
-
const OPTIONS = [
|
|
3958
|
-
{
|
|
3959
|
-
value: 'bug',
|
|
3960
|
-
label: 'Bug',
|
|
3961
|
-
action: (
|
|
3962
|
-
<button
|
|
3963
|
-
type="button"
|
|
3964
|
-
onClick={() => {
|
|
3965
|
-
edit('bug');
|
|
3966
|
-
}}>
|
|
3967
|
-
Edit Bug
|
|
3968
|
-
</button>
|
|
3969
|
-
),
|
|
3970
|
-
},
|
|
3971
|
-
{value: 'feature', label: 'Feature'},
|
|
3972
|
-
{
|
|
3973
|
-
value: 'docs',
|
|
3974
|
-
label: 'Docs',
|
|
3975
|
-
disabled: true,
|
|
3976
|
-
action: (
|
|
3977
|
-
<button
|
|
3978
|
-
type="button"
|
|
3979
|
-
onClick={() => {
|
|
3980
|
-
edit('docs');
|
|
3981
|
-
}}>
|
|
3982
|
-
Edit Docs
|
|
3983
|
-
</button>
|
|
3984
|
-
),
|
|
3985
|
-
},
|
|
3986
|
-
];
|
|
3987
|
-
beforeEach(() => {
|
|
3988
|
-
edit.mockClear();
|
|
3989
|
-
});
|
|
3990
|
-
|
|
3991
|
-
it('stays a listbox when no option carries an action, and action: null is none', async () => {
|
|
3992
|
-
const user = userEvent.setup();
|
|
3993
|
-
render(
|
|
3994
|
-
<MultiSelector
|
|
3995
|
-
label="Labels"
|
|
3996
|
-
options={[
|
|
3997
|
-
{value: 'bug', label: 'Bug', action: null},
|
|
3998
|
-
{value: 'feature', label: 'Feature'},
|
|
3999
|
-
]}
|
|
4000
|
-
value={[]}
|
|
4001
|
-
onChange={() => {}}
|
|
4002
|
-
/>,
|
|
4003
|
-
);
|
|
4004
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4005
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
|
|
4006
|
-
await user.click(trigger);
|
|
4007
|
-
expect(screen.getByRole('listbox', h)).toBeInTheDocument();
|
|
4008
|
-
expect(screen.queryByRole('grid', h)).toBeNull();
|
|
4009
|
-
expect(screen.getAllByRole('option', h)).toHaveLength(2);
|
|
4010
|
-
});
|
|
4011
|
-
|
|
4012
|
-
it('is a grid of rows with exactly two gridcells once an option carries an action', async () => {
|
|
4013
|
-
const user = userEvent.setup();
|
|
4014
|
-
render(
|
|
4015
|
-
<MultiSelector
|
|
4016
|
-
label="Labels"
|
|
4017
|
-
options={OPTIONS}
|
|
4018
|
-
value={['bug']}
|
|
4019
|
-
onChange={() => {}}
|
|
4020
|
-
hasSelectAll
|
|
4021
|
-
/>,
|
|
4022
|
-
);
|
|
4023
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4024
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'grid');
|
|
4025
|
-
await user.click(trigger);
|
|
4026
|
-
|
|
4027
|
-
const grid = screen.getByRole('grid', h);
|
|
4028
|
-
expect(screen.queryByRole('listbox', h)).toBeNull();
|
|
4029
|
-
expect(screen.queryByRole('option', h)).toBeNull();
|
|
4030
|
-
expect(grid).toHaveAttribute('aria-multiselectable', 'true');
|
|
4031
|
-
expect(trigger.getAttribute('aria-controls')).toBe(grid.id);
|
|
4032
|
-
|
|
4033
|
-
const rows = screen.getAllByRole('row', h);
|
|
4034
|
-
// select-all + 3 options
|
|
4035
|
-
expect(rows).toHaveLength(4);
|
|
4036
|
-
for (const row of rows) {
|
|
4037
|
-
const cells = Array.from(row.children);
|
|
4038
|
-
expect(cells).toHaveLength(2);
|
|
4039
|
-
expect(cells[0]).toHaveAttribute('role', 'gridcell');
|
|
4040
|
-
expect(cells[1]).toHaveAttribute('role', 'gridcell');
|
|
4041
|
-
}
|
|
4042
|
-
// Every exposed child of the grid is a row.
|
|
4043
|
-
expect(
|
|
4044
|
-
Array.from(grid.children).every(c => c.getAttribute('role') === 'row'),
|
|
4045
|
-
).toBe(true);
|
|
4046
|
-
|
|
4047
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4048
|
-
expect(bug).toHaveAttribute('aria-selected', 'true');
|
|
4049
|
-
expect(bug.children[1]).toContainElement(
|
|
4050
|
-
screen.getByRole('button', {name: 'Edit Bug', ...h}),
|
|
4051
|
-
);
|
|
4052
|
-
// Rows without an action, and the select-all row, have an empty cell.
|
|
4053
|
-
expect(
|
|
4054
|
-
screen.getByRole('row', {name: 'Feature', ...h}).children[1],
|
|
4055
|
-
).toBeEmptyDOMElement();
|
|
4056
|
-
expect(rows[0].children[1]).toBeEmptyDOMElement();
|
|
4057
|
-
// The row's name is the option; the control keeps its own.
|
|
4058
|
-
expect(bug).toHaveAccessibleName('Bug');
|
|
4059
|
-
});
|
|
4060
|
-
|
|
4061
|
-
it('latches: a query that filters the actioned option out keeps the grid', async () => {
|
|
4062
|
-
const user = userEvent.setup();
|
|
4063
|
-
const {rerender} = render(
|
|
4064
|
-
<MultiSelector
|
|
4065
|
-
label="Labels"
|
|
4066
|
-
options={OPTIONS}
|
|
4067
|
-
value={[]}
|
|
4068
|
-
onChange={() => {}}
|
|
4069
|
-
hasSearch
|
|
4070
|
-
/>,
|
|
4071
|
-
);
|
|
4072
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
4073
|
-
expect(screen.getByRole('grid', h)).toBeInTheDocument();
|
|
4074
|
-
await user.type(screen.getByRole('combobox', h), 'Feat');
|
|
4075
|
-
expect(screen.getAllByRole('row', h).map(r => r.textContent)).toEqual([
|
|
4076
|
-
'Feature',
|
|
4077
|
-
]);
|
|
4078
|
-
expect(screen.getByRole('grid', h)).toBeInTheDocument();
|
|
4079
|
-
// The action removed on a later render: still a grid.
|
|
4080
|
-
rerender(
|
|
4081
|
-
<MultiSelector
|
|
4082
|
-
label="Labels"
|
|
4083
|
-
options={[{value: 'feature', label: 'Feature'}]}
|
|
4084
|
-
value={[]}
|
|
4085
|
-
onChange={() => {}}
|
|
4086
|
-
hasSearch
|
|
4087
|
-
/>,
|
|
4088
|
-
);
|
|
4089
|
-
expect(screen.getByRole('grid', h)).toBeInTheDocument();
|
|
4090
|
-
});
|
|
4091
|
-
|
|
4092
|
-
it('becomes a grid when options arrive after loading, inside a section too', async () => {
|
|
4093
|
-
const user = userEvent.setup();
|
|
4094
|
-
const {rerender} = render(
|
|
4095
|
-
<MultiSelector
|
|
4096
|
-
label="Labels"
|
|
4097
|
-
options={[]}
|
|
4098
|
-
value={[]}
|
|
4099
|
-
onChange={() => {}}
|
|
4100
|
-
isLoading
|
|
4101
|
-
/>,
|
|
4102
|
-
);
|
|
4103
|
-
expect(screen.getByRole('combobox', {name: 'Labels'})).toHaveAttribute(
|
|
4104
|
-
'aria-haspopup',
|
|
4105
|
-
'listbox',
|
|
4106
|
-
);
|
|
4107
|
-
rerender(
|
|
4108
|
-
<MultiSelector
|
|
4109
|
-
label="Labels"
|
|
4110
|
-
options={[
|
|
4111
|
-
{
|
|
4112
|
-
type: 'section',
|
|
4113
|
-
title: 'Type',
|
|
4114
|
-
options: OPTIONS,
|
|
4115
|
-
},
|
|
4116
|
-
]}
|
|
4117
|
-
value={[]}
|
|
4118
|
-
onChange={() => {}}
|
|
4119
|
-
/>,
|
|
4120
|
-
);
|
|
4121
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4122
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'grid');
|
|
4123
|
-
await user.click(trigger);
|
|
4124
|
-
const group = screen.getByRole('rowgroup', {name: 'Type', ...h});
|
|
4125
|
-
expect(screen.queryByRole('group', h)).toBeNull();
|
|
4126
|
-
expect(group.querySelectorAll('[role="row"]')).toHaveLength(3);
|
|
4127
|
-
});
|
|
4128
|
-
|
|
4129
|
-
it('pressing the option cell toggles; pressing the action fires only the action', async () => {
|
|
4130
|
-
const user = userEvent.setup();
|
|
4131
|
-
const onChange = vi.fn();
|
|
4132
|
-
render(
|
|
4133
|
-
<MultiSelector
|
|
4134
|
-
label="Labels"
|
|
4135
|
-
options={OPTIONS}
|
|
4136
|
-
value={[]}
|
|
4137
|
-
onChange={onChange}
|
|
4138
|
-
/>,
|
|
4139
|
-
);
|
|
4140
|
-
await user.click(screen.getByRole('combobox', {name: 'Labels'}));
|
|
4141
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4142
|
-
await user.click(bug.children[0]);
|
|
4143
|
-
expect(onChange).toHaveBeenCalledWith(['bug']);
|
|
4144
|
-
|
|
4145
|
-
onChange.mockClear();
|
|
4146
|
-
await user.click(screen.getByRole('button', {name: 'Edit Bug', ...h}));
|
|
4147
|
-
expect(edit).toHaveBeenCalledWith('bug');
|
|
4148
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
4149
|
-
// The panel stays open.
|
|
4150
|
-
expect(screen.getByRole('grid', h)).toBeInTheDocument();
|
|
4151
|
-
|
|
4152
|
-
// A disabled option's action is the caller's control, still live.
|
|
4153
|
-
await user.click(screen.getByRole('button', {name: 'Edit Docs', ...h}));
|
|
4154
|
-
expect(edit).toHaveBeenCalledWith('docs');
|
|
4155
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
4156
|
-
});
|
|
4157
|
-
|
|
4158
|
-
it('draws the standard focus ring on the active action cell, from the focus tokens', async () => {
|
|
4159
|
-
// jsdom resolves no custom property, so the ring is read from StyleX
|
|
4160
|
-
// classes: a class name hashes one property and its value, and the same
|
|
4161
|
-
// declaration yields the same class wherever it is written.
|
|
4162
|
-
const ring = stylex.create({
|
|
4163
|
-
active: {
|
|
4164
|
-
outlineWidth: focusVars['--focus-outline-width'],
|
|
4165
|
-
outlineStyle: focusVars['--focus-outline-style'],
|
|
4166
|
-
outlineColor: focusVars['--focus-outline-color'],
|
|
4167
|
-
outlineOffset: focusVars['--focus-outline-offset'],
|
|
4168
|
-
},
|
|
4169
|
-
});
|
|
4170
|
-
const ringClasses = stylex
|
|
4171
|
-
.props(ring.active)
|
|
4172
|
-
.className!.split(' ')
|
|
4173
|
-
.filter(token => /^x[a-z0-9]+$/.test(token));
|
|
4174
|
-
expect(ringClasses).toHaveLength(4);
|
|
4175
|
-
|
|
4176
|
-
const user = userEvent.setup();
|
|
4177
|
-
render(
|
|
4178
|
-
<MultiSelector
|
|
4179
|
-
label="Labels"
|
|
4180
|
-
options={OPTIONS}
|
|
4181
|
-
value={[]}
|
|
4182
|
-
onChange={() => {}}
|
|
4183
|
-
/>,
|
|
4184
|
-
);
|
|
4185
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4186
|
-
await user.click(trigger);
|
|
4187
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4188
|
-
const actionCell = bug.children[1] as HTMLElement;
|
|
4189
|
-
for (const token of ringClasses) {
|
|
4190
|
-
expect(actionCell).not.toHaveClass(token);
|
|
4191
|
-
}
|
|
4192
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4193
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
|
|
4194
|
-
for (const token of ringClasses) {
|
|
4195
|
-
expect(actionCell).toHaveClass(token);
|
|
4196
|
-
}
|
|
4197
|
-
});
|
|
4198
|
-
|
|
4199
|
-
it('draws no focus ring on an action cell highlighted after pointer input, until a key moves the highlight', async () => {
|
|
4200
|
-
const ring = stylex.create({
|
|
4201
|
-
active: {
|
|
4202
|
-
outlineWidth: focusVars['--focus-outline-width'],
|
|
4203
|
-
outlineStyle: focusVars['--focus-outline-style'],
|
|
4204
|
-
outlineColor: focusVars['--focus-outline-color'],
|
|
4205
|
-
outlineOffset: focusVars['--focus-outline-offset'],
|
|
4206
|
-
},
|
|
4207
|
-
});
|
|
4208
|
-
const ringClasses = stylex
|
|
4209
|
-
.props(ring.active)
|
|
4210
|
-
.className!.split(' ')
|
|
4211
|
-
.filter(token => /^x[a-z0-9]+$/.test(token));
|
|
4212
|
-
expect(ringClasses).toHaveLength(4);
|
|
4213
|
-
|
|
4214
|
-
const user = userEvent.setup();
|
|
4215
|
-
render(
|
|
4216
|
-
<MultiSelector
|
|
4217
|
-
label="Labels"
|
|
4218
|
-
options={OPTIONS}
|
|
4219
|
-
value={[]}
|
|
4220
|
-
onChange={() => {}}
|
|
4221
|
-
/>,
|
|
4222
|
-
);
|
|
4223
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4224
|
-
await user.click(trigger);
|
|
4225
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4226
|
-
const actionCell = bug.children[1] as HTMLElement;
|
|
4227
|
-
fireEvent.mouseEnter(actionCell);
|
|
4228
|
-
// The hover moved the highlight onto the action cell, without the ring.
|
|
4229
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
|
|
4230
|
-
for (const token of ringClasses) {
|
|
4231
|
-
expect(actionCell).not.toHaveClass(token);
|
|
4232
|
-
}
|
|
4233
|
-
// Keyboard navigation back onto the cell draws it.
|
|
4234
|
-
fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
|
|
4235
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4236
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
|
|
4237
|
-
for (const token of ringClasses) {
|
|
4238
|
-
expect(actionCell).toHaveClass(token);
|
|
4239
|
-
}
|
|
4240
|
-
});
|
|
4241
|
-
|
|
4242
|
-
describe('ring under mixed input', () => {
|
|
4243
|
-
const ringClasses = () => {
|
|
4244
|
-
const ring = stylex.create({
|
|
4245
|
-
active: {
|
|
4246
|
-
outlineWidth: focusVars['--focus-outline-width'],
|
|
4247
|
-
outlineStyle: focusVars['--focus-outline-style'],
|
|
4248
|
-
outlineColor: focusVars['--focus-outline-color'],
|
|
4249
|
-
outlineOffset: focusVars['--focus-outline-offset'],
|
|
4250
|
-
},
|
|
4251
|
-
});
|
|
4252
|
-
return stylex
|
|
4253
|
-
.props(ring.active)
|
|
4254
|
-
.className!.split(' ')
|
|
4255
|
-
.filter(token => /^x[a-z0-9]+$/.test(token));
|
|
4256
|
-
};
|
|
4257
|
-
const hasRing = (cell: HTMLElement) =>
|
|
4258
|
-
ringClasses().every(token => cell.classList.contains(token));
|
|
4259
|
-
|
|
4260
|
-
async function openWithKeyboardOnBugAction() {
|
|
4261
|
-
const user = userEvent.setup();
|
|
4262
|
-
render(
|
|
4263
|
-
<MultiSelector
|
|
4264
|
-
label="Labels"
|
|
4265
|
-
options={OPTIONS}
|
|
4266
|
-
value={[]}
|
|
4267
|
-
onChange={() => {}}
|
|
4268
|
-
/>,
|
|
4269
|
-
);
|
|
4270
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4271
|
-
await user.click(trigger);
|
|
4272
|
-
const row = (name: string) => screen.getByRole('row', {name, ...h});
|
|
4273
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4274
|
-
const bugAction = row('Bug').children[1] as HTMLElement;
|
|
4275
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
|
|
4276
|
-
expect(hasRing(bugAction)).toBe(true);
|
|
4277
|
-
return {trigger, row, bugAction};
|
|
4278
|
-
}
|
|
4279
|
-
|
|
4280
|
-
it('keeps the ring when the pointer crosses a disabled option the highlight ignores', async () => {
|
|
4281
|
-
const {trigger, row, bugAction} = await openWithKeyboardOnBugAction();
|
|
4282
|
-
fireEvent.mouseEnter(row('Docs').children[0]);
|
|
4283
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
|
|
4284
|
-
expect(hasRing(bugAction)).toBe(true);
|
|
4285
|
-
});
|
|
4286
|
-
|
|
4287
|
-
it('follows the keyboard modality when a hover moves the highlight to another action', async () => {
|
|
4288
|
-
const {trigger, row} = await openWithKeyboardOnBugAction();
|
|
4289
|
-
const docsAction = row('Docs').children[1] as HTMLElement;
|
|
4290
|
-
fireEvent.mouseEnter(docsAction);
|
|
4291
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', docsAction.id);
|
|
4292
|
-
expect(hasRing(docsAction)).toBe(true);
|
|
4293
|
-
});
|
|
4294
|
-
|
|
4295
|
-
it('draws no ring for a modifier chord after pointer input', async () => {
|
|
4296
|
-
const user = userEvent.setup();
|
|
4297
|
-
render(
|
|
4298
|
-
<MultiSelector
|
|
4299
|
-
label="Labels"
|
|
4300
|
-
options={OPTIONS}
|
|
4301
|
-
value={[]}
|
|
4302
|
-
onChange={() => {}}
|
|
4303
|
-
/>,
|
|
4304
|
-
);
|
|
4305
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4306
|
-
await user.click(trigger);
|
|
4307
|
-
const bugAction = screen.getByRole('row', {name: 'Bug', ...h})
|
|
4308
|
-
.children[1] as HTMLElement;
|
|
4309
|
-
fireEvent.mouseEnter(bugAction);
|
|
4310
|
-
expect(hasRing(bugAction)).toBe(false);
|
|
4311
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight', ctrlKey: true});
|
|
4312
|
-
expect(hasRing(bugAction)).toBe(false);
|
|
4313
|
-
});
|
|
4314
|
-
});
|
|
4315
|
-
|
|
4316
|
-
it('the inline-end arrow reaches the action cell, Enter fires it, inline-start returns', async () => {
|
|
4317
|
-
const user = userEvent.setup();
|
|
4318
|
-
const onChange = vi.fn();
|
|
4319
|
-
render(
|
|
4320
|
-
<MultiSelector
|
|
4321
|
-
label="Labels"
|
|
4322
|
-
options={OPTIONS}
|
|
4323
|
-
value={[]}
|
|
4324
|
-
onChange={onChange}
|
|
4325
|
-
/>,
|
|
4326
|
-
);
|
|
4327
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4328
|
-
await user.click(trigger);
|
|
4329
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4330
|
-
// Click-open highlights the first row, on its option cell.
|
|
4331
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
|
|
4332
|
-
|
|
4333
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4334
|
-
expect(trigger).toHaveAttribute(
|
|
4335
|
-
'aria-activedescendant',
|
|
4336
|
-
bug.children[1].id,
|
|
4337
|
-
);
|
|
4338
|
-
fireEvent.keyDown(trigger, {key: 'Enter'});
|
|
4339
|
-
expect(edit).toHaveBeenCalledWith('bug');
|
|
4340
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
4341
|
-
// The highlight stays where it was.
|
|
4342
|
-
expect(trigger).toHaveAttribute(
|
|
4343
|
-
'aria-activedescendant',
|
|
4344
|
-
bug.children[1].id,
|
|
4345
|
-
);
|
|
4346
|
-
|
|
4347
|
-
fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
|
|
4348
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
|
|
4349
|
-
fireEvent.keyDown(trigger, {key: 'Enter'});
|
|
4350
|
-
expect(onChange).toHaveBeenCalledWith(['bug']);
|
|
4351
|
-
|
|
4352
|
-
// A row without an action: the inline-end arrow is a no-op there, and
|
|
4353
|
-
// Down from the action cell lands on the next row's option cell.
|
|
4354
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4355
|
-
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
|
|
4356
|
-
const feature = screen.getByRole('row', {name: 'Feature', ...h});
|
|
4357
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', feature.id);
|
|
4358
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4359
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', feature.id);
|
|
4360
|
-
});
|
|
4361
|
-
|
|
4362
|
-
it('follows visual direction under RTL', async () => {
|
|
4363
|
-
const user = userEvent.setup();
|
|
4364
|
-
const original = window.getComputedStyle;
|
|
4365
|
-
const spy = vi
|
|
4366
|
-
.spyOn(window, 'getComputedStyle')
|
|
4367
|
-
.mockImplementation((el: Element, pseudo?: string | null) => {
|
|
4368
|
-
const style = original.call(window, el, pseudo);
|
|
4369
|
-
Object.defineProperty(style, 'direction', {value: 'rtl'});
|
|
4370
|
-
return style;
|
|
4371
|
-
});
|
|
4372
|
-
try {
|
|
4373
|
-
render(
|
|
4374
|
-
<MultiSelector
|
|
4375
|
-
label="Labels"
|
|
4376
|
-
options={OPTIONS}
|
|
4377
|
-
value={[]}
|
|
4378
|
-
onChange={() => {}}
|
|
4379
|
-
/>,
|
|
4380
|
-
);
|
|
4381
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4382
|
-
await user.click(trigger);
|
|
4383
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4384
|
-
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
|
|
4385
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
|
|
4386
|
-
fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
|
|
4387
|
-
expect(trigger).toHaveAttribute(
|
|
4388
|
-
'aria-activedescendant',
|
|
4389
|
-
bug.children[1].id,
|
|
4390
|
-
);
|
|
4391
|
-
} finally {
|
|
4392
|
-
spy.mockRestore();
|
|
4393
|
-
}
|
|
4394
|
-
});
|
|
4395
|
-
|
|
4396
|
-
it('with search, Left/Right move the caret until a row is highlighted', async () => {
|
|
4397
|
-
const user = userEvent.setup();
|
|
4398
|
-
render(
|
|
4399
|
-
<MultiSelector
|
|
4400
|
-
label="Labels"
|
|
4401
|
-
options={OPTIONS}
|
|
4402
|
-
value={[]}
|
|
4403
|
-
onChange={() => {}}
|
|
4404
|
-
hasSearch
|
|
4405
|
-
/>,
|
|
4406
|
-
);
|
|
4407
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
4408
|
-
const search = screen.getByRole('combobox', h);
|
|
4409
|
-
await user.type(search, 'Bu');
|
|
4410
|
-
// No row highlighted: the arrow is the input's.
|
|
4411
|
-
const right = fireEvent.keyDown(search, {key: 'ArrowRight'});
|
|
4412
|
-
expect(right).toBe(true);
|
|
4413
|
-
expect(search).not.toHaveAttribute('aria-activedescendant');
|
|
4414
|
-
|
|
4415
|
-
fireEvent.keyDown(search, {key: 'ArrowDown'});
|
|
4416
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4417
|
-
expect(search).toHaveAttribute('aria-activedescendant', bug.id);
|
|
4418
|
-
const moved = fireEvent.keyDown(search, {key: 'ArrowRight'});
|
|
4419
|
-
expect(moved).toBe(false);
|
|
4420
|
-
expect(search).toHaveAttribute('aria-activedescendant', bug.children[1].id);
|
|
4421
|
-
fireEvent.keyDown(search, {key: 'Enter'});
|
|
4422
|
-
expect(edit).toHaveBeenCalledWith('bug');
|
|
4423
|
-
expect(search).toHaveValue('Bu');
|
|
4424
|
-
});
|
|
4425
|
-
|
|
4426
|
-
it('hovering the action cell highlights that row on its action cell', async () => {
|
|
4427
|
-
const user = userEvent.setup();
|
|
4428
|
-
render(
|
|
4429
|
-
<MultiSelector
|
|
4430
|
-
label="Labels"
|
|
4431
|
-
options={OPTIONS}
|
|
4432
|
-
value={[]}
|
|
4433
|
-
onChange={() => {}}
|
|
4434
|
-
/>,
|
|
4435
|
-
);
|
|
4436
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4437
|
-
await user.click(trigger);
|
|
4438
|
-
const bug = screen.getByRole('row', {name: 'Bug', ...h});
|
|
4439
|
-
const feature = screen.getByRole('row', {name: 'Feature', ...h});
|
|
4440
|
-
fireEvent.mouseEnter(feature.children[0]);
|
|
4441
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', feature.id);
|
|
4442
|
-
fireEvent.mouseEnter(bug.children[1]);
|
|
4443
|
-
expect(trigger).toHaveAttribute(
|
|
4444
|
-
'aria-activedescendant',
|
|
4445
|
-
bug.children[1].id,
|
|
4446
|
-
);
|
|
4447
|
-
fireEvent.mouseEnter(bug.children[0]);
|
|
4448
|
-
expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
|
|
4449
|
-
});
|
|
4450
|
-
|
|
4451
|
-
it('Tab closes the panel as before: no action control is a stop inside it', async () => {
|
|
4452
|
-
const user = userEvent.setup();
|
|
4453
|
-
render(
|
|
4454
|
-
<MultiSelector
|
|
4455
|
-
label="Labels"
|
|
4456
|
-
options={OPTIONS}
|
|
4457
|
-
value={[]}
|
|
4458
|
-
onChange={() => {}}
|
|
4459
|
-
/>,
|
|
4460
|
-
);
|
|
4461
|
-
const trigger = screen.getByRole('combobox', {name: 'Labels'});
|
|
4462
|
-
await user.click(trigger);
|
|
4463
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
4464
|
-
fireEvent.keyDown(trigger, {key: 'Tab'});
|
|
4465
|
-
await waitFor(() => {
|
|
4466
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
4467
|
-
});
|
|
4468
|
-
});
|
|
4469
|
-
|
|
4470
|
-
it('warns in development about an action control with no accessible name', async () => {
|
|
4471
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
4472
|
-
try {
|
|
4473
|
-
const user = userEvent.setup();
|
|
4474
|
-
render(
|
|
4475
|
-
<MultiSelector
|
|
4476
|
-
label="Labels"
|
|
4477
|
-
options={[
|
|
4478
|
-
{value: 'bug', label: 'Bug', action: <button type="button" />},
|
|
4479
|
-
]}
|
|
4480
|
-
value={[]}
|
|
4481
|
-
onChange={() => {}}
|
|
4482
|
-
/>,
|
|
4483
|
-
);
|
|
4484
|
-
await user.click(screen.getByRole('combobox', {name: 'Labels'}));
|
|
4485
|
-
expect(warn).toHaveBeenCalledWith(
|
|
4486
|
-
expect.stringMatching(/MultiSelector[\s\S]*accessible name/),
|
|
4487
|
-
);
|
|
4488
|
-
} finally {
|
|
4489
|
-
warn.mockRestore();
|
|
4490
|
-
}
|
|
4491
|
-
});
|
|
4492
|
-
});
|
|
4493
|
-
|
|
4494
|
-
describe('MultiSelector create row inside a grid', () => {
|
|
4495
|
-
it('is a row with exactly two gridcells, the action cell empty, and the arrow stays on the option', async () => {
|
|
4496
|
-
// spec:AST-058 FR3: in a grid every row has exactly two cells, and a
|
|
4497
|
-
// system-minted row is no exception. The create row has no control of
|
|
4498
|
-
// its own, so its action cell is present and empty, and the inline-end
|
|
4499
|
-
// arrow has nowhere to go.
|
|
4500
|
-
const user = userEvent.setup();
|
|
4501
|
-
const onChange =
|
|
4502
|
-
vi.fn<(value: string[], change?: MultiSelectorChange) => void>();
|
|
4503
|
-
render(
|
|
4504
|
-
<MultiSelector
|
|
4505
|
-
label="Labels"
|
|
4506
|
-
options={[
|
|
4507
|
-
{
|
|
4508
|
-
value: 'bug',
|
|
4509
|
-
label: 'Bug',
|
|
4510
|
-
action: <button type="button">Edit Bug</button>,
|
|
4511
|
-
},
|
|
4512
|
-
{value: 'feature', label: 'Feature'},
|
|
4513
|
-
]}
|
|
4514
|
-
value={[]}
|
|
4515
|
-
onChange={onChange}
|
|
4516
|
-
hasSearch
|
|
4517
|
-
hasCreate
|
|
4518
|
-
/>,
|
|
4519
|
-
);
|
|
4520
|
-
await user.click(screen.getByRole('button', {name: 'Labels'}));
|
|
4521
|
-
const search = screen.getByRole('combobox', {hidden: true});
|
|
4522
|
-
await user.type(search, 'Urgent');
|
|
4523
|
-
|
|
4524
|
-
const rows = screen.getAllByRole('row', {hidden: true});
|
|
4525
|
-
expect(rows).toHaveLength(1);
|
|
4526
|
-
const create = rows[0];
|
|
4527
|
-
expect(create).toHaveTextContent('Create "Urgent"');
|
|
4528
|
-
const cells = Array.from(create.children).filter(
|
|
4529
|
-
c => c.getAttribute('role') === 'gridcell',
|
|
4530
|
-
);
|
|
4531
|
-
expect(cells).toHaveLength(2);
|
|
4532
|
-
expect(cells[1]).toBeEmptyDOMElement();
|
|
4533
|
-
|
|
4534
|
-
await user.keyboard('{ArrowDown}{ArrowRight}{Enter}');
|
|
4535
|
-
expect(onChange).toHaveBeenCalledWith(['Urgent'], {
|
|
4536
|
-
type: 'create',
|
|
4537
|
-
query: 'Urgent',
|
|
4538
|
-
});
|
|
4539
|
-
});
|
|
4540
|
-
});
|
|
4541
|
-
|
|
4542
|
-
describe('MultiSelector in an InputGroup', () => {
|
|
4543
|
-
const groupClasses = () =>
|
|
4544
|
-
stylex.props(groupStyles.inGroup).className!.split(' ');
|
|
4545
|
-
const triggerBox = (name: RegExp) =>
|
|
4546
|
-
screen
|
|
4547
|
-
.getByRole('combobox', {name})
|
|
4548
|
-
.closest('.astryx-multi-selector') as HTMLElement;
|
|
4549
|
-
|
|
4550
|
-
it.each(['input', 'secondary'] as const)(
|
|
4551
|
-
'gives a grouped %s trigger the connected group geometry in every position',
|
|
4552
|
-
variant => {
|
|
4553
|
-
const selector = (label: string) => (
|
|
4554
|
-
<MultiSelector
|
|
4555
|
-
label={label}
|
|
4556
|
-
isLabelHidden
|
|
4557
|
-
variant={variant}
|
|
4558
|
-
options={['Apple', 'Banana', 'Cherry']}
|
|
4559
|
-
value={EMPTY_VALUE}
|
|
4560
|
-
onChange={() => {}}
|
|
4561
|
-
/>
|
|
4562
|
-
);
|
|
4563
|
-
render(
|
|
4564
|
-
<>
|
|
4565
|
-
<InputGroup label="First">
|
|
4566
|
-
{selector('Fruit first')}
|
|
4567
|
-
<InputGroupText>kg</InputGroupText>
|
|
4568
|
-
</InputGroup>
|
|
4569
|
-
<InputGroup label="Middle">
|
|
4570
|
-
<InputGroupText>#</InputGroupText>
|
|
4571
|
-
{selector('Fruit middle')}
|
|
4572
|
-
<InputGroupText>kg</InputGroupText>
|
|
4573
|
-
</InputGroup>
|
|
4574
|
-
<InputGroup label="Last">
|
|
4575
|
-
<InputGroupText>#</InputGroupText>
|
|
4576
|
-
{selector('Fruit last')}
|
|
4577
|
-
</InputGroup>
|
|
4578
|
-
</>,
|
|
4579
|
-
);
|
|
4580
|
-
|
|
4581
|
-
// Row height, seam radii, and focus stacking come from these classes;
|
|
4582
|
-
// a secondary trigger keeps its fill but not its standalone edges.
|
|
4583
|
-
for (const position of ['first', 'middle', 'last']) {
|
|
4584
|
-
const classes = new Set(
|
|
4585
|
-
triggerBox(new RegExp(`Fruit ${position}`)).className.split(' '),
|
|
4586
|
-
);
|
|
4587
|
-
expect(groupClasses().every(c => classes.has(c))).toBe(true);
|
|
4588
|
-
}
|
|
4589
|
-
},
|
|
4590
|
-
);
|
|
4591
|
-
|
|
4592
|
-
it('keeps a standalone secondary trigger out of the group geometry', () => {
|
|
4593
|
-
render(
|
|
4594
|
-
<MultiSelector
|
|
4595
|
-
label="Fruit"
|
|
4596
|
-
variant="secondary"
|
|
4597
|
-
options={['Apple', 'Banana', 'Cherry']}
|
|
4598
|
-
value={EMPTY_VALUE}
|
|
4599
|
-
onChange={() => {}}
|
|
4600
|
-
/>,
|
|
4601
|
-
);
|
|
4602
|
-
const classes = new Set(triggerBox(/Fruit/).className.split(' '));
|
|
4603
|
-
expect(groupClasses().some(c => classes.has(c))).toBe(false);
|
|
4604
|
-
});
|
|
4605
|
-
});
|
|
4606
|
-
|
|
4607
|
-
describe('MultiSelector caller aria-labelledby', () => {
|
|
4608
|
-
const FRUITS = ['Apple', 'Banana', 'Cherry'];
|
|
4609
|
-
|
|
4610
|
-
it('names the trigger by its label and a caller aria-labelledby', () => {
|
|
4611
|
-
render(
|
|
4612
|
-
<>
|
|
4613
|
-
<span id="lunch-heading">Lunch</span>
|
|
4614
|
-
<MultiSelector
|
|
4615
|
-
label="Fruits"
|
|
4616
|
-
options={FRUITS}
|
|
4617
|
-
value={[]}
|
|
4618
|
-
onChange={() => {}}
|
|
4619
|
-
aria-labelledby="lunch-heading"
|
|
4620
|
-
/>
|
|
4621
|
-
</>,
|
|
4622
|
-
);
|
|
4623
|
-
const trigger = screen.getByRole('combobox', {name: 'Fruits Lunch'});
|
|
4624
|
-
expect(trigger.getAttribute('aria-labelledby')?.split(' ')).toContain(
|
|
4625
|
-
'lunch-heading',
|
|
4626
|
-
);
|
|
4627
|
-
});
|
|
4628
|
-
|
|
4629
|
-
it('names a grouped trigger by the group, its label, and a caller aria-labelledby', () => {
|
|
4630
|
-
render(
|
|
4631
|
-
<>
|
|
4632
|
-
<span id="lunch-heading">Lunch</span>
|
|
4633
|
-
<InputGroup label="Order">
|
|
4634
|
-
<MultiSelector
|
|
4635
|
-
label="Fruits"
|
|
4636
|
-
options={FRUITS}
|
|
4637
|
-
value={[]}
|
|
4638
|
-
onChange={() => {}}
|
|
4639
|
-
aria-labelledby="lunch-heading"
|
|
4640
|
-
/>
|
|
4641
|
-
</InputGroup>
|
|
4642
|
-
</>,
|
|
4643
|
-
);
|
|
4644
|
-
expect(
|
|
4645
|
-
screen.getByRole('combobox', {name: 'Order Fruits Lunch'}),
|
|
4646
|
-
).toBeInTheDocument();
|
|
4647
|
-
});
|
|
4648
|
-
|
|
4649
|
-
it('leaves a standalone trigger named by its label element without a caller aria-labelledby', () => {
|
|
4650
|
-
render(
|
|
4651
|
-
<MultiSelector
|
|
4652
|
-
label="Fruits"
|
|
4653
|
-
options={FRUITS}
|
|
4654
|
-
value={[]}
|
|
4655
|
-
onChange={() => {}}
|
|
4656
|
-
/>,
|
|
4657
|
-
);
|
|
4658
|
-
const trigger = screen.getByRole('combobox', {name: 'Fruits'});
|
|
4659
|
-
expect(trigger).not.toHaveAttribute('aria-labelledby');
|
|
4660
|
-
});
|
|
4661
|
-
|
|
4662
|
-
it('still hands a caller aria-labelledby to a renderTrigger control unchanged', () => {
|
|
4663
|
-
render(
|
|
4664
|
-
<>
|
|
4665
|
-
<span id="lunch-heading">Lunch</span>
|
|
4666
|
-
<MultiSelector
|
|
4667
|
-
label="Fruits"
|
|
4668
|
-
options={FRUITS}
|
|
4669
|
-
value={[]}
|
|
4670
|
-
onChange={() => {}}
|
|
4671
|
-
aria-labelledby="lunch-heading"
|
|
4672
|
-
renderTrigger={props => (
|
|
4673
|
-
<button type="button" {...props}>
|
|
4674
|
-
Pick fruits
|
|
4675
|
-
</button>
|
|
4676
|
-
)}
|
|
4677
|
-
/>
|
|
4678
|
-
</>,
|
|
4679
|
-
);
|
|
4680
|
-
const opener = screen.getByRole('button', {name: 'Lunch'});
|
|
4681
|
-
expect(opener).toHaveAttribute('aria-labelledby', 'lunch-heading');
|
|
4682
|
-
});
|
|
4683
|
-
|
|
4684
|
-
it("leaves a renderTrigger control's own aria-labelledby in place without a caller value", () => {
|
|
4685
|
-
render(
|
|
4686
|
-
<>
|
|
4687
|
-
<span id="row-title">Lunch fruits</span>
|
|
4688
|
-
<MultiSelector
|
|
4689
|
-
label="Fruits"
|
|
4690
|
-
options={FRUITS}
|
|
4691
|
-
value={[]}
|
|
4692
|
-
onChange={() => {}}
|
|
4693
|
-
renderTrigger={props => (
|
|
4694
|
-
<button type="button" aria-labelledby="row-title" {...props} />
|
|
4695
|
-
)}
|
|
4696
|
-
/>
|
|
4697
|
-
</>,
|
|
4698
|
-
);
|
|
4699
|
-
const opener = screen.getByRole('button', {name: 'Lunch fruits'});
|
|
4700
|
-
expect(opener).toHaveAttribute('aria-labelledby', 'row-title');
|
|
4701
|
-
});
|
|
4702
|
-
});
|