@astryxdesign/core 0.7.5 → 0.7.6-canary.0bd3b37
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 +183 -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.js +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +2 -1
- package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +30 -3
- 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/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -5
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +37 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +49 -6
- 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.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 +34 -9
- 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 +1 -0
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +7 -2
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +35 -22
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +2 -1
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
- 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/FieldLabel.d.ts +1 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +31 -6
- package/dist/Field/InputClearButton.js +3 -3
- package/dist/Field/PanelSearchInput.js +2 -2
- package/dist/FieldStatus/FieldStatus.d.ts +3 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +3 -2
- package/dist/FileInput/FileInput.d.ts +3 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +66 -34
- 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 +21 -7
- package/dist/Item/ItemSwipeLayer.js +1 -1
- 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 +1 -1
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +120 -65
- package/dist/Layer/useTouchTrigger.d.ts +9 -0
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
- package/dist/Layer/useTouchTrigger.js +1 -1
- 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/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +7 -2
- package/dist/MultiSelector/MultiSelector.d.ts +1 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +45 -19
- package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenuItem.js +5 -29
- package/dist/NumberInput/NumberInput.d.ts +3 -1
- 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/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +32 -12
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +10 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +37 -30
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +16 -12
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +11 -3
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +81 -12
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +9 -6
- package/dist/Selector/hooks.js +2 -2
- package/dist/Selector/useSelectorPresentation.d.ts +8 -1
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
- package/dist/Selector/useSelectorPresentation.js +20 -4
- 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/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 +100 -12
- 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/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +36 -10
- 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/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 +1 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +13 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +3 -3
- package/dist/TimeInput/TimeInput.d.ts +6 -2
- 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/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/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.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +95 -54
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
- package/dist/Tokenizer/TokenizerBottomSheet.js +11 -17
- 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 +3 -0
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +1 -0
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +27 -4
- package/dist/Typeahead/BaseTypeahead.d.ts +4 -4
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +52 -9
- package/dist/Typeahead/Typeahead.d.ts +1 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +43 -9
- package/dist/astryx.css +26 -5
- 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 +182 -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/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/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +12 -1
- 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/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 +8 -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 +8 -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/animator.d.ts +83 -8
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +701 -119
- 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/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/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 +17 -1
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +11 -4
- package/dist/utils/getNodeText.d.ts +8 -0
- package/dist/utils/getNodeText.d.ts.map +1 -0
- package/dist/utils/getNodeText.js +37 -0
- package/dist/utils/interactionModality.d.ts +11 -2
- package/dist/utils/interactionModality.d.ts.map +1 -1
- package/dist/utils/interactionModality.js +58 -5
- 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/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +20 -1
- 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/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 +35 -23
- 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 +23 -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 +19 -5
- package/src/AlertDialog/AlertDialog.tsx +6 -5
- package/src/AppShell/AppShell.doc.mjs +1 -1
- package/src/AppShell/AppShell.spec.md +9 -12
- package/src/AppShell/AppShell.tsx +225 -5
- package/src/AppShell/observeMainOverflow.ts +143 -0
- package/src/Avatar/Avatar.spec.md +22 -22
- 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 +1 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
- package/src/BottomSheet/useSheetGestures.ts +37 -6
- package/src/Button/Button.tsx +4 -4
- package/src/Calendar/Calendar.spec.md +87 -48
- package/src/Carousel/Carousel.doc.mjs +1 -1
- package/src/Carousel/Carousel.spec.md +9 -9
- package/src/Carousel/Carousel.tsx +10 -2
- package/src/Chat/ChatComposer.spec.md +75 -59
- package/src/Chat/ChatComposerTokenElement.spec.md +45 -33
- package/src/Chat/ChatDictationButton.spec.md +126 -81
- package/src/Chat/ChatLayout.spec.md +41 -36
- package/src/Chat/ChatLayoutScrollButton.spec.md +87 -103
- package/src/Chat/ChatMessage.spec.md +64 -65
- package/src/Chat/ChatSendButton.spec.md +78 -67
- package/src/CheckboxInput/CheckboxInput.tsx +6 -5
- package/src/Citation/Citation.spec.md +60 -47
- package/src/ClickableCard/ClickableCard.tsx +48 -4
- package/src/Collapsible/Collapsible.spec.md +2 -0
- package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
- package/src/ComplexSelector/ComplexSelector.tsx +60 -6
- package/src/ContextMenu/ContextMenu.spec.md +84 -56
- package/src/ContextMenu/ContextMenuItem.spec.md +132 -0
- package/src/DateInput/DateInput.spec.md +74 -57
- package/src/DateInput/DateInput.tsx +29 -14
- 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 +34 -15
- package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
- package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
- 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 +42 -9
- package/src/Dialog/DialogHeader.tsx +5 -4
- package/src/Divider/Divider.tsx +2 -1
- package/src/Drawer/Drawer.spec.md +111 -80
- package/src/Drawer/Drawer.tsx +13 -2
- package/src/Drawer/DrawerHeader.spec.md +181 -0
- package/src/DropdownMenu/DropdownMenu.spec.md +198 -218
- package/src/DropdownMenu/DropdownMenu.tsx +43 -21
- package/src/DropdownMenu/DropdownMenuCheckboxItem.spec.md +192 -0
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +25 -31
- package/src/DropdownMenu/DropdownMenuGroup.spec.md +145 -0
- package/src/DropdownMenu/DropdownMenuGroup.tsx +2 -1
- package/src/DropdownMenu/DropdownMenuItem.spec.md +48 -42
- package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +34 -37
- package/src/DropdownMenu/DropdownMenuRadioItem.spec.md +54 -53
- package/src/DropdownMenu/DropdownMenuSubMenu.spec.md +47 -41
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +36 -7
- package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
- package/src/EmptyState/EmptyState.spec.md +35 -29
- package/src/EmptyState/EmptyState.tsx +2 -1
- package/src/Field/Field.doc.mjs +2 -1
- package/src/Field/FieldLabel.doc.mjs +4 -2
- package/src/Field/FieldLabel.spec.md +30 -21
- package/src/Field/FieldLabel.tsx +42 -7
- package/src/Field/InputClearButton.tsx +3 -3
- package/src/Field/PanelSearchInput.tsx +2 -2
- package/src/FieldStatus/FieldStatus.tsx +9 -2
- package/src/FileInput/FileInput.tsx +73 -29
- package/src/FormLayout/FormLayout.spec.md +11 -20
- package/src/Heading/Heading.tsx +1 -1
- package/src/Icon/Icon.tsx +1 -1
- package/src/Indicator/CheckIndicator.spec.md +47 -42
- package/src/Indicator/CheckIndicator.tsx +2 -1
- package/src/Indicator/CheckboxIndicator.spec.md +40 -37
- package/src/Indicator/CheckboxIndicator.tsx +3 -1
- package/src/Indicator/Indicator.spec.md +188 -0
- package/src/Indicator/RadioIndicator.tsx +3 -1
- package/src/Item/Item.tsx +16 -8
- package/src/Item/ItemSwipeLayer.tsx +1 -1
- package/src/Kbd/Kbd.spec.md +81 -38
- package/src/Layer/popoverOperations.ts +172 -0
- package/src/Layer/useLayer.tsx +145 -77
- package/src/Layer/useTouchTrigger.ts +1 -1
- package/src/Layout/Layout.spec.md +127 -131
- package/src/Layout/Layout.tsx +1 -1
- package/src/Layout/LayoutContent.spec.md +205 -0
- package/src/Layout/LayoutContent.tsx +1 -1
- package/src/Layout/LayoutFooter.spec.md +190 -0
- package/src/Layout/LayoutFooter.tsx +1 -1
- package/src/Layout/LayoutHeader.spec.md +193 -0
- package/src/Layout/LayoutHeader.tsx +1 -1
- package/src/Layout/LayoutPanel.spec.md +194 -0
- package/src/Lightbox/Lightbox.spec.md +249 -61
- package/src/Lightbox/Lightbox.tsx +5 -0
- package/src/Link/Link.spec.md +3 -2
- package/src/Link/Link.tsx +20 -2
- package/src/List/List.tsx +1 -1
- package/src/List/ListItem.spec.md +205 -0
- package/src/List/ListItem.tsx +3 -3
- package/src/MetadataList/MetadataList.spec.md +187 -0
- package/src/MetadataList/MetadataListItem.spec.md +165 -0
- package/src/MobileNav/MobileNav.tsx +9 -1
- package/src/MultiSelector/MultiSelector.spec.md +124 -114
- package/src/MultiSelector/MultiSelector.tsx +44 -15
- package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
- package/src/NavMenu/NavHeadingMenuItem.tsx +5 -39
- package/src/NumberInput/NumberInput.doc.mjs +13 -7
- package/src/NumberInput/NumberInput.spec.md +59 -37
- package/src/NumberInput/NumberInput.tsx +37 -4
- package/src/NumberInput/numberParser.ts +7 -45
- package/src/Outline/Outline.spec.md +165 -69
- package/src/Outline/Outline.tsx +23 -0
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +3 -4
- package/src/OverflowList/OverflowList.spec.md +102 -55
- package/src/Pagination/Pagination.spec.md +158 -84
- package/src/Popover/Popover.tsx +34 -14
- package/src/Popover/usePopover.tsx +10 -3
- package/src/PowerSearch/PowerSearch.tsx +42 -31
- package/src/ProgressBar/ProgressBar.spec.md +143 -119
- package/src/ProgressBar/ProgressBar.tsx +58 -10
- package/src/RadioList/RadioList.spec.md +1 -1
- package/src/RadioList/RadioListItem.tsx +11 -2
- package/src/Resizable/Resizable.spec.md +144 -124
- package/src/Resizable/useResizable.ts +82 -16
- package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
- package/src/Selector/Selector.tsx +16 -6
- package/src/Selector/hooks.ts +2 -2
- package/src/Selector/useSelectorPresentation.ts +31 -3
- package/src/SideNav/SideNavItem.spec.md +14 -24
- package/src/SideNav/SideNavItem.tsx +27 -10
- package/src/Skeleton/Skeleton.tsx +3 -1
- package/src/Slider/Slider.spec.md +18 -16
- package/src/Slider/Slider.tsx +117 -12
- 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.spec.md +36 -42
- package/src/Stepper/Step.tsx +33 -6
- package/src/Stepper/Stepper.doc.mjs +1 -1
- package/src/Switch/Switch.spec.md +1 -1
- package/src/Switch/Switch.tsx +11 -3
- package/src/TabList/TabList.tsx +16 -1
- package/src/Table/Table.spec.md +1 -1
- package/src/Table/Table.tsx +43 -11
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
- package/src/Table/plugins/sortable/useTableSortable.tsx +44 -73
- package/src/Text/Text.tsx +1 -1
- package/src/Text/useTruncation.ts +23 -5
- package/src/TextArea/TextArea.spec.md +10 -13
- package/src/TextArea/TextArea.tsx +15 -1
- package/src/TextInput/TextInput.tsx +3 -1
- package/src/TimeInput/TimeInput.doc.mjs +8 -8
- package/src/TimeInput/TimeInput.spec.md +58 -61
- package/src/TimeInput/TimeInput.tsx +36 -18
- package/src/TimeInput/TouchTimeField.tsx +4 -6
- package/src/Timestamp/Timestamp.spec.md +6 -12
- package/src/Timestamp/Timestamp.tsx +6 -2
- package/src/Timestamp/TimestampHoverCard.tsx +4 -4
- package/src/Toast/Toast.spec.md +15 -24
- package/src/Toast/Toast.tsx +104 -36
- package/src/Toast/ToastViewport.tsx +121 -53
- package/src/ToggleButton/ToggleButtonGroup.tsx +20 -1
- package/src/Token/Token.doc.mjs +2 -2
- package/src/Token/Token.spec.md +52 -61
- package/src/Token/Token.tsx +75 -5
- package/src/Token/TokenLink.tsx +21 -7
- package/src/Tokenizer/Tokenizer.spec.md +18 -4
- package/src/Tokenizer/Tokenizer.tsx +102 -60
- package/src/Tokenizer/TokenizerBottomSheet.tsx +11 -17
- package/src/Tokenizer/TokenizerClearAllMessageContext.ts +21 -0
- package/src/Toolbar/Toolbar.spec.md +5 -6
- package/src/Toolbar/Toolbar.tsx +2 -1
- package/src/TopNav/TopNavMegaMenu.tsx +4 -1
- package/src/TopNav/TopNavMenu.tsx +1 -0
- package/src/TreeList/TreeListItem.tsx +26 -4
- package/src/Typeahead/BaseTypeahead.doc.mjs +3 -3
- package/src/Typeahead/BaseTypeahead.spec.md +45 -45
- package/src/Typeahead/BaseTypeahead.tsx +66 -11
- package/src/Typeahead/Typeahead.tsx +51 -9
- 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 +203 -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/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/useMenuHover.ts +15 -1
- package/src/hooks/useScrollOverflow.doc.mjs +1 -1
- package/src/hooks/useScrollOverflow.ts +59 -10
- 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 +9 -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 +9 -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/animator.ts +932 -130
- 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/theme/Theme.doc.mjs +1 -1
- 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/utils/focusOutline.stylex.ts +28 -0
- package/src/utils/focusReturn.ts +17 -6
- package/src/utils/getNodeText.ts +37 -0
- package/src/utils/interactionModality.ts +64 -5
- 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/pressFeedback.ts +34 -11
- 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/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.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.test.tsx +0 -1148
- package/src/AspectRatio/AspectRatio.test.tsx +0 -398
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +0 -158
- package/src/Avatar/Avatar.test.tsx +0 -867
- package/src/Avatar/AvatarStatusDot.test.tsx +0 -306
- package/src/AvatarGroup/AvatarGroup.test.tsx +0 -313
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +0 -263
- package/src/Badge/Badge.test.tsx +0 -125
- package/src/Banner/Banner.test.tsx +0 -879
- package/src/Blockquote/Blockquote.test.tsx +0 -121
- 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.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/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.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.test.tsx +0 -1025
- 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.test.tsx +0 -202
- 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.test.tsx +0 -406
- package/src/Chat/ChatComposer.test.tsx +0 -364
- package/src/Chat/ChatComposerDrawer.test.tsx +0 -130
- package/src/Chat/ChatComposerInput.test.tsx +0 -2136
- package/src/Chat/ChatComposerTokenElement.test.tsx +0 -85
- package/src/Chat/ChatDictationButton.test.tsx +0 -108
- package/src/Chat/ChatLayout.test.tsx +0 -311
- package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -99
- package/src/Chat/ChatMessage.test.tsx +0 -344
- package/src/Chat/ChatMessageBubble.test.tsx +0 -294
- package/src/Chat/ChatMessageList.test.tsx +0 -198
- package/src/Chat/ChatMessageMetadata.test.tsx +0 -66
- package/src/Chat/ChatPastedTextToken.test.tsx +0 -71
- package/src/Chat/ChatSendButton.test.tsx +0 -309
- package/src/Chat/ChatSystemMessage.test.tsx +0 -189
- package/src/Chat/ChatTokenizedText.test.tsx +0 -163
- 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.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.test.tsx +0 -1159
- package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +0 -1998
- package/src/Citation/Citation.test.tsx +0 -361
- package/src/ClickableCard/ClickableCard.test.tsx +0 -286
- package/src/Code/Code.test.tsx +0 -75
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +0 -87
- 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.test.tsx +0 -620
- 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.test.tsx +0 -750
- 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.test.tsx +0 -539
- package/src/CommandPalette/CommandPaletteItem.test.tsx +0 -417
- 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.test.tsx +0 -1023
- package/src/ComplexSelector/__tests__/ComplexSelector.a11y.chromium.spec.ts +0 -965
- 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.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.test.tsx +0 -1108
- package/src/DateTimeInput/DateTimeInput.fr10.test.tsx +0 -319
- 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.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/useImperativeDialog.test.tsx +0 -78
- package/src/Divider/Divider.test.tsx +0 -155
- 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.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/DropdownMenuLazySheet.test.tsx +0 -126
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -635
- package/src/DropdownMenu/DropdownMenuSheetFirstOpen.test.tsx +0 -72
- 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/EmptyState/EmptyState.announce.test.tsx +0 -227
- package/src/EmptyState/EmptyState.test.tsx +0 -284
- package/src/Field/Field.test.tsx +0 -800
- 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.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.test.tsx +0 -1270
- package/src/FormLayout/FormLayout.test.tsx +0 -444
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -46
- package/src/Grid/Grid.test.tsx +0 -509
- package/src/HStack/HStack.test.tsx +0 -129
- package/src/Heading/Heading.test.tsx +0 -328
- package/src/HoverCard/HoverCard.test.tsx +0 -1339
- package/src/Icon/Icon.test.tsx +0 -990
- package/src/Icon/globalIconRegistry.test.tsx +0 -613
- package/src/IconButton/IconButton.test.tsx +0 -156
- package/src/Indicator/Indicator.test.tsx +0 -389
- package/src/Indicator/indicatorRegistry.test.tsx +0 -106
- package/src/InputGroup/InputGroup.test.tsx +0 -502
- package/src/InteractiveRoleContext/InteractiveRoleContext.test.tsx +0 -185
- package/src/Item/Item.source-build.test.mjs +0 -88
- 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.test.tsx +0 -299
- 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.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.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.test.tsx +0 -457
- 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.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.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/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.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.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.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.test.tsx +0 -41
- 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.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.test.tsx +0 -1069
- package/src/Outline/OutlineFocus.a11y.chromium.spec.ts +0 -551
- package/src/Outline/parseOutlineFromMarkdown.test.ts +0 -104
- package/src/OverflowList/OverflowList.test.tsx +0 -1073
- package/src/Overlay/Overlay.test.tsx +0 -424
- package/src/Pagination/Pagination.test.tsx +0 -1754
- 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.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.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.test.tsx +0 -967
- package/src/RadioList/RadioListItem.press.source-build.test.mjs +0 -80
- 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/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.test.tsx +0 -304
- package/src/Section/Section.test.tsx +0 -376
- package/src/SegmentedControl/SegmentedControl.test.tsx +0 -897
- 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.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.test.tsx +0 -4128
- package/src/SideNav/SideNavRailMotion.test.tsx +0 -620
- package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +0 -417
- package/src/SizeContext/SizeContext.test.tsx +0 -226
- package/src/Skeleton/Skeleton.test.tsx +0 -148
- package/src/Slider/Slider.fr10.test.tsx +0 -93
- 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.test.tsx +0 -484
- package/src/Stack/Stack.test.tsx +0 -473
- package/src/Stack/StackItem.test.tsx +0 -105
- package/src/StatusDot/StatusDot.test.tsx +0 -213
- package/src/Stepper/Stepper.public.test.ts +0 -91
- package/src/Stepper/Stepper.test.tsx +0 -2068
- package/src/Switch/Switch.focusRing.source-build.test.mjs +0 -81
- 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/TabList.test.tsx +0 -1701
- 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.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/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/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.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/tableContextMenu.test.tsx +0 -208
- 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.test.tsx +0 -1257
- package/src/TextInput/TextInput.fr10.test.tsx +0 -21
- 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.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.test.tsx +0 -831
- package/src/Timer/Timer.test.tsx +0 -302
- package/src/Timestamp/Timestamp.test.tsx +0 -1636
- package/src/Timestamp/tooltipEntries.test.ts +0 -566
- 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.test.tsx +0 -973
- 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.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.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.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.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.test.tsx +0 -836
- package/src/TopNav/TopNavMegaMenu.reduced-motion.source-build.test.mjs +0 -83
- package/src/TopNav/TopNavMegaMenu.test.tsx +0 -1132
- package/src/TopNav/TopNavMenu.test.tsx +0 -485
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +0 -75
- 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.test.tsx +0 -553
- package/src/Typeahead/Typeahead.fr10.test.tsx +0 -28
- 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.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/__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,4128 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file SideNav.test.tsx
|
|
5
|
-
* @input Uses vitest, @testing-library/react, SideNav components
|
|
6
|
-
* @output Unit tests for SideNav component suite
|
|
7
|
-
* @position Testing; validates SideNav implementations
|
|
8
|
-
*
|
|
9
|
-
* SYNC: When SideNav components change, update tests to match new behavior
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import React from 'react';
|
|
13
|
-
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
14
|
-
import {
|
|
15
|
-
render,
|
|
16
|
-
screen,
|
|
17
|
-
act,
|
|
18
|
-
fireEvent,
|
|
19
|
-
waitFor,
|
|
20
|
-
within,
|
|
21
|
-
} from '@testing-library/react';
|
|
22
|
-
import userEvent from '@testing-library/user-event';
|
|
23
|
-
import {useRef, useState, type ReactNode} from 'react';
|
|
24
|
-
import * as stylex from '@stylexjs/stylex';
|
|
25
|
-
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
26
|
-
import {getForcedColorsRules} from '../__tests__/forcedColors';
|
|
27
|
-
import {Button} from '../Button';
|
|
28
|
-
import {SideNav} from './SideNav';
|
|
29
|
-
import {__resetMenuPressForTest} from '../hooks/useMenuPress';
|
|
30
|
-
import {expectNativeLinkClicks} from '../hooks/__tests__/menuLinkClicks';
|
|
31
|
-
import {NavHeadingMenu, NavHeadingMenuItem} from '../NavMenu';
|
|
32
|
-
import {SideNavCollapseButton} from './SideNavCollapseButton';
|
|
33
|
-
import {SideNavHeading} from './SideNavHeading';
|
|
34
|
-
import {SideNavItem} from './SideNavItem';
|
|
35
|
-
import {SideNavSection} from './SideNavSection';
|
|
36
|
-
import {LinkProvider} from '../Link/LinkProvider';
|
|
37
|
-
import {InternationalizationProvider} from '../i18n/InternationalizationProvider';
|
|
38
|
-
import pseudoCatalog from '../../locales/pseudo.json';
|
|
39
|
-
import {
|
|
40
|
-
SideNavCollapseContext,
|
|
41
|
-
type SideNavImperativeCollapseHandle,
|
|
42
|
-
} from './SideNavCollapseContext';
|
|
43
|
-
|
|
44
|
-
function CustomLink({
|
|
45
|
-
children,
|
|
46
|
-
ref,
|
|
47
|
-
...props
|
|
48
|
-
}: React.ComponentPropsWithRef<'a'>) {
|
|
49
|
-
return (
|
|
50
|
-
<a ref={ref} data-custom-link {...props}>
|
|
51
|
-
{children}
|
|
52
|
-
</a>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// =============================================================================
|
|
57
|
-
// SideNav
|
|
58
|
-
// =============================================================================
|
|
59
|
-
|
|
60
|
-
describe('SideNav', () => {
|
|
61
|
-
it('renders with navigation role', () => {
|
|
62
|
-
render(<SideNav>Content</SideNav>);
|
|
63
|
-
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('renders aria-label for page navigation', () => {
|
|
67
|
-
render(<SideNav>Content</SideNav>);
|
|
68
|
-
expect(screen.getByRole('navigation')).toHaveAttribute(
|
|
69
|
-
'aria-label',
|
|
70
|
-
'Side navigation',
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('renders children in scrollable area', () => {
|
|
75
|
-
render(
|
|
76
|
-
<SideNav>
|
|
77
|
-
<span data-testid="nav-content">Nav items</span>
|
|
78
|
-
</SideNav>,
|
|
79
|
-
);
|
|
80
|
-
expect(screen.getByTestId('nav-content')).toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('renders header slot', () => {
|
|
84
|
-
render(
|
|
85
|
-
<SideNav header={<span data-testid="header">Header</span>}>
|
|
86
|
-
Content
|
|
87
|
-
</SideNav>,
|
|
88
|
-
);
|
|
89
|
-
expect(screen.getByTestId('header')).toBeInTheDocument();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('renders topContent slot', () => {
|
|
93
|
-
render(
|
|
94
|
-
<SideNav topContent={<span data-testid="sticky">Sticky</span>}>
|
|
95
|
-
Content
|
|
96
|
-
</SideNav>,
|
|
97
|
-
);
|
|
98
|
-
expect(screen.getByTestId('sticky')).toBeInTheDocument();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('renders footer slot', () => {
|
|
102
|
-
render(
|
|
103
|
-
<SideNav footer={<span data-testid="footer">Footer</span>}>
|
|
104
|
-
Content
|
|
105
|
-
</SideNav>,
|
|
106
|
-
);
|
|
107
|
-
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('renders footerIcons slot', () => {
|
|
111
|
-
render(
|
|
112
|
-
<SideNav footerIcons={<span data-testid="footer-icons">Icons</span>}>
|
|
113
|
-
Content
|
|
114
|
-
</SideNav>,
|
|
115
|
-
);
|
|
116
|
-
expect(screen.getByTestId('footer-icons')).toBeInTheDocument();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('renders all slots together', () => {
|
|
120
|
-
render(
|
|
121
|
-
<SideNav
|
|
122
|
-
header={<span data-testid="header">Header</span>}
|
|
123
|
-
topContent={<span data-testid="sticky">Sticky</span>}
|
|
124
|
-
footer={<span data-testid="footer">Footer</span>}
|
|
125
|
-
footerIcons={<span data-testid="icons">Icons</span>}>
|
|
126
|
-
<span data-testid="content">Content</span>
|
|
127
|
-
</SideNav>,
|
|
128
|
-
);
|
|
129
|
-
expect(screen.getByTestId('header')).toBeInTheDocument();
|
|
130
|
-
expect(screen.getByTestId('sticky')).toBeInTheDocument();
|
|
131
|
-
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
132
|
-
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
|
133
|
-
expect(screen.getByTestId('icons')).toBeInTheDocument();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('forwards ref correctly', () => {
|
|
137
|
-
const ref = vi.fn();
|
|
138
|
-
render(<SideNav ref={ref}>Content</SideNav>);
|
|
139
|
-
expect(ref).toHaveBeenCalledWith(expect.any(HTMLElement));
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('passes data-testid to root', () => {
|
|
143
|
-
render(<SideNav data-testid="page-nav">Content</SideNav>);
|
|
144
|
-
expect(screen.getByTestId('page-nav')).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('renders and toggles from outside SideNav when handleRef is provided', async () => {
|
|
148
|
-
const user = userEvent.setup();
|
|
149
|
-
|
|
150
|
-
function Example() {
|
|
151
|
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
152
|
-
const handleRef = useRef<SideNavImperativeCollapseHandle>(null);
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<>
|
|
156
|
-
<SideNavCollapseButton handleRef={handleRef} />
|
|
157
|
-
<SideNav
|
|
158
|
-
handleRef={handleRef}
|
|
159
|
-
collapsible={{
|
|
160
|
-
isCollapsed,
|
|
161
|
-
onCollapsedChange: setIsCollapsed,
|
|
162
|
-
hasButton: false,
|
|
163
|
-
}}>
|
|
164
|
-
<SideNavSection title="Main">
|
|
165
|
-
<SideNavItem label="Dashboard" icon={StubIcon} />
|
|
166
|
-
</SideNavSection>
|
|
167
|
-
</SideNav>
|
|
168
|
-
</>
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
render(<Example />);
|
|
173
|
-
|
|
174
|
-
const button = screen.getByRole('button', {name: 'Collapse sidebar'});
|
|
175
|
-
await user.click(button);
|
|
176
|
-
|
|
177
|
-
expect(
|
|
178
|
-
screen.getByRole('button', {name: 'Expand sidebar'}),
|
|
179
|
-
).toBeInTheDocument();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('does not render an empty footer container when collapsible.hasButton is false', () => {
|
|
183
|
-
render(
|
|
184
|
-
<SideNav data-testid="nav" collapsible={{hasButton: false}}>
|
|
185
|
-
Content
|
|
186
|
-
</SideNav>,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
// The built-in collapse button is opted out (consumers render their own
|
|
190
|
-
// SideNavCollapseButton in the header), so it must not appear...
|
|
191
|
-
expect(
|
|
192
|
-
screen.queryByRole('button', {name: 'Collapse sidebar'}),
|
|
193
|
-
).not.toBeInTheDocument();
|
|
194
|
-
|
|
195
|
-
// ...and no empty sticky-bottom container should be left behind. With no
|
|
196
|
-
// footer/footerIcons and no built-in button, the scrollable content region
|
|
197
|
-
// is the nav's only child.
|
|
198
|
-
const nav = screen.getByTestId('nav');
|
|
199
|
-
expect(nav.children).toHaveLength(1);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('centers footer content when collapsed, matching children alignment', () => {
|
|
203
|
-
render(
|
|
204
|
-
<SideNav
|
|
205
|
-
data-testid="nav"
|
|
206
|
-
collapsible={{isCollapsed: true, hasButton: false}}
|
|
207
|
-
footer={<span data-testid="footer-content">F</span>}>
|
|
208
|
-
<span data-testid="children-content">C</span>
|
|
209
|
-
</SideNav>,
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
// scrollableCollapsed (wraps children) and stickyBottomCollapsed (wraps
|
|
213
|
-
// footer) are structurally parallel collapsed-rail containers; both
|
|
214
|
-
// must center their content the same way, or full-width footer content
|
|
215
|
-
// stretches to the collapsed rail's width instead of centering.
|
|
216
|
-
const childrenContainer =
|
|
217
|
-
screen.getByTestId('children-content').parentElement;
|
|
218
|
-
const footerContainer = screen.getByTestId('footer-content').parentElement;
|
|
219
|
-
expect(getComputedStyle(childrenContainer!).alignItems).toBe('center');
|
|
220
|
-
expect(getComputedStyle(footerContainer!).alignItems).toBe('center');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('fires a consumer onClick on the collapse button in addition to toggling', async () => {
|
|
224
|
-
const user = userEvent.setup();
|
|
225
|
-
const onClick = vi.fn();
|
|
226
|
-
|
|
227
|
-
function Example() {
|
|
228
|
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
229
|
-
const handleRef = useRef<SideNavImperativeCollapseHandle>(null);
|
|
230
|
-
return (
|
|
231
|
-
<>
|
|
232
|
-
<SideNavCollapseButton handleRef={handleRef} onClick={onClick} />
|
|
233
|
-
<SideNav
|
|
234
|
-
handleRef={handleRef}
|
|
235
|
-
collapsible={{
|
|
236
|
-
isCollapsed,
|
|
237
|
-
onCollapsedChange: setIsCollapsed,
|
|
238
|
-
hasButton: false,
|
|
239
|
-
}}>
|
|
240
|
-
<SideNavSection title="Main">
|
|
241
|
-
<SideNavItem label="Dashboard" icon={StubIcon} />
|
|
242
|
-
</SideNavSection>
|
|
243
|
-
</SideNav>
|
|
244
|
-
</>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
render(<Example />);
|
|
249
|
-
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
250
|
-
|
|
251
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
252
|
-
// Toggle still ran: the label flipped to "Expand sidebar".
|
|
253
|
-
expect(
|
|
254
|
-
screen.getByRole('button', {name: 'Expand sidebar'}),
|
|
255
|
-
).toBeInTheDocument();
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// =============================================================================
|
|
260
|
-
// SideNavHeading
|
|
261
|
-
// =============================================================================
|
|
262
|
-
|
|
263
|
-
describe('SideNavHeading', () => {
|
|
264
|
-
it('renders heading text', () => {
|
|
265
|
-
render(<SideNavHeading heading="My App" />);
|
|
266
|
-
expect(screen.getByText('My App')).toBeInTheDocument();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('renders icon', () => {
|
|
270
|
-
render(
|
|
271
|
-
<SideNavHeading
|
|
272
|
-
heading="My App"
|
|
273
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
274
|
-
/>,
|
|
275
|
-
);
|
|
276
|
-
expect(screen.getByTestId('app-icon')).toBeInTheDocument();
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('renders superheading', () => {
|
|
280
|
-
render(<SideNavHeading heading="Product" superheading="Suite Name" />);
|
|
281
|
-
expect(screen.getByText('Suite Name')).toBeInTheDocument();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('renders subheading', () => {
|
|
285
|
-
render(<SideNavHeading heading="Product" subheading="Account" />);
|
|
286
|
-
expect(screen.getByText('Account')).toBeInTheDocument();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('renders as link when headingHref is provided without menu', () => {
|
|
290
|
-
render(<SideNavHeading heading="My App" headingHref="/home" />);
|
|
291
|
-
const link = screen.getByRole('link');
|
|
292
|
-
expect(link).toHaveAttribute('href', '/home');
|
|
293
|
-
expect(link).toHaveTextContent('My App');
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('uses custom link component from as prop', () => {
|
|
297
|
-
render(
|
|
298
|
-
<SideNavHeading heading="My App" headingHref="/home" as={CustomLink} />,
|
|
299
|
-
);
|
|
300
|
-
const link = screen.getByRole('link');
|
|
301
|
-
expect(link).toHaveAttribute('data-custom-link');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('uses custom link component from LinkProvider', () => {
|
|
305
|
-
render(
|
|
306
|
-
<LinkProvider component={CustomLink}>
|
|
307
|
-
<SideNavHeading heading="My App" headingHref="/home" />
|
|
308
|
-
</LinkProvider>,
|
|
309
|
-
);
|
|
310
|
-
const link = screen.getByRole('link');
|
|
311
|
-
expect(link).toHaveAttribute('data-custom-link');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('renders independent links when headingHref and superheadingHref are provided', () => {
|
|
315
|
-
render(
|
|
316
|
-
<SideNavHeading
|
|
317
|
-
heading="Product"
|
|
318
|
-
headingHref="/product"
|
|
319
|
-
superheading="Suite"
|
|
320
|
-
superheadingHref="/suite"
|
|
321
|
-
/>,
|
|
322
|
-
);
|
|
323
|
-
const links = screen.getAllByRole('link');
|
|
324
|
-
expect(links).toHaveLength(2);
|
|
325
|
-
expect(links[0]).toHaveAttribute('href', '/suite');
|
|
326
|
-
expect(links[1]).toHaveAttribute('href', '/product');
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('gives every link an accessible name with superheadingHref, headingHref, and menu', () => {
|
|
330
|
-
render(
|
|
331
|
-
<SideNavHeading
|
|
332
|
-
icon={<span>Icon</span>}
|
|
333
|
-
superheading="Suite Name"
|
|
334
|
-
superheadingHref="/suite"
|
|
335
|
-
heading="Product Name"
|
|
336
|
-
headingHref="/product"
|
|
337
|
-
menu={<div>Analytics</div>}
|
|
338
|
-
/>,
|
|
339
|
-
);
|
|
340
|
-
// The icon link to /product previously rendered with no text and no
|
|
341
|
-
// aria-label, producing an empty accessible name (axe rule: link-name).
|
|
342
|
-
// Every link pointing at /product must now expose "Product Name".
|
|
343
|
-
const productLinks = screen
|
|
344
|
-
.getAllByRole('link', {name: 'Product Name'})
|
|
345
|
-
.filter(link => link.getAttribute('href') === '/product');
|
|
346
|
-
expect(productLinks.length).toBeGreaterThan(0);
|
|
347
|
-
for (const link of productLinks) {
|
|
348
|
-
expect(link).toHaveAccessibleName('Product Name');
|
|
349
|
-
}
|
|
350
|
-
// The independent superheading link is unaffected.
|
|
351
|
-
expect(screen.getByRole('link', {name: 'Suite Name'})).toHaveAttribute(
|
|
352
|
-
'href',
|
|
353
|
-
'/suite',
|
|
354
|
-
);
|
|
355
|
-
// No link should be missing an accessible name.
|
|
356
|
-
for (const link of screen.getAllByRole('link')) {
|
|
357
|
-
expect(link).toHaveAccessibleName();
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('shows chevron when menu is provided', () => {
|
|
362
|
-
render(<SideNavHeading heading="My App" menu={<div>Menu content</div>} />);
|
|
363
|
-
// The chevron SVG should be rendered
|
|
364
|
-
const button = screen.getByRole('button');
|
|
365
|
-
expect(button).toBeInTheDocument();
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('does not show chevron without menu', () => {
|
|
369
|
-
const {container} = render(<SideNavHeading heading="My App" />);
|
|
370
|
-
const svg = container.querySelector('svg');
|
|
371
|
-
expect(svg).not.toBeInTheDocument();
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('whole heading is popover trigger when menu provided without hrefs', () => {
|
|
375
|
-
render(<SideNavHeading heading="My App" menu={<div>Menu</div>} />);
|
|
376
|
-
const button = screen.getByRole('button');
|
|
377
|
-
// The popup is no longer a dialog (role: 'none' on usePopover), so the
|
|
378
|
-
// trigger advertises a generic popup rather than aria-haspopup="dialog".
|
|
379
|
-
expect(button).toHaveAttribute('aria-haspopup', 'true');
|
|
380
|
-
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('has popoverTarget on trigger button when menu is provided', () => {
|
|
384
|
-
render(
|
|
385
|
-
<SideNavHeading
|
|
386
|
-
heading="My App"
|
|
387
|
-
menu={<div data-testid="menu-content">Menu</div>}
|
|
388
|
-
/>,
|
|
389
|
-
);
|
|
390
|
-
const button = screen.getByRole('button');
|
|
391
|
-
// The trigger button uses aria attributes from usePopover and
|
|
392
|
-
// an onClick handler from useMenuHover for click-to-lock toggle.
|
|
393
|
-
expect(button).toHaveAttribute('aria-haspopup', 'true');
|
|
394
|
-
expect(button).toHaveAttribute('aria-expanded');
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('renders chevron as separate trigger when menu and hrefs are provided', () => {
|
|
398
|
-
render(
|
|
399
|
-
<SideNavHeading
|
|
400
|
-
heading="Product"
|
|
401
|
-
headingHref="/product"
|
|
402
|
-
menu={<div>Menu</div>}
|
|
403
|
-
/>,
|
|
404
|
-
);
|
|
405
|
-
const button = screen.getByRole('button', {name: 'Open menu'});
|
|
406
|
-
expect(button).toHaveAttribute('aria-haspopup', 'true');
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('passes data-testid', () => {
|
|
410
|
-
render(<SideNavHeading heading="My App" data-testid="nav-header" />);
|
|
411
|
-
expect(screen.getByTestId('nav-header')).toBeInTheDocument();
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
// ===========================================================================
|
|
415
|
-
// Menu popover semantics — the popup must not be announced as a modal
|
|
416
|
-
// dialog, and role="menu" must be scoped to the actual menu items so the
|
|
417
|
-
// heading button is not an invalid child of the menu.
|
|
418
|
-
// ===========================================================================
|
|
419
|
-
|
|
420
|
-
// The popover layer keeps `popover` content display:none in jsdom even
|
|
421
|
-
// when open, hiding it from role queries — so these tests assert the
|
|
422
|
-
// popup's ARIA semantics at the DOM level instead.
|
|
423
|
-
describe('menu popover semantics', () => {
|
|
424
|
-
const menuItems = (
|
|
425
|
-
<>
|
|
426
|
-
<div role="menuitem">Alpha</div>
|
|
427
|
-
<div role="menuitem">Beta</div>
|
|
428
|
-
</>
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
it('leaves a press on caller content that is not an item to that content: a button click and a link click run (interaction-modality INV4)', async () => {
|
|
432
|
-
__resetMenuPressForTest();
|
|
433
|
-
const user = userEvent.setup();
|
|
434
|
-
const onButton = vi.fn();
|
|
435
|
-
const onLink = vi.fn((event: React.MouseEvent) => event.preventDefault());
|
|
436
|
-
render(
|
|
437
|
-
<SideNavHeading
|
|
438
|
-
heading="My App"
|
|
439
|
-
menu={
|
|
440
|
-
<>
|
|
441
|
-
<button type="button" onClick={onButton}>
|
|
442
|
-
Switch account
|
|
443
|
-
</button>
|
|
444
|
-
<a href="#docs" onClick={onLink}>
|
|
445
|
-
Docs
|
|
446
|
-
</a>
|
|
447
|
-
</>
|
|
448
|
-
}
|
|
449
|
-
/>,
|
|
450
|
-
);
|
|
451
|
-
await user.click(screen.getByRole('button', {name: 'Open menu'}));
|
|
452
|
-
for (const pointer of [
|
|
453
|
-
{pointerType: 'mouse', pointerId: 1, button: 0},
|
|
454
|
-
{pointerType: 'touch', pointerId: 2},
|
|
455
|
-
]) {
|
|
456
|
-
onButton.mockClear();
|
|
457
|
-
onLink.mockClear();
|
|
458
|
-
const button = screen.getByRole('button', {
|
|
459
|
-
name: 'Switch account',
|
|
460
|
-
hidden: true,
|
|
461
|
-
});
|
|
462
|
-
fireEvent.pointerDown(button, pointer);
|
|
463
|
-
fireEvent.pointerUp(button, pointer);
|
|
464
|
-
// A pointer's click reports detail 1 (a keyboard's reports 0).
|
|
465
|
-
fireEvent.click(button, {detail: 1});
|
|
466
|
-
expect(onButton).toHaveBeenCalledTimes(1);
|
|
467
|
-
const link = screen.getByRole('link', {name: 'Docs', hidden: true});
|
|
468
|
-
fireEvent.pointerDown(link, pointer);
|
|
469
|
-
fireEvent.pointerUp(link, pointer);
|
|
470
|
-
fireEvent.click(link, {detail: 1});
|
|
471
|
-
expect(onLink).toHaveBeenCalledTimes(1);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it('leaves a Ctrl-, Cmd-, Shift- or middle click on a link item in its menu to the browser', async () => {
|
|
476
|
-
__resetMenuPressForTest();
|
|
477
|
-
const user = userEvent.setup();
|
|
478
|
-
render(
|
|
479
|
-
<SideNavHeading
|
|
480
|
-
heading="My App"
|
|
481
|
-
menu={
|
|
482
|
-
<NavHeadingMenu>
|
|
483
|
-
<NavHeadingMenuItem label="Docs" href="/docs" />
|
|
484
|
-
<NavHeadingMenuItem label="Status" href="/status" />
|
|
485
|
-
</NavHeadingMenu>
|
|
486
|
-
}
|
|
487
|
-
/>,
|
|
488
|
-
);
|
|
489
|
-
await user.click(screen.getByRole('button', {name: 'Open menu'}));
|
|
490
|
-
expectNativeLinkClicks(
|
|
491
|
-
screen.getByRole('menuitem', {name: 'Docs', hidden: true}),
|
|
492
|
-
);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('closes when a mouse pressed on a NavHeadingMenu item is released outside the menu (useMenuPress FR3)', async () => {
|
|
496
|
-
__resetMenuPressForTest();
|
|
497
|
-
const user = userEvent.setup();
|
|
498
|
-
render(
|
|
499
|
-
<>
|
|
500
|
-
<SideNavHeading
|
|
501
|
-
heading="My App"
|
|
502
|
-
menu={
|
|
503
|
-
<NavHeadingMenu>
|
|
504
|
-
<NavHeadingMenuItem label="Alpha" onClick={() => {}} />
|
|
505
|
-
<NavHeadingMenuItem label="Beta" onClick={() => {}} />
|
|
506
|
-
</NavHeadingMenu>
|
|
507
|
-
}
|
|
508
|
-
/>
|
|
509
|
-
<button type="button">Outside</button>
|
|
510
|
-
</>,
|
|
511
|
-
);
|
|
512
|
-
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
513
|
-
await user.click(trigger);
|
|
514
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
515
|
-
const mouse = {pointerType: 'mouse', pointerId: 3, button: 0};
|
|
516
|
-
const outside = screen.getByRole('button', {name: 'Outside'});
|
|
517
|
-
fireEvent.pointerDown(
|
|
518
|
-
screen.getByRole('menuitem', {name: 'Alpha', hidden: true}),
|
|
519
|
-
mouse,
|
|
520
|
-
);
|
|
521
|
-
fireEvent.pointerMove(outside, mouse);
|
|
522
|
-
fireEvent.pointerUp(outside, mouse);
|
|
523
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it('acts on the item a press slid across the menu lifts on, once (family:action-groups FR1, FR2, FR5)', async () => {
|
|
527
|
-
__resetMenuPressForTest();
|
|
528
|
-
const user = userEvent.setup();
|
|
529
|
-
const onAlpha = vi.fn();
|
|
530
|
-
const onBeta = vi.fn();
|
|
531
|
-
render(
|
|
532
|
-
<SideNavHeading
|
|
533
|
-
heading="My App"
|
|
534
|
-
menu={
|
|
535
|
-
<>
|
|
536
|
-
<div role="menuitem" tabIndex={-1} onClick={onAlpha}>
|
|
537
|
-
Alpha
|
|
538
|
-
</div>
|
|
539
|
-
<div role="menuitem" tabIndex={-1} onClick={onBeta}>
|
|
540
|
-
Beta
|
|
541
|
-
</div>
|
|
542
|
-
</>
|
|
543
|
-
}
|
|
544
|
-
/>,
|
|
545
|
-
);
|
|
546
|
-
await user.click(screen.getByRole('button', {name: 'Open menu'}));
|
|
547
|
-
const item = (name: string) =>
|
|
548
|
-
screen.getByRole('menuitem', {name, hidden: true});
|
|
549
|
-
for (const pointer of [
|
|
550
|
-
{pointerType: 'touch', pointerId: 1},
|
|
551
|
-
{pointerType: 'mouse', pointerId: 2, button: 0},
|
|
552
|
-
]) {
|
|
553
|
-
onAlpha.mockClear();
|
|
554
|
-
onBeta.mockClear();
|
|
555
|
-
fireEvent.pointerDown(item('Alpha'), pointer);
|
|
556
|
-
fireEvent.pointerMove(item('Beta'), pointer);
|
|
557
|
-
fireEvent.pointerUp(item('Beta'), pointer);
|
|
558
|
-
expect(onBeta).toHaveBeenCalledTimes(1);
|
|
559
|
-
expect(onAlpha).not.toHaveBeenCalled();
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it('does not wrap the heading menu popup in a modal dialog', async () => {
|
|
564
|
-
const user = userEvent.setup();
|
|
565
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
566
|
-
await user.click(screen.getByRole('button', {name: 'Open menu'}));
|
|
567
|
-
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
568
|
-
expect(document.querySelector('[aria-modal="true"]')).toBeNull();
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it('scopes role="menu" to only menuitem children with an accessible name', async () => {
|
|
572
|
-
const user = userEvent.setup();
|
|
573
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
574
|
-
await user.click(screen.getByRole('button', {name: 'Open menu'}));
|
|
575
|
-
const menu = document.querySelector('[role="menu"]');
|
|
576
|
-
expect(menu).not.toBeNull();
|
|
577
|
-
expect(menu).toHaveAttribute('aria-label', 'My App');
|
|
578
|
-
const children = Array.from(menu!.children);
|
|
579
|
-
expect(children.length).toBeGreaterThan(0);
|
|
580
|
-
for (const child of children) {
|
|
581
|
-
expect(child).toHaveAttribute('role', 'menuitem');
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it('keeps the popup heading button outside the menu and closes on click', async () => {
|
|
586
|
-
const user = userEvent.setup();
|
|
587
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
588
|
-
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
589
|
-
await user.click(trigger);
|
|
590
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
591
|
-
const menu = document.querySelector('[role="menu"]');
|
|
592
|
-
expect(menu).not.toBeNull();
|
|
593
|
-
// The heading replica button in the popup is a sibling of the menu,
|
|
594
|
-
// not an invalid menu child.
|
|
595
|
-
const headingButton = Array.from(
|
|
596
|
-
document.querySelectorAll('button'),
|
|
597
|
-
).find(b => b !== trigger && b.textContent?.includes('My App'));
|
|
598
|
-
expect(headingButton).toBeDefined();
|
|
599
|
-
expect(menu!.contains(headingButton!)).toBe(false);
|
|
600
|
-
// Clicking it still closes the popup.
|
|
601
|
-
fireEvent.click(headingButton!);
|
|
602
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it('applies the same semantics in mixed mode (menu + hrefs)', async () => {
|
|
606
|
-
const user = userEvent.setup();
|
|
607
|
-
render(
|
|
608
|
-
<SideNavHeading
|
|
609
|
-
heading="Product"
|
|
610
|
-
headingHref="/product"
|
|
611
|
-
menu={menuItems}
|
|
612
|
-
/>,
|
|
613
|
-
);
|
|
614
|
-
await user.click(screen.getByRole('button', {name: 'Open menu'}));
|
|
615
|
-
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
616
|
-
const menu = document.querySelector('[role="menu"]');
|
|
617
|
-
expect(menu).not.toBeNull();
|
|
618
|
-
expect(menu).toHaveAttribute('aria-label', 'Product');
|
|
619
|
-
for (const child of Array.from(menu!.children)) {
|
|
620
|
-
expect(child).toHaveAttribute('role', 'menuitem');
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// =============================================================================
|
|
627
|
-
// SideNavHeading — collapsed mode
|
|
628
|
-
// =============================================================================
|
|
629
|
-
|
|
630
|
-
const COLLAPSED_CONTEXT = {
|
|
631
|
-
isCollapsed: true,
|
|
632
|
-
toggle: () => {},
|
|
633
|
-
isCollapsible: true,
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
function CollapsedWrapper({children}: {children: ReactNode}) {
|
|
637
|
-
return (
|
|
638
|
-
<SideNavCollapseContext value={COLLAPSED_CONTEXT}>
|
|
639
|
-
{children}
|
|
640
|
-
</SideNavCollapseContext>
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
describe('SideNavHeading collapsed', () => {
|
|
645
|
-
it('returns null when collapsed without icon', () => {
|
|
646
|
-
const {container} = render(
|
|
647
|
-
<CollapsedWrapper>
|
|
648
|
-
<SideNavHeading heading="My App" />
|
|
649
|
-
</CollapsedWrapper>,
|
|
650
|
-
);
|
|
651
|
-
expect(container.innerHTML).toBe('');
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
it('renders icon when collapsed with icon', () => {
|
|
655
|
-
render(
|
|
656
|
-
<CollapsedWrapper>
|
|
657
|
-
<SideNavHeading
|
|
658
|
-
heading="My App"
|
|
659
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
660
|
-
/>
|
|
661
|
-
</CollapsedWrapper>,
|
|
662
|
-
);
|
|
663
|
-
expect(screen.getByTestId('app-icon')).toBeInTheDocument();
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it('does not show heading text inline when collapsed (only in tooltip)', () => {
|
|
667
|
-
const {container} = render(
|
|
668
|
-
<CollapsedWrapper>
|
|
669
|
-
<SideNavHeading
|
|
670
|
-
heading="My App"
|
|
671
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
672
|
-
/>
|
|
673
|
-
</CollapsedWrapper>,
|
|
674
|
-
);
|
|
675
|
-
// The heading text should not appear as a visible inline element
|
|
676
|
-
// (it exists only in the tooltip for accessibility)
|
|
677
|
-
const headingSpans = container.querySelectorAll('span');
|
|
678
|
-
const inlineHeadingText = Array.from(headingSpans).find(
|
|
679
|
-
el =>
|
|
680
|
-
el.textContent === 'My App' &&
|
|
681
|
-
!el.closest('[role="tooltip"]') &&
|
|
682
|
-
!el.hasAttribute('data-tooltip'),
|
|
683
|
-
);
|
|
684
|
-
expect(inlineHeadingText).toBeUndefined();
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it('renders as link when collapsed with headingHref', () => {
|
|
688
|
-
render(
|
|
689
|
-
<CollapsedWrapper>
|
|
690
|
-
<SideNavHeading
|
|
691
|
-
heading="My App"
|
|
692
|
-
headingHref="/home"
|
|
693
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
694
|
-
/>
|
|
695
|
-
</CollapsedWrapper>,
|
|
696
|
-
);
|
|
697
|
-
const link = screen.getByRole('link');
|
|
698
|
-
expect(link).toHaveAttribute('href', '/home');
|
|
699
|
-
expect(link).toHaveAttribute('aria-label', 'My App');
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it('does not show chevron when collapsed', () => {
|
|
703
|
-
const {container} = render(
|
|
704
|
-
<CollapsedWrapper>
|
|
705
|
-
<SideNavHeading
|
|
706
|
-
heading="My App"
|
|
707
|
-
headingHref="/home"
|
|
708
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
709
|
-
menu={<div>Menu</div>}
|
|
710
|
-
/>
|
|
711
|
-
</CollapsedWrapper>,
|
|
712
|
-
);
|
|
713
|
-
const svg = container.querySelector('svg');
|
|
714
|
-
expect(svg).not.toBeInTheDocument();
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
it('passes data-testid when collapsed', () => {
|
|
718
|
-
render(
|
|
719
|
-
<CollapsedWrapper>
|
|
720
|
-
<SideNavHeading
|
|
721
|
-
heading="My App"
|
|
722
|
-
icon={<span>🏠</span>}
|
|
723
|
-
data-testid="nav-header"
|
|
724
|
-
/>
|
|
725
|
-
</CollapsedWrapper>,
|
|
726
|
-
);
|
|
727
|
-
expect(screen.getByTestId('nav-header')).toBeInTheDocument();
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
it('collapsed menu popup is not a dialog and scopes role="menu" to menu items', async () => {
|
|
731
|
-
const user = userEvent.setup();
|
|
732
|
-
render(
|
|
733
|
-
<CollapsedWrapper>
|
|
734
|
-
<SideNavHeading
|
|
735
|
-
heading="My App"
|
|
736
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
737
|
-
menu={
|
|
738
|
-
<>
|
|
739
|
-
<div role="menuitem">Alpha</div>
|
|
740
|
-
<div role="menuitem">Beta</div>
|
|
741
|
-
</>
|
|
742
|
-
}
|
|
743
|
-
/>
|
|
744
|
-
</CollapsedWrapper>,
|
|
745
|
-
);
|
|
746
|
-
await user.click(screen.getByRole('button', {name: 'My App'}));
|
|
747
|
-
// No modal dialog wrapper around the menu popup.
|
|
748
|
-
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
749
|
-
expect(document.querySelector('[aria-modal="true"]')).toBeNull();
|
|
750
|
-
// role="menu" is scoped to the menu items and has a direct name.
|
|
751
|
-
const menu = document.querySelector('[role="menu"]');
|
|
752
|
-
expect(menu).not.toBeNull();
|
|
753
|
-
expect(menu).toHaveAttribute('aria-label', 'My App');
|
|
754
|
-
const children = Array.from(menu!.children);
|
|
755
|
-
expect(children.length).toBeGreaterThan(0);
|
|
756
|
-
for (const child of children) {
|
|
757
|
-
expect(child).toHaveAttribute('role', 'menuitem');
|
|
758
|
-
}
|
|
759
|
-
// No button (trigger or heading replica) lives inside the menu.
|
|
760
|
-
for (const button of Array.from(document.querySelectorAll('button'))) {
|
|
761
|
-
expect(menu!.contains(button)).toBe(false);
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
it('anchors the collapsed icon-only trigger so the popover positions against it', () => {
|
|
766
|
-
render(
|
|
767
|
-
<CollapsedWrapper>
|
|
768
|
-
<SideNavHeading
|
|
769
|
-
heading="My App"
|
|
770
|
-
icon={<span data-testid="app-icon">🏠</span>}
|
|
771
|
-
menu={<div role="menuitem">Alpha</div>}
|
|
772
|
-
/>
|
|
773
|
-
</CollapsedWrapper>,
|
|
774
|
-
);
|
|
775
|
-
const trigger = screen.getByRole('button', {name: 'My App'});
|
|
776
|
-
// The same element also carries the collapsed-item Tooltip's own
|
|
777
|
-
// anchor-name (via anchorRef={collapsedItemRef}), so a plain
|
|
778
|
-
// non-empty check would pass even when the menu popover itself isn't
|
|
779
|
-
// anchored. Assert the popover's own position-anchor id specifically
|
|
780
|
-
// appears in the trigger's (possibly multi-value) anchor-name list.
|
|
781
|
-
const popoverEl = document.querySelector('[popover]') as HTMLElement;
|
|
782
|
-
const anchorId = popoverEl.style.positionAnchor;
|
|
783
|
-
expect(anchorId).not.toBe('');
|
|
784
|
-
const triggerAnchorNames = trigger.style.anchorName
|
|
785
|
-
.split(',')
|
|
786
|
-
.map(s => s.trim());
|
|
787
|
-
expect(triggerAnchorNames).toContain(anchorId);
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
// =============================================================================
|
|
792
|
-
// SideNavHeading — headerEndContent
|
|
793
|
-
// =============================================================================
|
|
794
|
-
|
|
795
|
-
describe('SideNavHeading headerEndContent', () => {
|
|
796
|
-
it('renders headerEndContent in the default static path', () => {
|
|
797
|
-
render(
|
|
798
|
-
<SideNavHeading
|
|
799
|
-
heading="My App"
|
|
800
|
-
headerEndContent={<span data-testid="end-badge">3</span>}
|
|
801
|
-
/>,
|
|
802
|
-
);
|
|
803
|
-
expect(screen.getByTestId('end-badge')).toBeInTheDocument();
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
it('renders headerEndContent beside the link, not inside it, in isWholeHeadingLink path', () => {
|
|
807
|
-
const onPress = vi.fn();
|
|
808
|
-
render(
|
|
809
|
-
<SideNavHeading
|
|
810
|
-
heading="My App"
|
|
811
|
-
headingHref="/home"
|
|
812
|
-
headerEndContent={
|
|
813
|
-
<button type="button" onClick={onPress}>
|
|
814
|
-
Create
|
|
815
|
-
</button>
|
|
816
|
-
}
|
|
817
|
-
/>,
|
|
818
|
-
);
|
|
819
|
-
const link = screen.getByRole('link', {name: 'My App'});
|
|
820
|
-
expect(link).toHaveAttribute('href', '/home');
|
|
821
|
-
// A control inside a link is invalid and its press would follow the link.
|
|
822
|
-
const button = screen.getByRole('button', {name: 'Create'});
|
|
823
|
-
expect(button.closest('a')).toBeNull();
|
|
824
|
-
expect(link.parentElement).toBe(button.parentElement?.parentElement);
|
|
825
|
-
fireEvent.click(button);
|
|
826
|
-
expect(onPress).toHaveBeenCalledTimes(1);
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
it('renders headerEndContent in isWholeHeadingTrigger path', () => {
|
|
830
|
-
render(
|
|
831
|
-
<SideNavHeading
|
|
832
|
-
heading="My App"
|
|
833
|
-
menu={<div>Menu</div>}
|
|
834
|
-
headerEndContent={<span data-testid="end-badge">3</span>}
|
|
835
|
-
/>,
|
|
836
|
-
);
|
|
837
|
-
const badge = screen.getByTestId('end-badge');
|
|
838
|
-
expect(badge).toBeInTheDocument();
|
|
839
|
-
// Badge renders inside the heading container (div), alongside the chevron button
|
|
840
|
-
expect(badge.closest('[class]')).not.toBeNull();
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
it('renders headerEndContent in mixed mode (menu + href)', () => {
|
|
844
|
-
render(
|
|
845
|
-
<SideNavHeading
|
|
846
|
-
heading="My App"
|
|
847
|
-
headingHref="/home"
|
|
848
|
-
menu={<div>Menu</div>}
|
|
849
|
-
headerEndContent={<span data-testid="end-badge">3</span>}
|
|
850
|
-
/>,
|
|
851
|
-
);
|
|
852
|
-
expect(screen.getByTestId('end-badge')).toBeInTheDocument();
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
it('renders headerEndContent with independent links (no menu)', () => {
|
|
856
|
-
render(
|
|
857
|
-
<SideNavHeading
|
|
858
|
-
heading="Product"
|
|
859
|
-
headingHref="/product"
|
|
860
|
-
superheading="Suite"
|
|
861
|
-
superheadingHref="/suite"
|
|
862
|
-
headerEndContent={<span data-testid="end-badge">3</span>}
|
|
863
|
-
/>,
|
|
864
|
-
);
|
|
865
|
-
expect(screen.getByTestId('end-badge')).toBeInTheDocument();
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
it('hides headerEndContent when collapsed', () => {
|
|
869
|
-
render(
|
|
870
|
-
<CollapsedWrapper>
|
|
871
|
-
<SideNavHeading
|
|
872
|
-
heading="My App"
|
|
873
|
-
icon={<span>🏠</span>}
|
|
874
|
-
headerEndContent={<span data-testid="end-badge">3</span>}
|
|
875
|
-
/>
|
|
876
|
-
</CollapsedWrapper>,
|
|
877
|
-
);
|
|
878
|
-
expect(screen.queryByTestId('end-badge')).not.toBeInTheDocument();
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
// =============================================================================
|
|
883
|
-
// SideNavHeading — truncation tooltips
|
|
884
|
-
// =============================================================================
|
|
885
|
-
|
|
886
|
-
describe('SideNavHeading truncation tooltips', () => {
|
|
887
|
-
it('attaches truncation refs to heading text spans', () => {
|
|
888
|
-
render(
|
|
889
|
-
<SideNavHeading
|
|
890
|
-
heading="My App"
|
|
891
|
-
superheading="Acme Corp"
|
|
892
|
-
subheading="admin@acme.com"
|
|
893
|
-
/>,
|
|
894
|
-
);
|
|
895
|
-
// All three text spans should be present
|
|
896
|
-
expect(screen.getByText('My App')).toBeInTheDocument();
|
|
897
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
898
|
-
expect(screen.getByText('admin@acme.com')).toBeInTheDocument();
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
it('does not crash with truncation hooks when only heading is provided', () => {
|
|
902
|
-
render(<SideNavHeading heading="My App" />);
|
|
903
|
-
expect(screen.getByText('My App')).toBeInTheDocument();
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
// =============================================================================
|
|
908
|
-
// SideNavItem
|
|
909
|
-
// =============================================================================
|
|
910
|
-
|
|
911
|
-
describe('SideNavItem', () => {
|
|
912
|
-
it('renders label text', () => {
|
|
913
|
-
render(<SideNavItem label="Dashboard" />);
|
|
914
|
-
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
it('renders as link when href is provided', () => {
|
|
918
|
-
render(<SideNavItem label="Dashboard" href="/dashboard" />);
|
|
919
|
-
const link = screen.getByRole('link');
|
|
920
|
-
expect(link).toHaveAttribute('href', '/dashboard');
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
it('renders as button when no href', () => {
|
|
924
|
-
render(<SideNavItem label="Dashboard" />);
|
|
925
|
-
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
it('sets aria-current="page" when selected', () => {
|
|
929
|
-
render(<SideNavItem label="Dashboard" isSelected />);
|
|
930
|
-
const button = screen.getByRole('button');
|
|
931
|
-
expect(button).toHaveAttribute('aria-current', 'page');
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
it('does not set aria-current when not selected', () => {
|
|
935
|
-
render(<SideNavItem label="Dashboard" />);
|
|
936
|
-
const button = screen.getByRole('button');
|
|
937
|
-
expect(button).not.toHaveAttribute('aria-current');
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
it('disables the button when isDisabled', () => {
|
|
941
|
-
render(<SideNavItem label="Dashboard" isDisabled />);
|
|
942
|
-
const button = screen.getByRole('button');
|
|
943
|
-
expect(button).toBeDisabled();
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
it('calls onClick handler', async () => {
|
|
947
|
-
const user = userEvent.setup();
|
|
948
|
-
const handleClick = vi.fn();
|
|
949
|
-
render(<SideNavItem label="Dashboard" onClick={handleClick} />);
|
|
950
|
-
await user.click(screen.getByRole('button'));
|
|
951
|
-
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
it('renders endContent', () => {
|
|
955
|
-
render(
|
|
956
|
-
<SideNavItem
|
|
957
|
-
label="Projects"
|
|
958
|
-
endContent={<span data-testid="badge">3</span>}
|
|
959
|
-
/>,
|
|
960
|
-
);
|
|
961
|
-
expect(screen.getByTestId('badge')).toBeInTheDocument();
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
it('renders nested children', () => {
|
|
965
|
-
render(
|
|
966
|
-
<SideNavItem label="Settings">
|
|
967
|
-
<SideNavItem label="General" />
|
|
968
|
-
<SideNavItem label="Security" />
|
|
969
|
-
</SideNavItem>,
|
|
970
|
-
);
|
|
971
|
-
expect(screen.getByText('General')).toBeInTheDocument();
|
|
972
|
-
expect(screen.getByText('Security')).toBeInTheDocument();
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
it('keeps nested rows full width and indents their content inside the row', () => {
|
|
976
|
-
render(
|
|
977
|
-
<SideNavItem label="Settings">
|
|
978
|
-
<SideNavItem label="General" href="/settings/general">
|
|
979
|
-
<SideNavItem label="Advanced" href="/settings/general/advanced" />
|
|
980
|
-
</SideNavItem>
|
|
981
|
-
</SideNavItem>,
|
|
982
|
-
);
|
|
983
|
-
|
|
984
|
-
// The children group adds no inset of its own, so a nested row's hover
|
|
985
|
-
// and selected fill spans the nav list like a top-level row's.
|
|
986
|
-
const groups = screen.getAllByRole('group', {hidden: true});
|
|
987
|
-
for (const group of groups) {
|
|
988
|
-
expect(declaredOn(group, 'padding-inline-start')).toEqual([]);
|
|
989
|
-
expect(
|
|
990
|
-
declaredOn(group.firstElementChild!, 'padding-inline-start'),
|
|
991
|
-
).toEqual([]);
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// The indent lives inside each row instead, one step per nesting level.
|
|
995
|
-
// An ordinary row is the link itself: its own padding plus the steps.
|
|
996
|
-
const top = screen.getByRole('button', {name: 'Settings'});
|
|
997
|
-
const general = screen.getByRole('link', {name: 'General'});
|
|
998
|
-
const advanced = screen.getByRole('link', {name: 'Advanced'});
|
|
999
|
-
expect(indentOf(top)).toBeNull();
|
|
1000
|
-
expect(indentOf(advanced)).toBe(
|
|
1001
|
-
'calc(var(--spacing-2) + 2 * var(--spacing-6))',
|
|
1002
|
-
);
|
|
1003
|
-
|
|
1004
|
-
// A split row (own href plus children) keeps the wrapper's base padding
|
|
1005
|
-
// and puts the steps on the link, so the whole indent is clickable
|
|
1006
|
-
// instead of dead space that still shows hover and press feedback.
|
|
1007
|
-
expect(indentOf(general.parentElement!)).toBeNull();
|
|
1008
|
-
expect(indentOf(general)).toBe('calc(1 * var(--spacing-6))');
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
it('clips the children group only while it is collapsed', async () => {
|
|
1012
|
-
// Nested rows sit flush with the group, so a permanent clip would cut off
|
|
1013
|
-
// their focus ring. The group clips for the collapse animation only.
|
|
1014
|
-
const user = userEvent.setup();
|
|
1015
|
-
render(
|
|
1016
|
-
<SideNavItem label="Settings" collapsible>
|
|
1017
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1018
|
-
</SideNavItem>,
|
|
1019
|
-
);
|
|
1020
|
-
const group = () => screen.getByRole('group', {hidden: true});
|
|
1021
|
-
expect(declaredOn(group(), 'overflow')).toEqual(['visible']);
|
|
1022
|
-
expect(declaredOn(group().firstElementChild!, 'overflow')).toEqual([]);
|
|
1023
|
-
|
|
1024
|
-
await user.click(screen.getByRole('button', {name: 'Settings'}));
|
|
1025
|
-
expect(declaredOn(group(), 'overflow')).toEqual(['hidden']);
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
it('puts the indent on the primary link of a nested actions row', () => {
|
|
1029
|
-
render(
|
|
1030
|
-
<SideNavItem label="Projects">
|
|
1031
|
-
<SideNavItem
|
|
1032
|
-
label="Alpha"
|
|
1033
|
-
href="/projects/alpha"
|
|
1034
|
-
actions={
|
|
1035
|
-
<button type="button" aria-label="Alpha actions">
|
|
1036
|
-
More
|
|
1037
|
-
</button>
|
|
1038
|
-
}
|
|
1039
|
-
/>
|
|
1040
|
-
</SideNavItem>,
|
|
1041
|
-
);
|
|
1042
|
-
const alpha = screen.getByRole('link', {name: 'Alpha'});
|
|
1043
|
-
expect(indentOf(alpha.parentElement!)).toBeNull();
|
|
1044
|
-
expect(indentOf(alpha)).toBe('calc(1 * var(--spacing-6))');
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
it("composes a caller's style with the indent on a nested row", () => {
|
|
1048
|
-
render(
|
|
1049
|
-
<SideNavItem label="Settings">
|
|
1050
|
-
<SideNavItem
|
|
1051
|
-
label="General"
|
|
1052
|
-
href="/settings/general"
|
|
1053
|
-
style={{color: 'rebeccapurple'}}
|
|
1054
|
-
/>
|
|
1055
|
-
<SideNavItem
|
|
1056
|
-
label="Security"
|
|
1057
|
-
href="/settings/security"
|
|
1058
|
-
style={{color: 'rebeccapurple'}}>
|
|
1059
|
-
<SideNavItem label="Keys" href="/settings/security/keys" />
|
|
1060
|
-
</SideNavItem>
|
|
1061
|
-
</SideNavItem>,
|
|
1062
|
-
);
|
|
1063
|
-
// Ordinary row: the link is the row.
|
|
1064
|
-
const general = screen.getByRole('link', {name: 'General'});
|
|
1065
|
-
expect(general.style.color).toBe('rebeccapurple');
|
|
1066
|
-
expect(indentOf(general)).toBe(
|
|
1067
|
-
'calc(var(--spacing-2) + 1 * var(--spacing-6))',
|
|
1068
|
-
);
|
|
1069
|
-
// Split row: the caller's style still lands on the primary link.
|
|
1070
|
-
const security = screen.getByRole('link', {name: 'Security'});
|
|
1071
|
-
expect(security.style.color).toBe('rebeccapurple');
|
|
1072
|
-
expect(indentOf(security)).toBe('calc(1 * var(--spacing-6))');
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
it("keeps a caller's className on the row element", () => {
|
|
1076
|
-
render(
|
|
1077
|
-
<SideNavItem label="Settings">
|
|
1078
|
-
<SideNavItem
|
|
1079
|
-
label="General"
|
|
1080
|
-
href="/settings/general"
|
|
1081
|
-
className="caller-row"
|
|
1082
|
-
/>
|
|
1083
|
-
<SideNavItem
|
|
1084
|
-
label="Security"
|
|
1085
|
-
href="/settings/security"
|
|
1086
|
-
className="caller-row">
|
|
1087
|
-
<SideNavItem label="Keys" href="/settings/security/keys" />
|
|
1088
|
-
</SideNavItem>
|
|
1089
|
-
</SideNavItem>,
|
|
1090
|
-
);
|
|
1091
|
-
// Ordinary row: the link is the row, and the caller's class sits beside
|
|
1092
|
-
// the row's own classes instead of being replaced by them.
|
|
1093
|
-
const general = screen.getByRole('link', {name: 'General'});
|
|
1094
|
-
expect(general).toHaveClass('caller-row');
|
|
1095
|
-
expect(general).toHaveClass('astryx-side-nav-item');
|
|
1096
|
-
// Split row: the class lands on the primary link, as `style` does.
|
|
1097
|
-
expect(screen.getByRole('link', {name: 'Security'})).toHaveClass(
|
|
1098
|
-
'caller-row',
|
|
1099
|
-
);
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
it("keeps a caller's className on a collapsed rail item", () => {
|
|
1103
|
-
render(
|
|
1104
|
-
<SideNav collapsible={{isCollapsed: true, hasButton: false}}>
|
|
1105
|
-
<SideNavItem
|
|
1106
|
-
label="Home"
|
|
1107
|
-
href="/"
|
|
1108
|
-
icon="calendar"
|
|
1109
|
-
className="caller-row"
|
|
1110
|
-
/>
|
|
1111
|
-
</SideNav>,
|
|
1112
|
-
);
|
|
1113
|
-
expect(screen.getByRole('link', {name: 'Home'})).toHaveClass('caller-row');
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
it("lets a caller's inline-start padding replace the indent", () => {
|
|
1117
|
-
// The caller's `style` composes after the indent: a caller-set
|
|
1118
|
-
// inline-start padding is an inline declaration, so it wins over the
|
|
1119
|
-
// indent's class rule on the element that carries the indent, on both
|
|
1120
|
-
// the ordinary and the split row path.
|
|
1121
|
-
render(
|
|
1122
|
-
<SideNavItem label="Settings">
|
|
1123
|
-
<SideNavItem
|
|
1124
|
-
label="General"
|
|
1125
|
-
href="/settings/general"
|
|
1126
|
-
style={{paddingInlineStart: '20px'}}
|
|
1127
|
-
/>
|
|
1128
|
-
<SideNavItem
|
|
1129
|
-
label="Security"
|
|
1130
|
-
href="/settings/security"
|
|
1131
|
-
style={{paddingInlineStart: '20px'}}>
|
|
1132
|
-
<SideNavItem label="Keys" href="/settings/security/keys" />
|
|
1133
|
-
</SideNavItem>
|
|
1134
|
-
</SideNavItem>,
|
|
1135
|
-
);
|
|
1136
|
-
const general = screen.getByRole('link', {name: 'General'});
|
|
1137
|
-
expect(general.style.paddingInlineStart).toBe('20px');
|
|
1138
|
-
const security = screen.getByRole('link', {name: 'Security'});
|
|
1139
|
-
expect(security.style.paddingInlineStart).toBe('20px');
|
|
1140
|
-
// A row whose caller sets no padding keeps its indent.
|
|
1141
|
-
expect(indentOf(screen.getByRole('link', {name: 'Keys'}))).toBe(
|
|
1142
|
-
'calc(var(--spacing-2) + 2 * var(--spacing-6))',
|
|
1143
|
-
);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
it('indents a small nested row from the small row padding', () => {
|
|
1147
|
-
render(
|
|
1148
|
-
<SideNavItem label="Settings">
|
|
1149
|
-
<SideNavItem label="General" href="/settings/general" size="sm" />
|
|
1150
|
-
</SideNavItem>,
|
|
1151
|
-
);
|
|
1152
|
-
expect(indentOf(screen.getByRole('link', {name: 'General'}))).toBe(
|
|
1153
|
-
'calc(var(--spacing-1) + 1 * var(--spacing-6))',
|
|
1154
|
-
);
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
it('does not indent the first level of a collapsed rail flyout', async () => {
|
|
1158
|
-
const user = userEvent.setup();
|
|
1159
|
-
renderCollapsed(
|
|
1160
|
-
<SideNavItem label="Settings" icon={StubIcon}>
|
|
1161
|
-
<SideNavItem label="General" href="/settings/general">
|
|
1162
|
-
<SideNavItem label="Advanced" href="/settings/general/advanced" />
|
|
1163
|
-
</SideNavItem>
|
|
1164
|
-
</SideNavItem>,
|
|
1165
|
-
);
|
|
1166
|
-
await user.click(screen.getByRole('button', {name: 'Settings'}));
|
|
1167
|
-
|
|
1168
|
-
// jsdom leaves the native popover `display: none`, hence `hidden: true`.
|
|
1169
|
-
// The flyout lists the parent's children as its own top level.
|
|
1170
|
-
const general = screen.getByRole('link', {name: 'General', hidden: true});
|
|
1171
|
-
const advanced = screen.getByRole('link', {name: 'Advanced', hidden: true});
|
|
1172
|
-
expect(indentOf(general)).toBeNull();
|
|
1173
|
-
expect(indentOf(general.parentElement!)).toBeNull();
|
|
1174
|
-
expect(indentOf(advanced)).toBe(
|
|
1175
|
-
'calc(var(--spacing-2) + 1 * var(--spacing-6))',
|
|
1176
|
-
);
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
it('passes data-testid', () => {
|
|
1180
|
-
render(<SideNavItem label="Dashboard" data-testid="nav-item" />);
|
|
1181
|
-
expect(screen.getByTestId('nav-item')).toBeInTheDocument();
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
it('renders with selected link', () => {
|
|
1185
|
-
render(<SideNavItem label="Dashboard" href="/dashboard" isSelected />);
|
|
1186
|
-
const link = screen.getByRole('link');
|
|
1187
|
-
expect(link).toHaveAttribute('aria-current', 'page');
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
it('places aria-current on the link, not the wrapper, for split-action items', () => {
|
|
1191
|
-
// A collapsible item (has children) WITH a primary href renders the
|
|
1192
|
-
// split-action path: the link and the expand toggle are siblings inside a
|
|
1193
|
-
// wrapper div. aria-current="page" must sit on the focusable link so it is
|
|
1194
|
-
// announced as the current page (navigation-8).
|
|
1195
|
-
render(
|
|
1196
|
-
<SideNavItem label="Reports" href="/reports" isSelected>
|
|
1197
|
-
<SideNavItem label="Weekly" href="/reports/weekly" />
|
|
1198
|
-
</SideNavItem>,
|
|
1199
|
-
);
|
|
1200
|
-
const link = screen.getByRole('link', {name: /Reports/});
|
|
1201
|
-
expect(link).toHaveAttribute('aria-current', 'page');
|
|
1202
|
-
// The wrapper div must NOT carry aria-current.
|
|
1203
|
-
expect(link.closest('[aria-current="page"]')).toBe(link);
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
it('renders custom component when as and href are provided', () => {
|
|
1207
|
-
render(<SideNavItem label="Dashboard" href="/dashboard" as={CustomLink} />);
|
|
1208
|
-
const link = screen.getByRole('link');
|
|
1209
|
-
expect(link).toHaveAttribute('data-custom-link');
|
|
1210
|
-
expect(link).toHaveAttribute('href', '/dashboard');
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
it('still renders button when no href even with as prop', () => {
|
|
1214
|
-
render(<SideNavItem label="Dashboard" as={CustomLink} />);
|
|
1215
|
-
const button = screen.getByRole('button');
|
|
1216
|
-
expect(button).toBeInTheDocument();
|
|
1217
|
-
expect(button).not.toHaveAttribute('data-custom-link');
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
it('renders custom component from LinkProvider when href is provided', () => {
|
|
1221
|
-
render(
|
|
1222
|
-
<LinkProvider component={CustomLink}>
|
|
1223
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
1224
|
-
</LinkProvider>,
|
|
1225
|
-
);
|
|
1226
|
-
const link = screen.getByRole('link');
|
|
1227
|
-
expect(link).toHaveAttribute('data-custom-link');
|
|
1228
|
-
});
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
// =============================================================================
|
|
1232
|
-
// SideNavSection
|
|
1233
|
-
// =============================================================================
|
|
1234
|
-
|
|
1235
|
-
describe('SideNavSection', () => {
|
|
1236
|
-
it('renders with group role', () => {
|
|
1237
|
-
render(
|
|
1238
|
-
<SideNavSection title="Main">
|
|
1239
|
-
<SideNavItem label="Dashboard" />
|
|
1240
|
-
</SideNavSection>,
|
|
1241
|
-
);
|
|
1242
|
-
expect(screen.getByRole('group')).toBeInTheDocument();
|
|
1243
|
-
});
|
|
1244
|
-
|
|
1245
|
-
it('renders heading text', () => {
|
|
1246
|
-
render(
|
|
1247
|
-
<SideNavSection title="Main">
|
|
1248
|
-
<SideNavItem label="Dashboard" />
|
|
1249
|
-
</SideNavSection>,
|
|
1250
|
-
);
|
|
1251
|
-
expect(screen.getByText('Main')).toBeInTheDocument();
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
it('uses aria-labelledby to link title to group', () => {
|
|
1255
|
-
render(
|
|
1256
|
-
<SideNavSection title="Main">
|
|
1257
|
-
<SideNavItem label="Dashboard" />
|
|
1258
|
-
</SideNavSection>,
|
|
1259
|
-
);
|
|
1260
|
-
const group = screen.getByRole('group');
|
|
1261
|
-
const labelId = group.getAttribute('aria-labelledby');
|
|
1262
|
-
expect(labelId).toBeTruthy();
|
|
1263
|
-
const label = document.getElementById(labelId!);
|
|
1264
|
-
expect(label).toHaveTextContent('Main');
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
it('renders subheading', () => {
|
|
1268
|
-
render(
|
|
1269
|
-
<SideNavSection title="Main" subtitle="Primary navigation">
|
|
1270
|
-
<SideNavItem label="Dashboard" />
|
|
1271
|
-
</SideNavSection>,
|
|
1272
|
-
);
|
|
1273
|
-
expect(screen.getByText('Primary navigation')).toBeInTheDocument();
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
it('renders endContent', () => {
|
|
1277
|
-
render(
|
|
1278
|
-
<SideNavSection
|
|
1279
|
-
title="Main"
|
|
1280
|
-
endContent={<span data-testid="section-action">+</span>}>
|
|
1281
|
-
<SideNavItem label="Dashboard" />
|
|
1282
|
-
</SideNavSection>,
|
|
1283
|
-
);
|
|
1284
|
-
expect(screen.getByTestId('section-action')).toBeInTheDocument();
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
it('passes data-testid', () => {
|
|
1288
|
-
render(
|
|
1289
|
-
<SideNavSection title="Main" data-testid="nav-section">
|
|
1290
|
-
<SideNavItem label="Dashboard" />
|
|
1291
|
-
</SideNavSection>,
|
|
1292
|
-
);
|
|
1293
|
-
expect(screen.getByTestId('nav-section')).toBeInTheDocument();
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
it('forwards className to root element', () => {
|
|
1297
|
-
render(
|
|
1298
|
-
<SideNavSection title="Main" className="custom-section">
|
|
1299
|
-
<SideNavItem label="Dashboard" />
|
|
1300
|
-
</SideNavSection>,
|
|
1301
|
-
);
|
|
1302
|
-
const group = screen.getByRole('group');
|
|
1303
|
-
expect(group.className).toContain('custom-section');
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
it('forwards style to root element', () => {
|
|
1307
|
-
render(
|
|
1308
|
-
<SideNavSection title="Main" style={{marginTop: 16}}>
|
|
1309
|
-
<SideNavItem label="Dashboard" />
|
|
1310
|
-
</SideNavSection>,
|
|
1311
|
-
);
|
|
1312
|
-
const group = screen.getByRole('group');
|
|
1313
|
-
expect(group.style.marginTop).toBe('16px');
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
it('forwards arbitrary pass-through attributes (id, aria-*) to root element', () => {
|
|
1317
|
-
render(
|
|
1318
|
-
<SideNavSection title="Main" id="section-1" aria-describedby="hint">
|
|
1319
|
-
<SideNavItem label="Dashboard" />
|
|
1320
|
-
</SideNavSection>,
|
|
1321
|
-
);
|
|
1322
|
-
const group = screen.getByRole('group');
|
|
1323
|
-
expect(group).toHaveAttribute('id', 'section-1');
|
|
1324
|
-
expect(group).toHaveAttribute('aria-describedby', 'hint');
|
|
1325
|
-
});
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// =============================================================================
|
|
1329
|
-
// Resizable
|
|
1330
|
-
// =============================================================================
|
|
1331
|
-
|
|
1332
|
-
describe('SideNav resizable', () => {
|
|
1333
|
-
it('renders drag handle when resizable', () => {
|
|
1334
|
-
render(<SideNav resizable>Content</SideNav>);
|
|
1335
|
-
expect(
|
|
1336
|
-
screen.getByTestId('astryx-sidenav-resize-handle'),
|
|
1337
|
-
).toBeInTheDocument();
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
it('names the nav it resizes with aria-controls (APG window splitter)', () => {
|
|
1341
|
-
render(<SideNav resizable>Content</SideNav>);
|
|
1342
|
-
const nav = screen.getByRole('navigation');
|
|
1343
|
-
const handle = screen.getByRole('separator');
|
|
1344
|
-
expect(nav.id).not.toBe('');
|
|
1345
|
-
expect(handle).toHaveAttribute('aria-controls', nav.id);
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
it("keeps a consumer's nav id and points the handle at it", () => {
|
|
1349
|
-
render(
|
|
1350
|
-
<SideNav resizable id="primary-nav">
|
|
1351
|
-
Content
|
|
1352
|
-
</SideNav>,
|
|
1353
|
-
);
|
|
1354
|
-
expect(screen.getByRole('navigation')).toHaveAttribute('id', 'primary-nav');
|
|
1355
|
-
expect(screen.getByRole('separator')).toHaveAttribute(
|
|
1356
|
-
'aria-controls',
|
|
1357
|
-
'primary-nav',
|
|
1358
|
-
);
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
it('keeps the generated nav id when a wrapper forwards id={undefined}', () => {
|
|
1362
|
-
render(
|
|
1363
|
-
<SideNav resizable id={undefined}>
|
|
1364
|
-
Content
|
|
1365
|
-
</SideNav>,
|
|
1366
|
-
);
|
|
1367
|
-
const nav = screen.getByRole('navigation');
|
|
1368
|
-
expect(nav.id).not.toBe('');
|
|
1369
|
-
expect(screen.getByRole('separator')).toHaveAttribute(
|
|
1370
|
-
'aria-controls',
|
|
1371
|
-
nav.id,
|
|
1372
|
-
);
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
it('clips the overlay drag handle within the resizable nav bounds', () => {
|
|
1376
|
-
render(<SideNav resizable>Content</SideNav>);
|
|
1377
|
-
const nav = screen.getByRole('navigation');
|
|
1378
|
-
const container = nav.parentElement;
|
|
1379
|
-
const handle = screen.getByTestId('astryx-sidenav-resize-handle');
|
|
1380
|
-
|
|
1381
|
-
expect(container).toContainElement(handle);
|
|
1382
|
-
expect(getComputedStyle(container!).overflow).toBe('clip');
|
|
1383
|
-
});
|
|
1384
|
-
|
|
1385
|
-
it('does not render drag handle without resizable', () => {
|
|
1386
|
-
render(<SideNav>Content</SideNav>);
|
|
1387
|
-
expect(
|
|
1388
|
-
screen.queryByTestId('astryx-sidenav-resize-handle'),
|
|
1389
|
-
).not.toBeInTheDocument();
|
|
1390
|
-
});
|
|
1391
|
-
|
|
1392
|
-
it('does not render drag handle when collapsed', () => {
|
|
1393
|
-
render(
|
|
1394
|
-
<SideNav
|
|
1395
|
-
resizable
|
|
1396
|
-
collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}>
|
|
1397
|
-
Content
|
|
1398
|
-
</SideNav>,
|
|
1399
|
-
);
|
|
1400
|
-
expect(
|
|
1401
|
-
screen.queryByTestId('astryx-sidenav-resize-handle'),
|
|
1402
|
-
).not.toBeInTheDocument();
|
|
1403
|
-
});
|
|
1404
|
-
|
|
1405
|
-
it('calls onWidthChange after drag', () => {
|
|
1406
|
-
const handleWidthChange = vi.fn();
|
|
1407
|
-
render(
|
|
1408
|
-
<SideNav resizable={{onWidthChange: handleWidthChange}}>Content</SideNav>,
|
|
1409
|
-
);
|
|
1410
|
-
const handle = screen.getByTestId('astryx-sidenav-resize-handle');
|
|
1411
|
-
// The pointer event handler is on the hit area child inside the handle.
|
|
1412
|
-
const hitArea = handle.firstElementChild as HTMLElement;
|
|
1413
|
-
|
|
1414
|
-
act(() => {
|
|
1415
|
-
fireEvent.pointerDown(hitArea, {pointerId: 1, clientX: 260});
|
|
1416
|
-
fireEvent.pointerMove(hitArea, {pointerId: 1, clientX: 310});
|
|
1417
|
-
fireEvent.pointerUp(hitArea, {pointerId: 1, clientX: 310});
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
expect(handleWidthChange).toHaveBeenCalledTimes(1);
|
|
1421
|
-
expect(handleWidthChange).toHaveBeenCalledWith(expect.any(Number));
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
it('respects defaultWidth', () => {
|
|
1425
|
-
render(<SideNav resizable={{defaultWidth: 300}}>Content</SideNav>);
|
|
1426
|
-
const nav = screen.getByRole('navigation');
|
|
1427
|
-
expect(nav.style.width).toBe('300px');
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
it('drag handle has separator role', () => {
|
|
1431
|
-
render(<SideNav resizable>Content</SideNav>);
|
|
1432
|
-
expect(screen.getByRole('separator')).toBeInTheDocument();
|
|
1433
|
-
});
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
// =============================================================================
|
|
1437
|
-
// Integration
|
|
1438
|
-
// =============================================================================
|
|
1439
|
-
|
|
1440
|
-
describe('SideNav integration', () => {
|
|
1441
|
-
it('renders a complete page nav', () => {
|
|
1442
|
-
render(
|
|
1443
|
-
<SideNav
|
|
1444
|
-
header={<SideNavHeading heading="My App" />}
|
|
1445
|
-
topContent={<button type="button">Create</button>}
|
|
1446
|
-
footer={<div data-testid="promo">Promo</div>}
|
|
1447
|
-
footerIcons={<button type="button">Help</button>}>
|
|
1448
|
-
<SideNavSection title="Main">
|
|
1449
|
-
<SideNavItem label="Dashboard" isSelected />
|
|
1450
|
-
<SideNavItem label="Projects" />
|
|
1451
|
-
</SideNavSection>
|
|
1452
|
-
<SideNavSection title="Settings">
|
|
1453
|
-
<SideNavItem label="General" />
|
|
1454
|
-
</SideNavSection>
|
|
1455
|
-
</SideNav>,
|
|
1456
|
-
);
|
|
1457
|
-
|
|
1458
|
-
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
1459
|
-
expect(screen.getByText('My App')).toBeInTheDocument();
|
|
1460
|
-
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
1461
|
-
expect(screen.getByText('Projects')).toBeInTheDocument();
|
|
1462
|
-
expect(screen.getByText('General')).toBeInTheDocument();
|
|
1463
|
-
expect(screen.getByTestId('promo')).toBeInTheDocument();
|
|
1464
|
-
});
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
// Stub icon for testing
|
|
1468
|
-
const StubIcon = () => <svg data-testid="stub-icon" />;
|
|
1469
|
-
|
|
1470
|
-
/** Helper to render inside a collapsed SideNav context */
|
|
1471
|
-
function renderCollapsed(ui: React.ReactElement) {
|
|
1472
|
-
return render(
|
|
1473
|
-
<SideNavCollapseContext
|
|
1474
|
-
value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
|
|
1475
|
-
{ui}
|
|
1476
|
-
</SideNavCollapseContext>,
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/** Helper to render inside an expanded SideNav context */
|
|
1481
|
-
function renderExpanded(ui: React.ReactElement) {
|
|
1482
|
-
return render(
|
|
1483
|
-
<SideNavCollapseContext
|
|
1484
|
-
value={{isCollapsed: false, toggle: () => {}, isCollapsible: true}}>
|
|
1485
|
-
{ui}
|
|
1486
|
-
</SideNavCollapseContext>,
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
// =============================================================================
|
|
1491
|
-
// SideNavItem — Collapsed mode
|
|
1492
|
-
// =============================================================================
|
|
1493
|
-
|
|
1494
|
-
describe('SideNavItem (collapsed)', () => {
|
|
1495
|
-
it('hides items without icons when collapsed', () => {
|
|
1496
|
-
const {container} = renderCollapsed(<SideNavItem label="No Icon Item" />);
|
|
1497
|
-
expect(screen.queryByText('No Icon Item')).not.toBeInTheDocument();
|
|
1498
|
-
expect(container.querySelector('[data-xds="side-nav-item"]')).toBeNull();
|
|
1499
|
-
});
|
|
1500
|
-
|
|
1501
|
-
it('renders icon-only button when collapsed with icon and no children', () => {
|
|
1502
|
-
renderCollapsed(
|
|
1503
|
-
<SideNavItem label="Dashboard" icon={StubIcon} data-testid="item" />,
|
|
1504
|
-
);
|
|
1505
|
-
// Should have an element with aria-label (icon-only)
|
|
1506
|
-
const item = screen.getByLabelText('Dashboard');
|
|
1507
|
-
expect(item).toBeInTheDocument();
|
|
1508
|
-
// Icon should be rendered
|
|
1509
|
-
expect(screen.getByTestId('stub-icon')).toBeInTheDocument();
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
it('renders collapsed link when href is provided', () => {
|
|
1513
|
-
renderCollapsed(
|
|
1514
|
-
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
|
|
1515
|
-
);
|
|
1516
|
-
const link = screen.getByRole('link');
|
|
1517
|
-
expect(link).toHaveAttribute('href', '/dashboard');
|
|
1518
|
-
expect(link).toHaveAttribute('aria-label', 'Dashboard');
|
|
1519
|
-
});
|
|
1520
|
-
|
|
1521
|
-
it('renders popover trigger when collapsed with icon and children', () => {
|
|
1522
|
-
renderCollapsed(
|
|
1523
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1524
|
-
<SideNavItem label="General" />
|
|
1525
|
-
<SideNavItem label="Security" />
|
|
1526
|
-
</SideNavItem>,
|
|
1527
|
-
);
|
|
1528
|
-
const trigger = screen.getByTestId('parent');
|
|
1529
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
|
|
1530
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1531
|
-
expect(trigger).toHaveAttribute('aria-label', 'Settings');
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
describe('consumer aria-label (#5641)', () => {
|
|
1535
|
-
it('keeps a consumer aria-label on the collapsed link, instead of falling back to label', () => {
|
|
1536
|
-
renderCollapsed(
|
|
1537
|
-
<SideNavItem
|
|
1538
|
-
label="Home"
|
|
1539
|
-
icon={StubIcon}
|
|
1540
|
-
href="/home"
|
|
1541
|
-
aria-label="Open Home, 3 items need attention"
|
|
1542
|
-
/>,
|
|
1543
|
-
);
|
|
1544
|
-
expect(screen.getByRole('link')).toHaveAttribute(
|
|
1545
|
-
'aria-label',
|
|
1546
|
-
'Open Home, 3 items need attention',
|
|
1547
|
-
);
|
|
1548
|
-
});
|
|
1549
|
-
|
|
1550
|
-
it('keeps a consumer aria-label on the collapsed button, instead of falling back to label', () => {
|
|
1551
|
-
renderCollapsed(
|
|
1552
|
-
<SideNavItem
|
|
1553
|
-
label="Home"
|
|
1554
|
-
icon={StubIcon}
|
|
1555
|
-
aria-label="Open Home, 3 items need attention"
|
|
1556
|
-
/>,
|
|
1557
|
-
);
|
|
1558
|
-
expect(screen.getByRole('button')).toHaveAttribute(
|
|
1559
|
-
'aria-label',
|
|
1560
|
-
'Open Home, 3 items need attention',
|
|
1561
|
-
);
|
|
1562
|
-
});
|
|
1563
|
-
|
|
1564
|
-
it('keeps a consumer aria-label on the collapsed popover trigger, instead of falling back to label', () => {
|
|
1565
|
-
renderCollapsed(
|
|
1566
|
-
<SideNavItem
|
|
1567
|
-
label="Settings"
|
|
1568
|
-
icon={StubIcon}
|
|
1569
|
-
data-testid="parent"
|
|
1570
|
-
aria-label="Settings, 2 updates available">
|
|
1571
|
-
<SideNavItem label="General" />
|
|
1572
|
-
</SideNavItem>,
|
|
1573
|
-
);
|
|
1574
|
-
expect(screen.getByTestId('parent')).toHaveAttribute(
|
|
1575
|
-
'aria-label',
|
|
1576
|
-
'Settings, 2 updates available',
|
|
1577
|
-
);
|
|
1578
|
-
});
|
|
1579
|
-
|
|
1580
|
-
it('falls back to label when a consumer aria-label is empty or whitespace', () => {
|
|
1581
|
-
const {rerender} = renderCollapsed(
|
|
1582
|
-
<SideNavItem
|
|
1583
|
-
label="Dashboard"
|
|
1584
|
-
icon={StubIcon}
|
|
1585
|
-
href="/dashboard"
|
|
1586
|
-
aria-label=""
|
|
1587
|
-
/>,
|
|
1588
|
-
);
|
|
1589
|
-
expect(screen.getByRole('link')).toHaveAttribute(
|
|
1590
|
-
'aria-label',
|
|
1591
|
-
'Dashboard',
|
|
1592
|
-
);
|
|
1593
|
-
|
|
1594
|
-
rerender(
|
|
1595
|
-
<SideNavCollapseContext value={COLLAPSED_CONTEXT}>
|
|
1596
|
-
<SideNavItem label="Dashboard" icon={StubIcon} aria-label=" " />
|
|
1597
|
-
</SideNavCollapseContext>,
|
|
1598
|
-
);
|
|
1599
|
-
expect(screen.getByRole('button')).toHaveAttribute(
|
|
1600
|
-
'aria-label',
|
|
1601
|
-
'Dashboard',
|
|
1602
|
-
);
|
|
1603
|
-
});
|
|
1604
|
-
|
|
1605
|
-
it('still falls back to label when no consumer aria-label is given', () => {
|
|
1606
|
-
renderCollapsed(
|
|
1607
|
-
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
|
|
1608
|
-
);
|
|
1609
|
-
expect(screen.getByRole('link')).toHaveAttribute(
|
|
1610
|
-
'aria-label',
|
|
1611
|
-
'Dashboard',
|
|
1612
|
-
);
|
|
1613
|
-
});
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
it('opens popover on click showing children in expanded form', async () => {
|
|
1617
|
-
const user = userEvent.setup();
|
|
1618
|
-
renderCollapsed(
|
|
1619
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1620
|
-
<SideNavItem label="General" data-testid="child-general" />
|
|
1621
|
-
<SideNavItem label="Security" data-testid="child-security" />
|
|
1622
|
-
</SideNavItem>,
|
|
1623
|
-
);
|
|
1624
|
-
await user.click(screen.getByTestId('parent'));
|
|
1625
|
-
|
|
1626
|
-
// Children should be visible in expanded form (label text visible)
|
|
1627
|
-
expect(screen.getByText('General')).toBeInTheDocument();
|
|
1628
|
-
expect(screen.getByText('Security')).toBeInTheDocument();
|
|
1629
|
-
});
|
|
1630
|
-
|
|
1631
|
-
it('shows parent label as header in the popover', async () => {
|
|
1632
|
-
const user = userEvent.setup();
|
|
1633
|
-
renderCollapsed(
|
|
1634
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1635
|
-
<SideNavItem label="General" />
|
|
1636
|
-
</SideNavItem>,
|
|
1637
|
-
);
|
|
1638
|
-
await user.click(screen.getByTestId('parent'));
|
|
1639
|
-
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
1640
|
-
});
|
|
1641
|
-
|
|
1642
|
-
it('does not render children without icon when collapsed', () => {
|
|
1643
|
-
renderCollapsed(
|
|
1644
|
-
<SideNavItem label="Settings">
|
|
1645
|
-
<SideNavItem label="General" />
|
|
1646
|
-
<SideNavItem label="Security" />
|
|
1647
|
-
</SideNavItem>,
|
|
1648
|
-
);
|
|
1649
|
-
expect(screen.queryByText('Settings')).not.toBeInTheDocument();
|
|
1650
|
-
expect(screen.queryByText('General')).not.toBeInTheDocument();
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
it('renders normally when not collapsed', () => {
|
|
1654
|
-
renderExpanded(
|
|
1655
|
-
<SideNavItem label="Dashboard" icon={StubIcon}>
|
|
1656
|
-
<SideNavItem label="General" />
|
|
1657
|
-
</SideNavItem>,
|
|
1658
|
-
);
|
|
1659
|
-
expect(screen.getAllByText('Dashboard').length).toBeGreaterThanOrEqual(1);
|
|
1660
|
-
expect(screen.getByText('General')).toBeInTheDocument();
|
|
1661
|
-
expect(screen.getByRole('group')).toBeInTheDocument();
|
|
1662
|
-
});
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
|
-
// =============================================================================
|
|
1666
|
-
// SideNavItem — collapsed submenu hover intent (useMenuHover adoption, C12)
|
|
1667
|
-
// =============================================================================
|
|
1668
|
-
|
|
1669
|
-
/**
|
|
1670
|
-
* Pin the ambient pointer modality: the global `matchMedia` stub never
|
|
1671
|
-
* matches, which reads as a coarse pointer, so hover assertions must opt in.
|
|
1672
|
-
*/
|
|
1673
|
-
function stubPointerModality(modality: 'fine' | 'coarse') {
|
|
1674
|
-
vi.stubGlobal('matchMedia', (query: string) => ({
|
|
1675
|
-
matches: modality === 'fine' && query.includes('hover: hover'),
|
|
1676
|
-
media: query,
|
|
1677
|
-
onchange: null,
|
|
1678
|
-
addListener: () => {},
|
|
1679
|
-
removeListener: () => {},
|
|
1680
|
-
addEventListener: () => {},
|
|
1681
|
-
removeEventListener: () => {},
|
|
1682
|
-
dispatchEvent: () => false,
|
|
1683
|
-
}));
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
/** The hook's hover-intent delays: 150 ms to show, 200 ms to hide. */
|
|
1687
|
-
const SHOW_DELAY = 150;
|
|
1688
|
-
const HIDE_DELAY = 200;
|
|
1689
|
-
|
|
1690
|
-
describe('SideNavItem — collapsed submenu hover intent', () => {
|
|
1691
|
-
afterEach(() => {
|
|
1692
|
-
vi.useRealTimers();
|
|
1693
|
-
vi.unstubAllGlobals();
|
|
1694
|
-
});
|
|
1695
|
-
|
|
1696
|
-
function renderFlyout() {
|
|
1697
|
-
renderCollapsed(
|
|
1698
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1699
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1700
|
-
<SideNavItem label="Security" href="/settings/security" />
|
|
1701
|
-
</SideNavItem>,
|
|
1702
|
-
);
|
|
1703
|
-
return screen.getByTestId('parent');
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
const advance = (ms: number) =>
|
|
1707
|
-
act(() => {
|
|
1708
|
-
vi.advanceTimersByTime(ms);
|
|
1709
|
-
});
|
|
1710
|
-
|
|
1711
|
-
it('opens the flyout only after the hover-intent delay', () => {
|
|
1712
|
-
stubPointerModality('fine');
|
|
1713
|
-
vi.useFakeTimers();
|
|
1714
|
-
const trigger = renderFlyout();
|
|
1715
|
-
|
|
1716
|
-
fireEvent.mouseEnter(trigger);
|
|
1717
|
-
// A pointer merely passing over the icon must not open anything.
|
|
1718
|
-
advance(SHOW_DELAY - 1);
|
|
1719
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1720
|
-
|
|
1721
|
-
advance(1);
|
|
1722
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
it('closes a hover-opened flyout when the pointer leaves', () => {
|
|
1726
|
-
stubPointerModality('fine');
|
|
1727
|
-
vi.useFakeTimers();
|
|
1728
|
-
const trigger = renderFlyout();
|
|
1729
|
-
|
|
1730
|
-
fireEvent.mouseEnter(trigger);
|
|
1731
|
-
advance(SHOW_DELAY);
|
|
1732
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1733
|
-
|
|
1734
|
-
fireEvent.mouseLeave(trigger);
|
|
1735
|
-
advance(HIDE_DELAY);
|
|
1736
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
it('keeps a click-opened flyout open when the pointer leaves', () => {
|
|
1740
|
-
stubPointerModality('fine');
|
|
1741
|
-
vi.useFakeTimers();
|
|
1742
|
-
const trigger = renderFlyout();
|
|
1743
|
-
|
|
1744
|
-
// `detail: 1` marks a pointer click: the hook reads a detail-0 click as
|
|
1745
|
-
// Enter/Space, which always opens rather than toggling.
|
|
1746
|
-
fireEvent.click(trigger, {detail: 1});
|
|
1747
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1748
|
-
|
|
1749
|
-
// Clicking is a commitment: the flyout outlives the pointer.
|
|
1750
|
-
fireEvent.mouseLeave(trigger);
|
|
1751
|
-
advance(HIDE_DELAY * 2);
|
|
1752
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1753
|
-
});
|
|
1754
|
-
|
|
1755
|
-
it('does not spring back open when a click dismisses it under a stationary pointer', () => {
|
|
1756
|
-
stubPointerModality('fine');
|
|
1757
|
-
vi.useFakeTimers();
|
|
1758
|
-
const trigger = renderFlyout();
|
|
1759
|
-
|
|
1760
|
-
fireEvent.mouseEnter(trigger);
|
|
1761
|
-
advance(SHOW_DELAY);
|
|
1762
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1763
|
-
|
|
1764
|
-
// The pointer never left, so dismissing fires a fresh mouseenter as the
|
|
1765
|
-
// flyout unmounts — which used to reopen what the user just closed.
|
|
1766
|
-
fireEvent.click(trigger, {detail: 1});
|
|
1767
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1768
|
-
|
|
1769
|
-
fireEvent.mouseEnter(trigger);
|
|
1770
|
-
advance(SHOW_DELAY);
|
|
1771
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1772
|
-
|
|
1773
|
-
// One-shot, not a latch: a deliberate leave-and-return opens it again.
|
|
1774
|
-
fireEvent.mouseLeave(trigger);
|
|
1775
|
-
fireEvent.mouseEnter(trigger);
|
|
1776
|
-
advance(SHOW_DELAY);
|
|
1777
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1778
|
-
});
|
|
1779
|
-
|
|
1780
|
-
it('schedules no hover timers on a coarse pointer', () => {
|
|
1781
|
-
stubPointerModality('coarse');
|
|
1782
|
-
vi.useFakeTimers();
|
|
1783
|
-
const trigger = renderFlyout();
|
|
1784
|
-
|
|
1785
|
-
// A touch tap synthesises mouseenter. On a device that cannot hover there
|
|
1786
|
-
// is no hover intent to read, so nothing may be scheduled from it.
|
|
1787
|
-
fireEvent.mouseEnter(trigger);
|
|
1788
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
1789
|
-
|
|
1790
|
-
advance(SHOW_DELAY * 2);
|
|
1791
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1792
|
-
|
|
1793
|
-
// Tapping still opens it — the touch path goes through click, untouched.
|
|
1794
|
-
fireEvent.click(trigger, {detail: 1});
|
|
1795
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1796
|
-
});
|
|
1797
|
-
|
|
1798
|
-
it('does not open the hover popover after the item unmounts', () => {
|
|
1799
|
-
stubPointerModality('fine');
|
|
1800
|
-
vi.useFakeTimers();
|
|
1801
|
-
const {unmount} = render(
|
|
1802
|
-
<SideNavCollapseContext
|
|
1803
|
-
value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
|
|
1804
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1805
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1806
|
-
</SideNavItem>
|
|
1807
|
-
</SideNavCollapseContext>,
|
|
1808
|
-
);
|
|
1809
|
-
|
|
1810
|
-
fireEvent.mouseEnter(screen.getByTestId('parent'));
|
|
1811
|
-
expect(vi.getTimerCount()).toBeGreaterThan(0);
|
|
1812
|
-
|
|
1813
|
-
unmount();
|
|
1814
|
-
|
|
1815
|
-
// The hook owns the timer cleanup now; it still has to hold.
|
|
1816
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
1817
|
-
});
|
|
1818
|
-
|
|
1819
|
-
it('leaves arrow keys in the flyout to the browser', async () => {
|
|
1820
|
-
stubPointerModality('fine');
|
|
1821
|
-
const user = userEvent.setup();
|
|
1822
|
-
renderCollapsed(
|
|
1823
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1824
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1825
|
-
<SideNavItem label="Security" href="/settings/security" />
|
|
1826
|
-
</SideNavItem>,
|
|
1827
|
-
);
|
|
1828
|
-
|
|
1829
|
-
const trigger = screen.getByTestId('parent');
|
|
1830
|
-
await user.click(trigger);
|
|
1831
|
-
|
|
1832
|
-
// The flyout renders as a native popover, which jsdom leaves
|
|
1833
|
-
// `display: none`, so its contents are outside the default a11y-tree
|
|
1834
|
-
// queries.
|
|
1835
|
-
const first = screen.getByRole('link', {name: 'General', hidden: true});
|
|
1836
|
-
first.focus();
|
|
1837
|
-
|
|
1838
|
-
// A dialog of links, not a menu of menuitems: wiring the hook's
|
|
1839
|
-
// list-focus half would preventDefault every arrow key and find no
|
|
1840
|
-
// `[role="menuitem"]` to move to.
|
|
1841
|
-
const notPrevented = fireEvent.keyDown(first, {key: 'ArrowDown'});
|
|
1842
|
-
expect(notPrevented).toBe(true);
|
|
1843
|
-
expect(first).toHaveFocus();
|
|
1844
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1845
|
-
});
|
|
1846
|
-
|
|
1847
|
-
it('opens from the keyboard, moves focus into the flyout, and restores it on Escape', async () => {
|
|
1848
|
-
stubPointerModality('fine');
|
|
1849
|
-
const user = userEvent.setup();
|
|
1850
|
-
renderCollapsed(
|
|
1851
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
1852
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1853
|
-
<SideNavItem label="Security" href="/settings/security" />
|
|
1854
|
-
</SideNavItem>,
|
|
1855
|
-
);
|
|
1856
|
-
|
|
1857
|
-
const trigger = screen.getByTestId('parent');
|
|
1858
|
-
trigger.focus();
|
|
1859
|
-
await user.keyboard('{Enter}');
|
|
1860
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1861
|
-
|
|
1862
|
-
// Keyboard opens move focus in; hover opens leave it on the trigger.
|
|
1863
|
-
await waitFor(() =>
|
|
1864
|
-
expect(
|
|
1865
|
-
screen.getByRole('link', {name: 'General', hidden: true}),
|
|
1866
|
-
).toHaveFocus(),
|
|
1867
|
-
);
|
|
1868
|
-
|
|
1869
|
-
await user.keyboard('{Escape}');
|
|
1870
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1871
|
-
await waitFor(() => expect(trigger).toHaveFocus());
|
|
1872
|
-
});
|
|
1873
|
-
|
|
1874
|
-
it('leaves focus on the trigger when hover opens the flyout', () => {
|
|
1875
|
-
stubPointerModality('fine');
|
|
1876
|
-
vi.useFakeTimers();
|
|
1877
|
-
const trigger = renderFlyout();
|
|
1878
|
-
trigger.focus();
|
|
1879
|
-
|
|
1880
|
-
fireEvent.mouseEnter(trigger);
|
|
1881
|
-
advance(SHOW_DELAY);
|
|
1882
|
-
act(() => {
|
|
1883
|
-
vi.advanceTimersToNextFrame();
|
|
1884
|
-
});
|
|
1885
|
-
|
|
1886
|
-
// `skipAutoFocus`: a mid-hover pointer user keeps their caret.
|
|
1887
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1888
|
-
expect(trigger).toHaveFocus();
|
|
1889
|
-
});
|
|
1890
|
-
});
|
|
1891
|
-
|
|
1892
|
-
// =============================================================================
|
|
1893
|
-
// Mobile nav close-on-activate
|
|
1894
|
-
// =============================================================================
|
|
1895
|
-
|
|
1896
|
-
// =============================================================================
|
|
1897
|
-
// SideNavItem — collapsible + href (independent toggle)
|
|
1898
|
-
// =============================================================================
|
|
1899
|
-
|
|
1900
|
-
describe('SideNavItem — collapsible + href', () => {
|
|
1901
|
-
it('renders a link that navigates when both collapsible and href are set', () => {
|
|
1902
|
-
render(
|
|
1903
|
-
<SideNavItem
|
|
1904
|
-
label="Settings"
|
|
1905
|
-
href="/settings"
|
|
1906
|
-
collapsible
|
|
1907
|
-
data-testid="parent">
|
|
1908
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1909
|
-
</SideNavItem>,
|
|
1910
|
-
);
|
|
1911
|
-
const link = screen.getByRole('link', {name: 'Settings'});
|
|
1912
|
-
expect(link).toHaveAttribute('href', '/settings');
|
|
1913
|
-
});
|
|
1914
|
-
|
|
1915
|
-
it('renders a separate toggle button for the chevron', () => {
|
|
1916
|
-
render(
|
|
1917
|
-
<SideNavItem label="Settings" href="/settings" collapsible>
|
|
1918
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1919
|
-
</SideNavItem>,
|
|
1920
|
-
);
|
|
1921
|
-
const toggle = screen.getByRole('button', {name: /collapse settings/i});
|
|
1922
|
-
expect(toggle).toBeInTheDocument();
|
|
1923
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
1924
|
-
});
|
|
1925
|
-
|
|
1926
|
-
it('toggle button collapses children without navigating', async () => {
|
|
1927
|
-
const user = userEvent.setup();
|
|
1928
|
-
render(
|
|
1929
|
-
<SideNavItem label="Settings" href="/settings" collapsible>
|
|
1930
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1931
|
-
</SideNavItem>,
|
|
1932
|
-
);
|
|
1933
|
-
const toggle = screen.getByRole('button', {name: /collapse settings/i});
|
|
1934
|
-
await user.click(toggle);
|
|
1935
|
-
// After collapsing, aria-hidden on children container
|
|
1936
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
1937
|
-
expect(toggle).toHaveAccessibleName('Expand Settings');
|
|
1938
|
-
});
|
|
1939
|
-
|
|
1940
|
-
it('link does not toggle collapse when clicked', async () => {
|
|
1941
|
-
const user = userEvent.setup();
|
|
1942
|
-
const onClick = vi.fn();
|
|
1943
|
-
render(
|
|
1944
|
-
<SideNavItem
|
|
1945
|
-
label="Settings"
|
|
1946
|
-
href="/settings"
|
|
1947
|
-
collapsible
|
|
1948
|
-
onClick={onClick}>
|
|
1949
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1950
|
-
</SideNavItem>,
|
|
1951
|
-
);
|
|
1952
|
-
const link = screen.getByRole('link', {name: 'Settings'});
|
|
1953
|
-
await user.click(link);
|
|
1954
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
1955
|
-
// Children should still be visible (not collapsed)
|
|
1956
|
-
const toggle = screen.getByRole('button', {name: /collapse settings/i});
|
|
1957
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
1958
|
-
});
|
|
1959
|
-
|
|
1960
|
-
it('link does not have aria-expanded (toggle button owns it)', () => {
|
|
1961
|
-
render(
|
|
1962
|
-
<SideNavItem label="Settings" href="/settings" collapsible>
|
|
1963
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
1964
|
-
</SideNavItem>,
|
|
1965
|
-
);
|
|
1966
|
-
const link = screen.getByRole('link', {name: 'Settings'});
|
|
1967
|
-
expect(link).not.toHaveAttribute('aria-expanded');
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
it('without href or onClick, clicking the item toggles collapse', async () => {
|
|
1971
|
-
const user = userEvent.setup();
|
|
1972
|
-
render(
|
|
1973
|
-
<SideNavItem label="Settings" collapsible>
|
|
1974
|
-
<SideNavItem label="General" />
|
|
1975
|
-
</SideNavItem>,
|
|
1976
|
-
);
|
|
1977
|
-
const button = screen.getByRole('button', {name: 'Settings'});
|
|
1978
|
-
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
1979
|
-
await user.click(button);
|
|
1980
|
-
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
1981
|
-
});
|
|
1982
|
-
|
|
1983
|
-
it('with onClick (no href), clicking the label fires onClick', async () => {
|
|
1984
|
-
const user = userEvent.setup();
|
|
1985
|
-
const onClick = vi.fn();
|
|
1986
|
-
render(
|
|
1987
|
-
<SideNavItem label="Settings" onClick={onClick} collapsible>
|
|
1988
|
-
<SideNavItem label="General" />
|
|
1989
|
-
</SideNavItem>,
|
|
1990
|
-
);
|
|
1991
|
-
const primaryButton = screen.getByRole('button', {name: 'Settings'});
|
|
1992
|
-
await user.click(primaryButton);
|
|
1993
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
1994
|
-
// Children should still be visible
|
|
1995
|
-
const toggle = screen.getByRole('button', {name: /collapse settings/i});
|
|
1996
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
1997
|
-
});
|
|
1998
|
-
|
|
1999
|
-
it('with onClick (no href), toggle collapses without firing onClick', async () => {
|
|
2000
|
-
const user = userEvent.setup();
|
|
2001
|
-
const onClick = vi.fn();
|
|
2002
|
-
render(
|
|
2003
|
-
<SideNavItem label="Settings" onClick={onClick} collapsible>
|
|
2004
|
-
<SideNavItem label="General" />
|
|
2005
|
-
</SideNavItem>,
|
|
2006
|
-
);
|
|
2007
|
-
const toggle = screen.getByRole('button', {name: /collapse settings/i});
|
|
2008
|
-
await user.click(toggle);
|
|
2009
|
-
expect(onClick).not.toHaveBeenCalled();
|
|
2010
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
2011
|
-
});
|
|
2012
|
-
|
|
2013
|
-
it('collapsed children are inert (not focusable)', async () => {
|
|
2014
|
-
const user = userEvent.setup();
|
|
2015
|
-
render(
|
|
2016
|
-
<SideNavItem label="Settings" collapsible>
|
|
2017
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2018
|
-
</SideNavItem>,
|
|
2019
|
-
);
|
|
2020
|
-
// Collapse the item
|
|
2021
|
-
const button = screen.getByRole('button', {name: 'Settings'});
|
|
2022
|
-
await user.click(button);
|
|
2023
|
-
// The children container should have inert attribute
|
|
2024
|
-
const childrenContainer = document.getElementById(
|
|
2025
|
-
button.getAttribute('aria-controls')!,
|
|
2026
|
-
);
|
|
2027
|
-
expect(childrenContainer).toHaveAttribute('inert');
|
|
2028
|
-
});
|
|
2029
|
-
});
|
|
2030
|
-
|
|
2031
|
-
// =============================================================================
|
|
2032
|
-
// SideNavItem — actions slot (row-level secondary controls, #4988)
|
|
2033
|
-
// =============================================================================
|
|
2034
|
-
|
|
2035
|
-
describe('SideNavItem — actions slot', () => {
|
|
2036
|
-
const rowAction = (onClick?: (e: React.MouseEvent) => void) => (
|
|
2037
|
-
<button type="button" data-testid="row-action" onClick={onClick}>
|
|
2038
|
-
⋯
|
|
2039
|
-
</button>
|
|
2040
|
-
);
|
|
2041
|
-
|
|
2042
|
-
it('renders actions outside the primary interactive element', () => {
|
|
2043
|
-
render(
|
|
2044
|
-
<SideNavItem label="Project" href="/project" actions={rowAction()} />,
|
|
2045
|
-
);
|
|
2046
|
-
const action = screen.getByTestId('row-action');
|
|
2047
|
-
const link = screen.getByRole('link', {name: 'Project'});
|
|
2048
|
-
// Sibling, not nested: the action's closest interactive element is itself.
|
|
2049
|
-
expect(within(link).queryByTestId('row-action')).toBeNull();
|
|
2050
|
-
expect(action.closest('a, button')).toBe(action);
|
|
2051
|
-
});
|
|
2052
|
-
|
|
2053
|
-
it('places actions after the primary element and before nested children', () => {
|
|
2054
|
-
render(
|
|
2055
|
-
<SideNavItem
|
|
2056
|
-
label="Project"
|
|
2057
|
-
href="/project"
|
|
2058
|
-
collapsible
|
|
2059
|
-
actions={rowAction()}>
|
|
2060
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2061
|
-
</SideNavItem>,
|
|
2062
|
-
);
|
|
2063
|
-
const primary = screen.getByRole('link', {name: 'Project'});
|
|
2064
|
-
const action = screen.getByTestId('row-action');
|
|
2065
|
-
const group = screen.getByRole('group');
|
|
2066
|
-
// Compare real node positions, not serialized markup.
|
|
2067
|
-
expect(
|
|
2068
|
-
primary.compareDocumentPosition(action) &
|
|
2069
|
-
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2070
|
-
).toBeTruthy();
|
|
2071
|
-
expect(
|
|
2072
|
-
action.compareDocumentPosition(group) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2073
|
-
).toBeTruthy();
|
|
2074
|
-
});
|
|
2075
|
-
|
|
2076
|
-
it('reaches every row-level control before nested items when tabbing', async () => {
|
|
2077
|
-
const user = userEvent.setup();
|
|
2078
|
-
render(
|
|
2079
|
-
<SideNavItem
|
|
2080
|
-
label="Project"
|
|
2081
|
-
href="/project"
|
|
2082
|
-
collapsible
|
|
2083
|
-
actions={rowAction()}>
|
|
2084
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2085
|
-
</SideNavItem>,
|
|
2086
|
-
);
|
|
2087
|
-
await user.tab();
|
|
2088
|
-
expect(screen.getByRole('link', {name: 'Project'})).toHaveFocus();
|
|
2089
|
-
await user.tab();
|
|
2090
|
-
expect(
|
|
2091
|
-
screen.getByRole('button', {name: /collapse project/i}),
|
|
2092
|
-
).toHaveFocus();
|
|
2093
|
-
await user.tab();
|
|
2094
|
-
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
2095
|
-
await user.tab();
|
|
2096
|
-
expect(screen.getByRole('link', {name: 'Session'})).toHaveFocus();
|
|
2097
|
-
});
|
|
2098
|
-
|
|
2099
|
-
it('clicking an action does not activate the item', async () => {
|
|
2100
|
-
const user = userEvent.setup();
|
|
2101
|
-
const onItemClick = vi.fn();
|
|
2102
|
-
const onActionClick = vi.fn();
|
|
2103
|
-
render(
|
|
2104
|
-
<SideNavItem
|
|
2105
|
-
label="Project"
|
|
2106
|
-
onClick={onItemClick}
|
|
2107
|
-
actions={rowAction(onActionClick)}
|
|
2108
|
-
/>,
|
|
2109
|
-
);
|
|
2110
|
-
await user.click(screen.getByTestId('row-action'));
|
|
2111
|
-
expect(onActionClick).toHaveBeenCalledTimes(1);
|
|
2112
|
-
expect(onItemClick).not.toHaveBeenCalled();
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
it('clicking an action does not toggle collapse of a whole-row toggle item', async () => {
|
|
2116
|
-
const user = userEvent.setup();
|
|
2117
|
-
render(
|
|
2118
|
-
<SideNavItem label="Project" collapsible actions={rowAction()}>
|
|
2119
|
-
<SideNavItem label="Session" />
|
|
2120
|
-
</SideNavItem>,
|
|
2121
|
-
);
|
|
2122
|
-
const rowToggle = screen.getByRole('button', {name: 'Project'});
|
|
2123
|
-
expect(rowToggle).toHaveAttribute('aria-expanded', 'true');
|
|
2124
|
-
await user.click(screen.getByTestId('row-action'));
|
|
2125
|
-
expect(rowToggle).toHaveAttribute('aria-expanded', 'true');
|
|
2126
|
-
});
|
|
2127
|
-
|
|
2128
|
-
it('whole-row toggle still collapses when actions are present', async () => {
|
|
2129
|
-
const user = userEvent.setup();
|
|
2130
|
-
render(
|
|
2131
|
-
<SideNavItem label="Project" collapsible actions={rowAction()}>
|
|
2132
|
-
<SideNavItem label="Session" />
|
|
2133
|
-
</SideNavItem>,
|
|
2134
|
-
);
|
|
2135
|
-
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
2136
|
-
const rowToggle = screen.getByRole('button', {name: 'Project'});
|
|
2137
|
-
await user.click(rowToggle);
|
|
2138
|
-
expect(rowToggle).toHaveAttribute('aria-expanded', 'false');
|
|
2139
|
-
});
|
|
2140
|
-
|
|
2141
|
-
it('adds a row wrapper only when actions are present', () => {
|
|
2142
|
-
const {unmount} = render(<SideNavItem label="Project" href="/project" />);
|
|
2143
|
-
// Without actions, the link itself is the styled row element.
|
|
2144
|
-
expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
|
|
2145
|
-
'astryx-side-nav-item',
|
|
2146
|
-
);
|
|
2147
|
-
unmount();
|
|
2148
|
-
|
|
2149
|
-
render(
|
|
2150
|
-
<SideNavItem
|
|
2151
|
-
label="Project"
|
|
2152
|
-
href="/project"
|
|
2153
|
-
data-testid="row"
|
|
2154
|
-
actions={rowAction()}
|
|
2155
|
-
/>,
|
|
2156
|
-
);
|
|
2157
|
-
// With actions, a wrapper div is the styled row; the link sits inside it.
|
|
2158
|
-
const link = screen.getByRole('link', {name: 'Project'});
|
|
2159
|
-
expect(link).not.toHaveClass('astryx-side-nav-item');
|
|
2160
|
-
const row = screen.getByTestId('row');
|
|
2161
|
-
expect(row).toHaveClass('astryx-side-nav-item');
|
|
2162
|
-
expect(row).toContainElement(link);
|
|
2163
|
-
expect(row).toContainElement(screen.getByTestId('row-action'));
|
|
2164
|
-
});
|
|
2165
|
-
|
|
2166
|
-
it('adds no row wrapper for a falsy actions value', () => {
|
|
2167
|
-
// `actions={canEdit ? <Menu /> : null}` is the ordinary consumer shape.
|
|
2168
|
-
// A wrapper there would change markup, focus order and the ring's
|
|
2169
|
-
// owner for a row that has no row controls at all.
|
|
2170
|
-
const {rerender} = render(
|
|
2171
|
-
<SideNavItem label="Project" href="/project" actions={null} />,
|
|
2172
|
-
);
|
|
2173
|
-
expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
|
|
2174
|
-
'astryx-side-nav-item',
|
|
2175
|
-
);
|
|
2176
|
-
|
|
2177
|
-
rerender(<SideNavItem label="Project" href="/project" actions={false} />);
|
|
2178
|
-
expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
|
|
2179
|
-
'astryx-side-nav-item',
|
|
2180
|
-
);
|
|
2181
|
-
});
|
|
2182
|
-
|
|
2183
|
-
it('keeps aria wiring on the toggle when actions are present', () => {
|
|
2184
|
-
render(
|
|
2185
|
-
<SideNavItem
|
|
2186
|
-
label="Project"
|
|
2187
|
-
href="/project"
|
|
2188
|
-
collapsible
|
|
2189
|
-
actions={rowAction()}>
|
|
2190
|
-
<SideNavItem label="Session" />
|
|
2191
|
-
</SideNavItem>,
|
|
2192
|
-
);
|
|
2193
|
-
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
2194
|
-
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
2195
|
-
const group = screen.getByRole('group');
|
|
2196
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
2197
|
-
expect(toggle).toHaveAttribute('aria-controls', group.id);
|
|
2198
|
-
expect(screen.getByRole('link', {name: 'Project'})).not.toHaveAttribute(
|
|
2199
|
-
'aria-expanded',
|
|
2200
|
-
);
|
|
2201
|
-
});
|
|
2202
|
-
|
|
2203
|
-
it('keeps endContent inside the primary element when actions are present', () => {
|
|
2204
|
-
render(
|
|
2205
|
-
<SideNavItem
|
|
2206
|
-
label="Project"
|
|
2207
|
-
href="/project"
|
|
2208
|
-
endContent={<span data-testid="badge">3</span>}
|
|
2209
|
-
actions={rowAction()}
|
|
2210
|
-
/>,
|
|
2211
|
-
);
|
|
2212
|
-
const link = screen.getByRole('link', {name: /Project/});
|
|
2213
|
-
expect(within(link).getByTestId('badge')).toBeInTheDocument();
|
|
2214
|
-
expect(within(link).queryByTestId('row-action')).toBeNull();
|
|
2215
|
-
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
2216
|
-
});
|
|
2217
|
-
|
|
2218
|
-
it('keeps collapsed children inert while the action stays in the row', () => {
|
|
2219
|
-
render(
|
|
2220
|
-
<SideNavItem
|
|
2221
|
-
label="Project"
|
|
2222
|
-
collapsible={{defaultIsCollapsed: true}}
|
|
2223
|
-
actions={rowAction()}>
|
|
2224
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2225
|
-
</SideNavItem>,
|
|
2226
|
-
);
|
|
2227
|
-
const rowToggle = screen.getByRole('button', {name: 'Project'});
|
|
2228
|
-
const group = document.getElementById(
|
|
2229
|
-
rowToggle.getAttribute('aria-controls')!,
|
|
2230
|
-
);
|
|
2231
|
-
expect(group).toHaveAttribute('inert');
|
|
2232
|
-
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
2233
|
-
});
|
|
2234
|
-
|
|
2235
|
-
it('does not render actions in the collapsed rail', () => {
|
|
2236
|
-
const {unmount} = renderCollapsed(
|
|
2237
|
-
<SideNavItem
|
|
2238
|
-
label="Project"
|
|
2239
|
-
icon={StubIcon}
|
|
2240
|
-
href="/project"
|
|
2241
|
-
actions={rowAction()}
|
|
2242
|
-
/>,
|
|
2243
|
-
);
|
|
2244
|
-
expect(screen.getByLabelText('Project')).toBeInTheDocument();
|
|
2245
|
-
expect(screen.queryByTestId('row-action')).toBeNull();
|
|
2246
|
-
unmount();
|
|
2247
|
-
|
|
2248
|
-
renderCollapsed(
|
|
2249
|
-
<SideNavItem label="Project" icon={StubIcon} actions={rowAction()}>
|
|
2250
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2251
|
-
</SideNavItem>,
|
|
2252
|
-
);
|
|
2253
|
-
expect(screen.getByLabelText('Project')).toBeInTheDocument();
|
|
2254
|
-
expect(screen.queryByTestId('row-action')).toBeNull();
|
|
2255
|
-
});
|
|
2256
|
-
|
|
2257
|
-
it('keeps actions clickable on a disabled item', async () => {
|
|
2258
|
-
const user = userEvent.setup();
|
|
2259
|
-
const onActionClick = vi.fn();
|
|
2260
|
-
render(
|
|
2261
|
-
<SideNavItem
|
|
2262
|
-
label="Project"
|
|
2263
|
-
href="/project"
|
|
2264
|
-
isDisabled
|
|
2265
|
-
actions={rowAction(onActionClick)}
|
|
2266
|
-
/>,
|
|
2267
|
-
);
|
|
2268
|
-
// The row's disabled styling must not swallow the sibling action:
|
|
2269
|
-
// the action control owns its own disabled state.
|
|
2270
|
-
await user.click(screen.getByTestId('row-action'));
|
|
2271
|
-
expect(onActionClick).toHaveBeenCalledTimes(1);
|
|
2272
|
-
});
|
|
2273
|
-
|
|
2274
|
-
it('places actions after the toggle when endContent is also present', () => {
|
|
2275
|
-
render(
|
|
2276
|
-
<SideNavItem
|
|
2277
|
-
label="Project"
|
|
2278
|
-
href="/project"
|
|
2279
|
-
collapsible
|
|
2280
|
-
endContent={<span data-testid="badge">3</span>}
|
|
2281
|
-
actions={rowAction()}>
|
|
2282
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2283
|
-
</SideNavItem>,
|
|
2284
|
-
);
|
|
2285
|
-
const link = screen.getByRole('link', {name: /Project/});
|
|
2286
|
-
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
2287
|
-
const action = screen.getByTestId('row-action');
|
|
2288
|
-
expect(within(link).getByTestId('badge')).toBeInTheDocument();
|
|
2289
|
-
expect(
|
|
2290
|
-
link.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2291
|
-
).toBeTruthy();
|
|
2292
|
-
expect(
|
|
2293
|
-
toggle.compareDocumentPosition(action) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2294
|
-
).toBeTruthy();
|
|
2295
|
-
});
|
|
2296
|
-
|
|
2297
|
-
it('reaches the action after the whole-row toggle and before nested items', async () => {
|
|
2298
|
-
const user = userEvent.setup();
|
|
2299
|
-
render(
|
|
2300
|
-
<SideNavItem label="Project" collapsible actions={rowAction()}>
|
|
2301
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2302
|
-
</SideNavItem>,
|
|
2303
|
-
);
|
|
2304
|
-
await user.tab();
|
|
2305
|
-
expect(screen.getByRole('button', {name: 'Project'})).toHaveFocus();
|
|
2306
|
-
await user.tab();
|
|
2307
|
-
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
2308
|
-
await user.tab();
|
|
2309
|
-
expect(screen.getByRole('link', {name: 'Session'})).toHaveFocus();
|
|
2310
|
-
});
|
|
2311
|
-
|
|
2312
|
-
it('renders the custom link component inside the row wrapper', () => {
|
|
2313
|
-
render(
|
|
2314
|
-
<LinkProvider component={CustomLink}>
|
|
2315
|
-
<SideNavItem label="Project" href="/project" actions={rowAction()} />
|
|
2316
|
-
</LinkProvider>,
|
|
2317
|
-
);
|
|
2318
|
-
const link = screen.getByRole('link', {name: 'Project'});
|
|
2319
|
-
expect(link).toHaveAttribute('data-custom-link');
|
|
2320
|
-
expect(link.parentElement).toHaveClass('astryx-side-nav-item');
|
|
2321
|
-
});
|
|
2322
|
-
|
|
2323
|
-
it('controlled collapse reports toggle intent without flipping until the prop changes', async () => {
|
|
2324
|
-
const user = userEvent.setup();
|
|
2325
|
-
const onCollapsedChange = vi.fn();
|
|
2326
|
-
render(
|
|
2327
|
-
<SideNavItem
|
|
2328
|
-
label="Project"
|
|
2329
|
-
href="/project"
|
|
2330
|
-
collapsible={{isCollapsed: false, onCollapsedChange}}
|
|
2331
|
-
actions={rowAction()}>
|
|
2332
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
2333
|
-
</SideNavItem>,
|
|
2334
|
-
);
|
|
2335
|
-
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
2336
|
-
await user.click(toggle);
|
|
2337
|
-
expect(onCollapsedChange).toHaveBeenCalledWith(true);
|
|
2338
|
-
// Controlled mode: stays expanded until the consumer flips the prop.
|
|
2339
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
2340
|
-
});
|
|
2341
|
-
});
|
|
2342
|
-
|
|
2343
|
-
import {SideNavRenderContext} from './SideNavRenderContext';
|
|
2344
|
-
import {AppShellMobileContext} from '../AppShell/AppShellMobileContext';
|
|
2345
|
-
|
|
2346
|
-
describe('SideNavItem — mobile drawer close-on-activate', () => {
|
|
2347
|
-
function renderInDrawer(ui: ReactNode, closeMobileNav = vi.fn()) {
|
|
2348
|
-
return {
|
|
2349
|
-
closeMobileNav,
|
|
2350
|
-
...render(
|
|
2351
|
-
<AppShellMobileContext
|
|
2352
|
-
value={{
|
|
2353
|
-
isMobile: true,
|
|
2354
|
-
isMobileNavOpen: true,
|
|
2355
|
-
toggleMobileNav: vi.fn(),
|
|
2356
|
-
openMobileNav: vi.fn(),
|
|
2357
|
-
closeMobileNav,
|
|
2358
|
-
isMobileNavEnabled: true,
|
|
2359
|
-
hasAutoToggle: true,
|
|
2360
|
-
}}>
|
|
2361
|
-
<SideNavRenderContext value="drawer">{ui}</SideNavRenderContext>
|
|
2362
|
-
</AppShellMobileContext>,
|
|
2363
|
-
),
|
|
2364
|
-
};
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
it('closes the mobile nav when a link item is clicked', async () => {
|
|
2368
|
-
const user = userEvent.setup();
|
|
2369
|
-
const {closeMobileNav} = renderInDrawer(
|
|
2370
|
-
<SideNavItem label="Home" href="/" data-testid="item" />,
|
|
2371
|
-
);
|
|
2372
|
-
await user.click(screen.getByTestId('item'));
|
|
2373
|
-
expect(closeMobileNav).toHaveBeenCalledTimes(1);
|
|
2374
|
-
});
|
|
2375
|
-
|
|
2376
|
-
it('closes the mobile nav when a button item is clicked', async () => {
|
|
2377
|
-
const user = userEvent.setup();
|
|
2378
|
-
const onClick = vi.fn();
|
|
2379
|
-
const {closeMobileNav} = renderInDrawer(
|
|
2380
|
-
<SideNavItem label="Action" onClick={onClick} data-testid="item" />,
|
|
2381
|
-
);
|
|
2382
|
-
await user.click(screen.getByTestId('item'));
|
|
2383
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
2384
|
-
expect(closeMobileNav).toHaveBeenCalledTimes(1);
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
|
-
it('does NOT close when a collapsible parent is toggled', async () => {
|
|
2388
|
-
const user = userEvent.setup();
|
|
2389
|
-
const {closeMobileNav} = renderInDrawer(
|
|
2390
|
-
<SideNavItem
|
|
2391
|
-
label="Settings"
|
|
2392
|
-
icon={StubIcon}
|
|
2393
|
-
collapsible
|
|
2394
|
-
data-testid="parent">
|
|
2395
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2396
|
-
</SideNavItem>,
|
|
2397
|
-
);
|
|
2398
|
-
await user.click(screen.getByTestId('parent'));
|
|
2399
|
-
expect(closeMobileNav).not.toHaveBeenCalled();
|
|
2400
|
-
});
|
|
2401
|
-
|
|
2402
|
-
it('does NOT close when not inside a drawer', async () => {
|
|
2403
|
-
const user = userEvent.setup();
|
|
2404
|
-
const closeMobileNav = vi.fn();
|
|
2405
|
-
render(
|
|
2406
|
-
<AppShellMobileContext
|
|
2407
|
-
value={{
|
|
2408
|
-
isMobile: false,
|
|
2409
|
-
isMobileNavOpen: false,
|
|
2410
|
-
toggleMobileNav: vi.fn(),
|
|
2411
|
-
openMobileNav: vi.fn(),
|
|
2412
|
-
closeMobileNav,
|
|
2413
|
-
isMobileNavEnabled: false,
|
|
2414
|
-
hasAutoToggle: true,
|
|
2415
|
-
}}>
|
|
2416
|
-
<SideNavItem label="Home" href="/" data-testid="item" />
|
|
2417
|
-
</AppShellMobileContext>,
|
|
2418
|
-
);
|
|
2419
|
-
await user.click(screen.getByTestId('item'));
|
|
2420
|
-
expect(closeMobileNav).not.toHaveBeenCalled();
|
|
2421
|
-
});
|
|
2422
|
-
|
|
2423
|
-
it('does not close the mobile nav when a row action is clicked', async () => {
|
|
2424
|
-
const user = userEvent.setup();
|
|
2425
|
-
const onActionClick = vi.fn();
|
|
2426
|
-
const {closeMobileNav} = renderInDrawer(
|
|
2427
|
-
<SideNavItem
|
|
2428
|
-
label="Home"
|
|
2429
|
-
href="/"
|
|
2430
|
-
actions={
|
|
2431
|
-
<button
|
|
2432
|
-
type="button"
|
|
2433
|
-
data-testid="drawer-row-action"
|
|
2434
|
-
onClick={onActionClick}>
|
|
2435
|
-
⋯
|
|
2436
|
-
</button>
|
|
2437
|
-
}
|
|
2438
|
-
/>,
|
|
2439
|
-
);
|
|
2440
|
-
await user.click(screen.getByTestId('drawer-row-action'));
|
|
2441
|
-
expect(onActionClick).toHaveBeenCalledTimes(1);
|
|
2442
|
-
expect(closeMobileNav).not.toHaveBeenCalled();
|
|
2443
|
-
});
|
|
2444
|
-
});
|
|
2445
|
-
|
|
2446
|
-
// =============================================================================
|
|
2447
|
-
// Keyboard operation (audit §6 V6)
|
|
2448
|
-
// =============================================================================
|
|
2449
|
-
|
|
2450
|
-
describe('SideNav keyboard operation', () => {
|
|
2451
|
-
it('reaches every nav item with Tab, in document order', async () => {
|
|
2452
|
-
const user = userEvent.setup();
|
|
2453
|
-
render(
|
|
2454
|
-
<SideNav>
|
|
2455
|
-
<SideNavSection title="Main">
|
|
2456
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
2457
|
-
<SideNavItem label="Projects" href="/projects" />
|
|
2458
|
-
<SideNavItem label="Settings" onClick={() => {}} />
|
|
2459
|
-
</SideNavSection>
|
|
2460
|
-
</SideNav>,
|
|
2461
|
-
);
|
|
2462
|
-
|
|
2463
|
-
await user.tab();
|
|
2464
|
-
expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveFocus();
|
|
2465
|
-
await user.tab();
|
|
2466
|
-
expect(screen.getByRole('link', {name: 'Projects'})).toHaveFocus();
|
|
2467
|
-
await user.tab();
|
|
2468
|
-
expect(screen.getByRole('button', {name: 'Settings'})).toHaveFocus();
|
|
2469
|
-
});
|
|
2470
|
-
|
|
2471
|
-
it('skips a disabled item in the tab order', async () => {
|
|
2472
|
-
const user = userEvent.setup();
|
|
2473
|
-
render(
|
|
2474
|
-
<SideNav>
|
|
2475
|
-
<SideNavItem label="Dashboard" onClick={() => {}} />
|
|
2476
|
-
<SideNavItem label="Archived" onClick={() => {}} isDisabled />
|
|
2477
|
-
<SideNavItem label="Settings" onClick={() => {}} />
|
|
2478
|
-
</SideNav>,
|
|
2479
|
-
);
|
|
2480
|
-
|
|
2481
|
-
await user.tab();
|
|
2482
|
-
expect(screen.getByRole('button', {name: 'Dashboard'})).toHaveFocus();
|
|
2483
|
-
await user.tab();
|
|
2484
|
-
expect(screen.getByRole('button', {name: 'Settings'})).toHaveFocus();
|
|
2485
|
-
});
|
|
2486
|
-
|
|
2487
|
-
it('toggles a collapsible group with Enter and with Space', async () => {
|
|
2488
|
-
const user = userEvent.setup();
|
|
2489
|
-
render(
|
|
2490
|
-
<SideNavItem label="Settings" onClick={() => {}} collapsible>
|
|
2491
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2492
|
-
</SideNavItem>,
|
|
2493
|
-
);
|
|
2494
|
-
|
|
2495
|
-
const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
|
|
2496
|
-
toggle.focus();
|
|
2497
|
-
|
|
2498
|
-
await user.keyboard('{Enter}');
|
|
2499
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
2500
|
-
|
|
2501
|
-
await user.keyboard(' ');
|
|
2502
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
2503
|
-
});
|
|
2504
|
-
|
|
2505
|
-
it('activates a button item with Enter', async () => {
|
|
2506
|
-
const user = userEvent.setup();
|
|
2507
|
-
const onClick = vi.fn();
|
|
2508
|
-
render(<SideNavItem label="Log out" onClick={onClick} />);
|
|
2509
|
-
|
|
2510
|
-
await user.tab();
|
|
2511
|
-
await user.keyboard('{Enter}');
|
|
2512
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
2513
|
-
});
|
|
2514
|
-
|
|
2515
|
-
it('opens the collapsed submenu from the keyboard and closes it with Escape', async () => {
|
|
2516
|
-
const user = userEvent.setup();
|
|
2517
|
-
renderCollapsed(
|
|
2518
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
2519
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2520
|
-
</SideNavItem>,
|
|
2521
|
-
);
|
|
2522
|
-
|
|
2523
|
-
const trigger = screen.getByTestId('parent');
|
|
2524
|
-
trigger.focus();
|
|
2525
|
-
await user.keyboard('{Enter}');
|
|
2526
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
2527
|
-
|
|
2528
|
-
await user.keyboard('{Escape}');
|
|
2529
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2530
|
-
});
|
|
2531
|
-
});
|
|
2532
|
-
|
|
2533
|
-
// =============================================================================
|
|
2534
|
-
// Focus management (audit §6 V7)
|
|
2535
|
-
// =============================================================================
|
|
2536
|
-
|
|
2537
|
-
/**
|
|
2538
|
-
* Holds requestAnimationFrame callbacks until `flush`, so a test decides when
|
|
2539
|
-
* a popover's auto-focus frame runs instead of racing a timer.
|
|
2540
|
-
*/
|
|
2541
|
-
function holdAnimationFrames() {
|
|
2542
|
-
const frames = new Map<number, FrameRequestCallback>();
|
|
2543
|
-
let nextFrame = 0;
|
|
2544
|
-
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
|
2545
|
-
nextFrame += 1;
|
|
2546
|
-
frames.set(nextFrame, callback);
|
|
2547
|
-
return nextFrame;
|
|
2548
|
-
});
|
|
2549
|
-
vi.stubGlobal('cancelAnimationFrame', (frame: number) => {
|
|
2550
|
-
frames.delete(frame);
|
|
2551
|
-
});
|
|
2552
|
-
return {
|
|
2553
|
-
flush() {
|
|
2554
|
-
act(() => {
|
|
2555
|
-
const due = [...frames.values()];
|
|
2556
|
-
frames.clear();
|
|
2557
|
-
due.forEach(callback => callback(performance.now()));
|
|
2558
|
-
});
|
|
2559
|
-
},
|
|
2560
|
-
};
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
describe('SideNav focus management', () => {
|
|
2564
|
-
afterEach(() => {
|
|
2565
|
-
vi.unstubAllGlobals();
|
|
2566
|
-
});
|
|
2567
|
-
|
|
2568
|
-
it('keeps focus on the toggle when a group collapses', async () => {
|
|
2569
|
-
const user = userEvent.setup();
|
|
2570
|
-
render(
|
|
2571
|
-
<SideNavItem label="Settings" href="/settings" collapsible>
|
|
2572
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2573
|
-
</SideNavItem>,
|
|
2574
|
-
);
|
|
2575
|
-
|
|
2576
|
-
const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
|
|
2577
|
-
await user.click(toggle);
|
|
2578
|
-
|
|
2579
|
-
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
2580
|
-
expect(toggle).toHaveFocus();
|
|
2581
|
-
});
|
|
2582
|
-
|
|
2583
|
-
it('does not leave focus on a child that the group just hid', async () => {
|
|
2584
|
-
const user = userEvent.setup();
|
|
2585
|
-
render(
|
|
2586
|
-
<SideNavItem label="Settings" href="/settings" collapsible>
|
|
2587
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2588
|
-
</SideNavItem>,
|
|
2589
|
-
);
|
|
2590
|
-
|
|
2591
|
-
const child = screen.getByRole('link', {name: 'General'});
|
|
2592
|
-
child.focus();
|
|
2593
|
-
|
|
2594
|
-
const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
|
|
2595
|
-
await user.click(toggle);
|
|
2596
|
-
|
|
2597
|
-
// The collapsed group is `inert`: focus may land neither inside it nor
|
|
2598
|
-
// on <body>.
|
|
2599
|
-
expect(document.activeElement).not.toBe(child);
|
|
2600
|
-
expect(document.activeElement).not.toBe(document.body);
|
|
2601
|
-
expect(toggle).toHaveFocus();
|
|
2602
|
-
});
|
|
2603
|
-
|
|
2604
|
-
it('returns focus to the trigger when the collapsed submenu closes', async () => {
|
|
2605
|
-
const frames = holdAnimationFrames();
|
|
2606
|
-
const user = userEvent.setup();
|
|
2607
|
-
renderCollapsed(
|
|
2608
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
2609
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2610
|
-
</SideNavItem>,
|
|
2611
|
-
);
|
|
2612
|
-
|
|
2613
|
-
const trigger = screen.getByTestId('parent');
|
|
2614
|
-
await user.click(trigger);
|
|
2615
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
2616
|
-
// The submenu focuses its first link in the frame after it opens.
|
|
2617
|
-
frames.flush();
|
|
2618
|
-
expect(screen.getByText('General').closest('a')).toHaveFocus();
|
|
2619
|
-
|
|
2620
|
-
// Closing restores focus as part of the close itself.
|
|
2621
|
-
await user.keyboard('{Escape}');
|
|
2622
|
-
expect(trigger).toHaveFocus();
|
|
2623
|
-
});
|
|
2624
|
-
|
|
2625
|
-
it('keeps focus on the trigger when the submenu closes before its first frame', async () => {
|
|
2626
|
-
const frames = holdAnimationFrames();
|
|
2627
|
-
const user = userEvent.setup();
|
|
2628
|
-
renderCollapsed(
|
|
2629
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
2630
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2631
|
-
</SideNavItem>,
|
|
2632
|
-
);
|
|
2633
|
-
|
|
2634
|
-
const trigger = screen.getByTestId('parent');
|
|
2635
|
-
await user.click(trigger);
|
|
2636
|
-
await user.keyboard('{Escape}');
|
|
2637
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2638
|
-
|
|
2639
|
-
// The open's auto-focus frame, late on a busy runner, must not land
|
|
2640
|
-
// focus in the closed submenu.
|
|
2641
|
-
frames.flush();
|
|
2642
|
-
expect(trigger).toHaveFocus();
|
|
2643
|
-
});
|
|
2644
|
-
});
|
|
2645
|
-
|
|
2646
|
-
// =============================================================================
|
|
2647
|
-
// Audit regressions — the defects this pass fixed
|
|
2648
|
-
// =============================================================================
|
|
2649
|
-
|
|
2650
|
-
describe('SideNav audit regressions', () => {
|
|
2651
|
-
it('names the collapsed submenu dialog from the catalog, not a concatenation', async () => {
|
|
2652
|
-
const user = userEvent.setup();
|
|
2653
|
-
renderCollapsed(
|
|
2654
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
2655
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2656
|
-
</SideNavItem>,
|
|
2657
|
-
);
|
|
2658
|
-
|
|
2659
|
-
await user.click(screen.getByTestId('parent'));
|
|
2660
|
-
const dialog = document.querySelector('[role="dialog"]');
|
|
2661
|
-
expect(dialog).not.toBeNull();
|
|
2662
|
-
expect(dialog).toHaveAttribute('aria-label', 'Settings submenu');
|
|
2663
|
-
});
|
|
2664
|
-
|
|
2665
|
-
it('translates the collapsed submenu dialog name', async () => {
|
|
2666
|
-
const user = userEvent.setup();
|
|
2667
|
-
render(
|
|
2668
|
-
<InternationalizationProvider
|
|
2669
|
-
locale="pseudo"
|
|
2670
|
-
messages={{pseudo: pseudoCatalog}}>
|
|
2671
|
-
<SideNavCollapseContext
|
|
2672
|
-
value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
|
|
2673
|
-
<SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
|
|
2674
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2675
|
-
</SideNavItem>
|
|
2676
|
-
</SideNavCollapseContext>
|
|
2677
|
-
</InternationalizationProvider>,
|
|
2678
|
-
);
|
|
2679
|
-
|
|
2680
|
-
await user.click(screen.getByTestId('parent'));
|
|
2681
|
-
const label = document
|
|
2682
|
-
.querySelector('[role="dialog"]')
|
|
2683
|
-
?.getAttribute('aria-label');
|
|
2684
|
-
|
|
2685
|
-
// A concatenated `${label} submenu` would stay plain English here.
|
|
2686
|
-
expect(label).toContain('Settings');
|
|
2687
|
-
expect(label).not.toBe('Settings submenu');
|
|
2688
|
-
});
|
|
2689
|
-
|
|
2690
|
-
it('forwards unrecognised props on SideNavItem through to the control', () => {
|
|
2691
|
-
render(
|
|
2692
|
-
<SideNavItem
|
|
2693
|
-
label="Dashboard"
|
|
2694
|
-
href="/dashboard"
|
|
2695
|
-
data-analytics="nav-dashboard"
|
|
2696
|
-
aria-describedby="hint"
|
|
2697
|
-
/>,
|
|
2698
|
-
);
|
|
2699
|
-
|
|
2700
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
2701
|
-
expect(link).toHaveAttribute('data-analytics', 'nav-dashboard');
|
|
2702
|
-
expect(link).toHaveAttribute('aria-describedby', 'hint');
|
|
2703
|
-
});
|
|
2704
|
-
|
|
2705
|
-
it('forwards unrecognised props on a collapsed SideNavItem', () => {
|
|
2706
|
-
renderCollapsed(
|
|
2707
|
-
<SideNavItem
|
|
2708
|
-
label="Dashboard"
|
|
2709
|
-
icon={StubIcon}
|
|
2710
|
-
href="/dashboard"
|
|
2711
|
-
data-analytics="nav-dashboard"
|
|
2712
|
-
/>,
|
|
2713
|
-
);
|
|
2714
|
-
|
|
2715
|
-
expect(screen.getByRole('link')).toHaveAttribute(
|
|
2716
|
-
'data-analytics',
|
|
2717
|
-
'nav-dashboard',
|
|
2718
|
-
);
|
|
2719
|
-
});
|
|
2720
|
-
|
|
2721
|
-
it('exposes a disabled item as a theming state', () => {
|
|
2722
|
-
render(<SideNavItem label="Archived" onClick={() => {}} isDisabled />);
|
|
2723
|
-
|
|
2724
|
-
expect(screen.getByRole('button', {name: 'Archived'})).toHaveAttribute(
|
|
2725
|
-
'data-disabled',
|
|
2726
|
-
'disabled',
|
|
2727
|
-
);
|
|
2728
|
-
});
|
|
2729
|
-
|
|
2730
|
-
it('keeps a hidden section header readable by assistive technology', () => {
|
|
2731
|
-
render(
|
|
2732
|
-
<SideNavSection title="Main" isHeaderHidden>
|
|
2733
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
2734
|
-
</SideNavSection>,
|
|
2735
|
-
);
|
|
2736
|
-
|
|
2737
|
-
expect(screen.getByRole('group')).toHaveAccessibleName('Main');
|
|
2738
|
-
expect(screen.getByText('Main')).toBeInTheDocument();
|
|
2739
|
-
});
|
|
2740
|
-
|
|
2741
|
-
it('keeps a SideNavCollapseButton outside the sidenav in step with it', async () => {
|
|
2742
|
-
const user = userEvent.setup();
|
|
2743
|
-
|
|
2744
|
-
function App() {
|
|
2745
|
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
2746
|
-
const collapsible = {isCollapsed, onCollapsedChange: setIsCollapsed};
|
|
2747
|
-
return (
|
|
2748
|
-
<>
|
|
2749
|
-
<SideNavCollapseButton collapsible={collapsible} />
|
|
2750
|
-
<SideNav collapsible={{...collapsible, hasButton: false}}>
|
|
2751
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
2752
|
-
</SideNav>
|
|
2753
|
-
</>
|
|
2754
|
-
);
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
render(<App />);
|
|
2758
|
-
|
|
2759
|
-
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
2760
|
-
const expand = await screen.findByRole('button', {name: 'Expand sidebar'});
|
|
2761
|
-
|
|
2762
|
-
await user.click(expand);
|
|
2763
|
-
expect(
|
|
2764
|
-
await screen.findByRole('button', {name: 'Collapse sidebar'}),
|
|
2765
|
-
).toBeInTheDocument();
|
|
2766
|
-
});
|
|
2767
|
-
});
|
|
2768
|
-
|
|
2769
|
-
// =============================================================================
|
|
2770
|
-
// A15 — the shared focus ring
|
|
2771
|
-
//
|
|
2772
|
-
// jsdom does not derive `:focus-visible` from `.focus()` for an element
|
|
2773
|
-
// matched directly, so the ring a tab stop draws on itself cannot be read
|
|
2774
|
-
// back here; the geometry is measured in a real browser instead. These pin
|
|
2775
|
-
// the composition — that the focusable element carries the classes
|
|
2776
|
-
// `focusOutlineStyles.focusVisible` compiles to.
|
|
2777
|
-
//
|
|
2778
|
-
// Composition is only proof for a tab stop. A row wrapper is not one, so
|
|
2779
|
-
// `:focus-visible` atoms sitting on it can never match, and asserting them
|
|
2780
|
-
// there proves nothing: the actions-row tests below passed with the
|
|
2781
|
-
// wrapper's ring deleted outright. A wrapper's ring is guarded by `:has()`,
|
|
2782
|
-
// which jsdom *does* re-evaluate against live focus, so those rules are read
|
|
2783
|
-
// out of the stylesheet and matched against the DOM instead.
|
|
2784
|
-
// =============================================================================
|
|
2785
|
-
|
|
2786
|
-
const sharedFocusRingClasses = stylex
|
|
2787
|
-
.props(focusOutlineStyles.focusVisible)
|
|
2788
|
-
.className!.split(' ');
|
|
2789
|
-
|
|
2790
|
-
function expectSharedFocusRing(el: Element) {
|
|
2791
|
-
const classes = el.className.split(' ');
|
|
2792
|
-
for (const c of sharedFocusRingClasses) {
|
|
2793
|
-
expect(classes).toContain(c);
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
function expectNoSharedFocusRing(el: Element) {
|
|
2798
|
-
const classes = el.className.split(' ');
|
|
2799
|
-
for (const c of sharedFocusRingClasses) {
|
|
2800
|
-
expect(classes).not.toContain(c);
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
/** Every style rule in the live sheet, flattened out of any grouping rule. */
|
|
2805
|
-
function allStyleRules(): CSSStyleRule[] {
|
|
2806
|
-
const out: CSSStyleRule[] = [];
|
|
2807
|
-
const walk = (rules: CSSRuleList) => {
|
|
2808
|
-
for (const rule of Array.from(rules)) {
|
|
2809
|
-
const nested = (rule as CSSGroupingRule).cssRules;
|
|
2810
|
-
if (nested) {
|
|
2811
|
-
walk(nested);
|
|
2812
|
-
}
|
|
2813
|
-
if ((rule as CSSStyleRule).selectorText) {
|
|
2814
|
-
out.push(rule as CSSStyleRule);
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
};
|
|
2818
|
-
for (const sheet of Array.from(document.styleSheets)) {
|
|
2819
|
-
try {
|
|
2820
|
-
walk(sheet.cssRules);
|
|
2821
|
-
} catch {
|
|
2822
|
-
// A sheet we cannot read cannot be one StyleX injected.
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
return out;
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
/**
|
|
2829
|
-
* A rule's declared value for `prop`, read out of its text.
|
|
2830
|
-
*
|
|
2831
|
-
* Not `rule.style`: these values are `var()`s, and jsdom's CSS object model
|
|
2832
|
-
* drops longhands it cannot parse, so they read back as an empty string there.
|
|
2833
|
-
*/
|
|
2834
|
-
function declaredValue(rule: CSSStyleRule, prop: string): string | undefined {
|
|
2835
|
-
const body = /\{([^}]*)\}/.exec(rule.cssText)?.[1] ?? '';
|
|
2836
|
-
return new RegExp(`(?:^|;)\\s*${prop}:\\s*([^;]+)`).exec(body)?.[1].trim();
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
/** A rule's `outline-style` — the discriminator for a painting focus ring. */
|
|
2840
|
-
function outlineStyleOf(rule: CSSStyleRule): string | undefined {
|
|
2841
|
-
return declaredValue(rule, 'outline-style');
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
/**
|
|
2845
|
-
* What the live sheet unconditionally declares for `prop` on `el`, gathered
|
|
2846
|
-
* from every bare-class rule the element carries. StyleX emits one atomic
|
|
2847
|
-
* class per property, so a well-formed element yields exactly one value —
|
|
2848
|
-
* comparing those values compares real CSS without pinning a generated hash.
|
|
2849
|
-
*/
|
|
2850
|
-
function declaredOn(el: Element, prop: string): string[] {
|
|
2851
|
-
const classes = new Set(el.className.split(' '));
|
|
2852
|
-
return allStyleRules()
|
|
2853
|
-
.filter(rule => {
|
|
2854
|
-
const bare = rule.selectorText.replace(/:not\(#\\?#\)/g, '');
|
|
2855
|
-
const owner = /^\.([^:\s]+)$/.exec(bare)?.[1];
|
|
2856
|
-
return owner != null && classes.has(owner);
|
|
2857
|
-
})
|
|
2858
|
-
.map(rule => declaredValue(rule, prop))
|
|
2859
|
-
.filter(value => value != null);
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
/**
|
|
2863
|
-
* The nesting indent a row carries, or null for none. The indent is a
|
|
2864
|
-
* runtime StyleX value, so it rides the row's inline style as a custom
|
|
2865
|
-
* property; read the value rather than the generated property name.
|
|
2866
|
-
*/
|
|
2867
|
-
function indentOf(el: HTMLElement): string | null {
|
|
2868
|
-
const {style} = el;
|
|
2869
|
-
for (let i = 0; i < style.length; i++) {
|
|
2870
|
-
const value = style.getPropertyValue(style[i]).trim();
|
|
2871
|
-
if (value.includes('var(--spacing-6)')) {
|
|
2872
|
-
return value;
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
return null;
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
/**
|
|
2879
|
-
* The selectors that actually paint the shared ring on `el`, read out of the
|
|
2880
|
-
* live stylesheet with StyleX's `:not(#\#)` specificity padding stripped so
|
|
2881
|
-
* jsdom can evaluate them.
|
|
2882
|
-
*
|
|
2883
|
-
* `outline-style` is the discriminator: StyleX emits one rule per property,
|
|
2884
|
-
* and no outline paints while that one resolves to `none`.
|
|
2885
|
-
*/
|
|
2886
|
-
function ringSelectorsFor(el: Element): string[] {
|
|
2887
|
-
const classes = new Set(el.className.split(' '));
|
|
2888
|
-
return allStyleRules()
|
|
2889
|
-
.filter(rule => {
|
|
2890
|
-
const painted = outlineStyleOf(rule);
|
|
2891
|
-
if (!painted || painted === 'none') {
|
|
2892
|
-
return false;
|
|
2893
|
-
}
|
|
2894
|
-
const owner = /^\.([^:\s]+)/.exec(rule.selectorText)?.[1];
|
|
2895
|
-
return owner != null && classes.has(owner);
|
|
2896
|
-
})
|
|
2897
|
-
.map(rule => rule.selectorText.replace(/:not\(#\\?#\)/g, ''));
|
|
2898
|
-
}
|
|
2899
|
-
|
|
2900
|
-
/**
|
|
2901
|
-
* Whether `el`'s ring is painting for whatever holds focus right now.
|
|
2902
|
-
*
|
|
2903
|
-
* `:focus-visible` is substituted with `:focus` before matching. jsdom
|
|
2904
|
-
* derives `:focus-visible` from a heuristic over the last interaction, so it
|
|
2905
|
-
* answers differently depending on which tests ran before — the same
|
|
2906
|
-
* assertion passes alone and fails in the file. `:focus` is unambiguous, and
|
|
2907
|
-
* the ring's modality is pinned separately, on the selector text, by the
|
|
2908
|
-
* keyboard-only test below.
|
|
2909
|
-
*/
|
|
2910
|
-
function ringIsPainting(el: Element): boolean {
|
|
2911
|
-
return ringSelectorsFor(el).some(selector =>
|
|
2912
|
-
el.matches(selector.replace(/:focus-visible/g, ':focus')),
|
|
2913
|
-
);
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
/**
|
|
2917
|
-
* Whether `el` unconditionally declares `outline-style: none`, which is what
|
|
2918
|
-
* keeps the UA's own focus ring off an element whose ring an ancestor paints.
|
|
2919
|
-
* Computed style cannot answer this: `none` is also the CSS initial value.
|
|
2920
|
-
*/
|
|
2921
|
-
function suppressesOwnOutline(el: Element): boolean {
|
|
2922
|
-
const classes = new Set(el.className.split(' '));
|
|
2923
|
-
return allStyleRules().some(rule => {
|
|
2924
|
-
if (outlineStyleOf(rule) !== 'none') {
|
|
2925
|
-
return false;
|
|
2926
|
-
}
|
|
2927
|
-
// Unconditional only — a bare class once the padding is stripped.
|
|
2928
|
-
const bare = rule.selectorText.replace(/:not\(#\\?#\)/g, '');
|
|
2929
|
-
const owner = /^\.([^:\s]+)$/.exec(bare)?.[1];
|
|
2930
|
-
return owner != null && classes.has(owner);
|
|
2931
|
-
});
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
describe('SideNav focus ring (A15)', () => {
|
|
2935
|
-
it('draws the shared ring on a link item', () => {
|
|
2936
|
-
render(<SideNavItem label="Dashboard" href="/dashboard" />);
|
|
2937
|
-
expectSharedFocusRing(screen.getByRole('link', {name: 'Dashboard'}));
|
|
2938
|
-
});
|
|
2939
|
-
|
|
2940
|
-
it('draws the shared ring on a button item', () => {
|
|
2941
|
-
render(<SideNavItem label="Dashboard" onClick={() => {}} />);
|
|
2942
|
-
expectSharedFocusRing(screen.getByRole('button', {name: 'Dashboard'}));
|
|
2943
|
-
});
|
|
2944
|
-
|
|
2945
|
-
it('draws the shared ring on a collapsed icon-only item', () => {
|
|
2946
|
-
renderCollapsed(
|
|
2947
|
-
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
|
|
2948
|
-
);
|
|
2949
|
-
expectSharedFocusRing(screen.getByRole('link', {name: 'Dashboard'}));
|
|
2950
|
-
});
|
|
2951
|
-
|
|
2952
|
-
it('draws the shared ring on a collapsed submenu trigger', () => {
|
|
2953
|
-
renderCollapsed(
|
|
2954
|
-
<SideNavItem label="Settings" icon={StubIcon}>
|
|
2955
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2956
|
-
</SideNavItem>,
|
|
2957
|
-
);
|
|
2958
|
-
expectSharedFocusRing(screen.getByRole('button', {name: 'Settings'}));
|
|
2959
|
-
});
|
|
2960
|
-
|
|
2961
|
-
it('rings each focusable of a split-action row, and not the row itself', () => {
|
|
2962
|
-
render(
|
|
2963
|
-
<SideNavItem label="Settings" href="/settings" collapsible>
|
|
2964
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
2965
|
-
</SideNavItem>,
|
|
2966
|
-
);
|
|
2967
|
-
|
|
2968
|
-
const link = screen.getByRole('link', {name: 'Settings'});
|
|
2969
|
-
const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
|
|
2970
|
-
expectSharedFocusRing(link);
|
|
2971
|
-
expectSharedFocusRing(toggle);
|
|
2972
|
-
|
|
2973
|
-
// A presentational <div> holding two independent tab stops; ringing it
|
|
2974
|
-
// too would paint a second outline around the whole row.
|
|
2975
|
-
const row = link.parentElement!;
|
|
2976
|
-
expect(row.tagName).toBe('DIV');
|
|
2977
|
-
expect(row).toContainElement(toggle);
|
|
2978
|
-
expectNoSharedFocusRing(row);
|
|
2979
|
-
});
|
|
2980
|
-
|
|
2981
|
-
it('rings the row wrapper of an actions row, not the truncated primary', async () => {
|
|
2982
|
-
const user = userEvent.setup();
|
|
2983
|
-
render(
|
|
2984
|
-
<SideNavItem
|
|
2985
|
-
label="Project"
|
|
2986
|
-
href="/project"
|
|
2987
|
-
actions={
|
|
2988
|
-
<button type="button" data-testid="row-action">
|
|
2989
|
-
More
|
|
2990
|
-
</button>
|
|
2991
|
-
}
|
|
2992
|
-
/>,
|
|
2993
|
-
);
|
|
2994
|
-
|
|
2995
|
-
const link = screen.getByRole('link', {name: 'Project'});
|
|
2996
|
-
const row = link.parentElement!;
|
|
2997
|
-
expect(row.tagName).toBe('DIV');
|
|
2998
|
-
expect(row).toContainElement(screen.getByTestId('row-action'));
|
|
2999
|
-
|
|
3000
|
-
// Same pill as an ordinary row: the ring lives on the wrapper, whose box
|
|
3001
|
-
// includes the actions. Putting it on the primary would clip to the
|
|
3002
|
-
// truncated split-action element (square, not full-row).
|
|
3003
|
-
expect(ringIsPainting(row)).toBe(false);
|
|
3004
|
-
await user.tab();
|
|
3005
|
-
expect(link).toHaveFocus();
|
|
3006
|
-
expect(ringIsPainting(row)).toBe(true);
|
|
3007
|
-
// And exactly one ring: nothing paints a second one on the primary.
|
|
3008
|
-
expect(ringSelectorsFor(link)).toEqual([]);
|
|
3009
|
-
});
|
|
3010
|
-
|
|
3011
|
-
it('leaves the wrapper ring to the primary, not the chevron toggle', async () => {
|
|
3012
|
-
const user = userEvent.setup();
|
|
3013
|
-
render(
|
|
3014
|
-
<SideNavItem
|
|
3015
|
-
label="Settings"
|
|
3016
|
-
href="/settings"
|
|
3017
|
-
collapsible
|
|
3018
|
-
actions={
|
|
3019
|
-
<button type="button" data-testid="row-action">
|
|
3020
|
-
More
|
|
3021
|
-
</button>
|
|
3022
|
-
}>
|
|
3023
|
-
<SideNavItem label="General" href="/settings/general" />
|
|
3024
|
-
</SideNavItem>,
|
|
3025
|
-
);
|
|
3026
|
-
|
|
3027
|
-
const link = screen.getByRole('link', {name: 'Settings'});
|
|
3028
|
-
const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
|
|
3029
|
-
const row = link.parentElement!;
|
|
3030
|
-
expect(row).toContainElement(screen.getByTestId('row-action'));
|
|
3031
|
-
|
|
3032
|
-
await user.tab();
|
|
3033
|
-
expect(link).toHaveFocus();
|
|
3034
|
-
expect(ringIsPainting(row)).toBe(true);
|
|
3035
|
-
|
|
3036
|
-
// The toggle owns its own ring, as it does in a split-action row with no
|
|
3037
|
-
// actions. Keeping the wrapper lit too would stack a full-row outline
|
|
3038
|
-
// around the chevron's own — two rings for one tab stop.
|
|
3039
|
-
await user.tab();
|
|
3040
|
-
expect(toggle).toHaveFocus();
|
|
3041
|
-
expectSharedFocusRing(toggle);
|
|
3042
|
-
expect(ringIsPainting(row)).toBe(false);
|
|
3043
|
-
});
|
|
3044
|
-
|
|
3045
|
-
it('does not ring the row wrapper when a supplied action takes focus', async () => {
|
|
3046
|
-
const user = userEvent.setup();
|
|
3047
|
-
render(
|
|
3048
|
-
<SideNavItem
|
|
3049
|
-
label="Project"
|
|
3050
|
-
href="/project"
|
|
3051
|
-
actions={
|
|
3052
|
-
<button type="button" data-testid="row-action">
|
|
3053
|
-
More
|
|
3054
|
-
</button>
|
|
3055
|
-
}
|
|
3056
|
-
/>,
|
|
3057
|
-
);
|
|
3058
|
-
|
|
3059
|
-
const row = screen.getByRole('link', {name: 'Project'}).parentElement!;
|
|
3060
|
-
await user.tab();
|
|
3061
|
-
expect(ringIsPainting(row)).toBe(true);
|
|
3062
|
-
|
|
3063
|
-
// An action draws whatever ring its own component draws. Focus is not on
|
|
3064
|
-
// the row, so the row must not light up around it.
|
|
3065
|
-
await user.tab();
|
|
3066
|
-
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
3067
|
-
expect(ringIsPainting(row)).toBe(false);
|
|
3068
|
-
});
|
|
3069
|
-
|
|
3070
|
-
it('rings the wrapper of a whole-row toggle carrying actions', async () => {
|
|
3071
|
-
const user = userEvent.setup();
|
|
3072
|
-
render(
|
|
3073
|
-
<SideNavItem
|
|
3074
|
-
label="Project"
|
|
3075
|
-
collapsible
|
|
3076
|
-
actions={
|
|
3077
|
-
<button type="button" data-testid="row-action">
|
|
3078
|
-
More
|
|
3079
|
-
</button>
|
|
3080
|
-
}>
|
|
3081
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
3082
|
-
</SideNavItem>,
|
|
3083
|
-
);
|
|
3084
|
-
|
|
3085
|
-
// No href and no onClick, so the primary *is* the collapse toggle and
|
|
3086
|
-
// keeps the chevron inside it — a different row shape from the split.
|
|
3087
|
-
const primary = screen.getByRole('button', {name: 'Project'});
|
|
3088
|
-
const row = primary.parentElement!;
|
|
3089
|
-
|
|
3090
|
-
await user.tab();
|
|
3091
|
-
expect(primary).toHaveFocus();
|
|
3092
|
-
expect(ringIsPainting(row)).toBe(true);
|
|
3093
|
-
|
|
3094
|
-
await user.tab();
|
|
3095
|
-
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
3096
|
-
expect(ringIsPainting(row)).toBe(false);
|
|
3097
|
-
});
|
|
3098
|
-
|
|
3099
|
-
it('keeps the UA focus ring off the primary the wrapper rings for', () => {
|
|
3100
|
-
render(
|
|
3101
|
-
<SideNavItem
|
|
3102
|
-
label="Project"
|
|
3103
|
-
href="/project"
|
|
3104
|
-
actions={
|
|
3105
|
-
<button type="button" data-testid="row-action">
|
|
3106
|
-
More
|
|
3107
|
-
</button>
|
|
3108
|
-
}
|
|
3109
|
-
/>,
|
|
3110
|
-
);
|
|
3111
|
-
|
|
3112
|
-
const link = screen.getByRole('link', {name: 'Project'});
|
|
3113
|
-
// Dropping the shared ring off the primary also drops the
|
|
3114
|
-
// `outline-style: none` default that came with it, and a bare <a> keeps
|
|
3115
|
-
// the browser's own focus ring — which would paint inside the wrapper's.
|
|
3116
|
-
expect(suppressesOwnOutline(link)).toBe(true);
|
|
3117
|
-
});
|
|
3118
|
-
|
|
3119
|
-
it('paints the actions-row ring for keyboard focus only', () => {
|
|
3120
|
-
render(
|
|
3121
|
-
<SideNavItem
|
|
3122
|
-
label="Project"
|
|
3123
|
-
href="/project"
|
|
3124
|
-
actions={
|
|
3125
|
-
<button type="button" data-testid="row-action">
|
|
3126
|
-
More
|
|
3127
|
-
</button>
|
|
3128
|
-
}
|
|
3129
|
-
/>,
|
|
3130
|
-
);
|
|
3131
|
-
|
|
3132
|
-
const link = screen.getByRole('link', {name: 'Project'});
|
|
3133
|
-
const row = link.parentElement!;
|
|
3134
|
-
// The wrapper is not a tab stop, so its ring has to be guarded by a
|
|
3135
|
-
// relational selector. `:focus-within` or `:focus` would light the row
|
|
3136
|
-
// for mouse users too; only `:focus-visible` keeps it to the keyboard.
|
|
3137
|
-
const selectors = ringSelectorsFor(row);
|
|
3138
|
-
expect(selectors.length).toBeGreaterThan(0);
|
|
3139
|
-
for (const selector of selectors) {
|
|
3140
|
-
expect(selector).toContain(':focus-visible');
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
// The guard is scoped to the wrapper's first child. Reordering the row
|
|
3144
|
-
// would hand the ring to the toggle or the actions instead.
|
|
3145
|
-
expect(row.firstElementChild).toBe(link);
|
|
3146
|
-
});
|
|
3147
|
-
|
|
3148
|
-
it('draws the shared ring on a heading rendered as one link', () => {
|
|
3149
|
-
render(<SideNavHeading heading="My App" headingHref="/" />);
|
|
3150
|
-
expectSharedFocusRing(screen.getByRole('link', {name: /My App/}));
|
|
3151
|
-
});
|
|
3152
|
-
|
|
3153
|
-
it('draws the shared ring on a collapsed heading link', () => {
|
|
3154
|
-
renderCollapsed(
|
|
3155
|
-
<SideNavHeading heading="My App" headingHref="/" icon={<span>i</span>} />,
|
|
3156
|
-
);
|
|
3157
|
-
expectSharedFocusRing(screen.getByRole('link', {name: 'My App'}));
|
|
3158
|
-
});
|
|
3159
|
-
|
|
3160
|
-
it('draws the shared ring on the heading menu trigger', () => {
|
|
3161
|
-
render(<SideNavHeading heading="My App" menu={<div>menu</div>} />);
|
|
3162
|
-
expectSharedFocusRing(screen.getByRole('button', {name: 'Open menu'}));
|
|
3163
|
-
});
|
|
3164
|
-
});
|
|
3165
|
-
|
|
3166
|
-
// jsdom cannot emulate forced-colors rendering, so these assert that the
|
|
3167
|
-
// compiled output carries the rules the selected row relies on. The rendered
|
|
3168
|
-
// behaviour was measured in Chromium under `forced-colors: active` — see the
|
|
3169
|
-
// evidence on #4880.
|
|
3170
|
-
describe('forced colors (WCAG 1.4.11)', () => {
|
|
3171
|
-
it('compiles forced-colors overrides so the current page survives Windows High Contrast', () => {
|
|
3172
|
-
render(
|
|
3173
|
-
<SideNav>
|
|
3174
|
-
<SideNavSection title="Main">
|
|
3175
|
-
<SideNavItem label="Dashboard" href="/dashboard" isSelected />
|
|
3176
|
-
<SideNavItem label="Projects" href="/projects" />
|
|
3177
|
-
</SideNavSection>
|
|
3178
|
-
</SideNav>,
|
|
3179
|
-
);
|
|
3180
|
-
const css = getForcedColorsRules();
|
|
3181
|
-
// `--color-neutral` is a background: forced colors flatten it to Canvas
|
|
3182
|
-
// and the selected row loses every trace of being selected. Highlight /
|
|
3183
|
-
// HighlightText is the platform convention that survives.
|
|
3184
|
-
expect(css).toContain('background-color: highlight;');
|
|
3185
|
-
expect(css).toContain('color: highlighttext;');
|
|
3186
|
-
// The hover fill has to hold the Highlight too, or hovering the current
|
|
3187
|
-
// page repaints it with the ordinary hover overlay and erases the state.
|
|
3188
|
-
// Nesting it inside the hover media query is what wins that fight: the
|
|
3189
|
-
// rule compiles to a triple-class selector, which outranks `item`'s
|
|
3190
|
-
// single-class hover overlay no matter the source order.
|
|
3191
|
-
expect(css).toMatch(
|
|
3192
|
-
/@media \(hover: hover\)[\s\S]*?forced-colors: active[\s\S]*?:hover[\s\S]*?background-color: highlight/,
|
|
3193
|
-
);
|
|
3194
|
-
});
|
|
3195
|
-
|
|
3196
|
-
it('does not opt nav rows out of UA colour remapping', () => {
|
|
3197
|
-
render(
|
|
3198
|
-
<SideNav>
|
|
3199
|
-
<SideNavSection title="Main">
|
|
3200
|
-
<SideNavItem label="Dashboard" href="/dashboard" isSelected />
|
|
3201
|
-
</SideNavSection>
|
|
3202
|
-
</SideNav>,
|
|
3203
|
-
);
|
|
3204
|
-
// A nav row is not a native form control — it resets `appearance` and
|
|
3205
|
-
// paints its own background, so the system keywords land without
|
|
3206
|
-
// `forced-color-adjust: none`. Setting it would inherit into
|
|
3207
|
-
// `endContent`, keeping a Badge's authored fill instead of remapping it.
|
|
3208
|
-
const row = screen.getByRole('link', {name: 'Dashboard'});
|
|
3209
|
-
expect(getComputedStyle(row).forcedColorAdjust).not.toBe('none');
|
|
3210
|
-
});
|
|
3211
|
-
});
|
|
3212
|
-
|
|
3213
|
-
// =============================================================================
|
|
3214
|
-
// Footer icon row — one size for the whole row
|
|
3215
|
-
// =============================================================================
|
|
3216
|
-
|
|
3217
|
-
describe('SideNav footer icon row sizing', () => {
|
|
3218
|
-
/**
|
|
3219
|
-
* The class(es) a `size="sm"` Button carries that a `size="md"` one does
|
|
3220
|
-
* not — i.e. whatever StyleX compiled the sm height into. Comparing class
|
|
3221
|
-
* sets keeps the assertion off the generated hashes.
|
|
3222
|
-
*/
|
|
3223
|
-
function sizeOnlyClasses(size: 'sm' | 'lg'): string[] {
|
|
3224
|
-
const {unmount} = render(
|
|
3225
|
-
<>
|
|
3226
|
-
<Button
|
|
3227
|
-
label="sized reference"
|
|
3228
|
-
size={size}
|
|
3229
|
-
isIconOnly
|
|
3230
|
-
icon={<span />}
|
|
3231
|
-
data-testid="sized-ref"
|
|
3232
|
-
/>
|
|
3233
|
-
<Button
|
|
3234
|
-
label="md reference"
|
|
3235
|
-
size="md"
|
|
3236
|
-
isIconOnly
|
|
3237
|
-
icon={<span />}
|
|
3238
|
-
data-testid="md-ref"
|
|
3239
|
-
/>
|
|
3240
|
-
</>,
|
|
3241
|
-
);
|
|
3242
|
-
const sized = new Set(screen.getByTestId('sized-ref').classList);
|
|
3243
|
-
const md = new Set(screen.getByTestId('md-ref').classList);
|
|
3244
|
-
const only = [...sized].filter(c => !md.has(c));
|
|
3245
|
-
unmount();
|
|
3246
|
-
// Guard against a vacuous pass: if the two sizes compiled to the same
|
|
3247
|
-
// classes there is nothing to assert and every check would trivially hold.
|
|
3248
|
-
expect(only.length).toBeGreaterThan(0);
|
|
3249
|
-
return only;
|
|
3250
|
-
}
|
|
3251
|
-
|
|
3252
|
-
it('renders the built-in collapse button at the row size, not the Button default', () => {
|
|
3253
|
-
const smClasses = sizeOnlyClasses('sm');
|
|
3254
|
-
render(
|
|
3255
|
-
<SideNav collapsible>
|
|
3256
|
-
<SideNavSection title="Main">
|
|
3257
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
3258
|
-
</SideNavSection>
|
|
3259
|
-
</SideNav>,
|
|
3260
|
-
);
|
|
3261
|
-
const collapseButton = screen.getByRole('button', {
|
|
3262
|
-
name: /collapse sidebar/i,
|
|
3263
|
-
});
|
|
3264
|
-
for (const className of smClasses) {
|
|
3265
|
-
expect(collapseButton).toHaveClass(className);
|
|
3266
|
-
}
|
|
3267
|
-
});
|
|
3268
|
-
|
|
3269
|
-
it('cascades the same size to footerIcons the consumer did not size', () => {
|
|
3270
|
-
const smClasses = sizeOnlyClasses('sm');
|
|
3271
|
-
render(
|
|
3272
|
-
<SideNav
|
|
3273
|
-
collapsible
|
|
3274
|
-
footerIcons={
|
|
3275
|
-
<Button label="Help" isIconOnly icon={<span />} data-testid="help" />
|
|
3276
|
-
}>
|
|
3277
|
-
<SideNavSection title="Main">
|
|
3278
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
3279
|
-
</SideNavSection>
|
|
3280
|
-
</SideNav>,
|
|
3281
|
-
);
|
|
3282
|
-
const help = screen.getByTestId('help');
|
|
3283
|
-
for (const className of smClasses) {
|
|
3284
|
-
expect(help).toHaveClass(className);
|
|
3285
|
-
}
|
|
3286
|
-
});
|
|
3287
|
-
|
|
3288
|
-
it('lets an explicit size on a footer icon win over the cascade', () => {
|
|
3289
|
-
const lgClasses = sizeOnlyClasses('lg');
|
|
3290
|
-
render(
|
|
3291
|
-
<SideNav
|
|
3292
|
-
collapsible
|
|
3293
|
-
footerIcons={
|
|
3294
|
-
<Button
|
|
3295
|
-
label="Help"
|
|
3296
|
-
size="lg"
|
|
3297
|
-
isIconOnly
|
|
3298
|
-
icon={<span />}
|
|
3299
|
-
data-testid="help"
|
|
3300
|
-
/>
|
|
3301
|
-
}>
|
|
3302
|
-
<SideNavSection title="Main">
|
|
3303
|
-
<SideNavItem label="Dashboard" href="/dashboard" />
|
|
3304
|
-
</SideNavSection>
|
|
3305
|
-
</SideNav>,
|
|
3306
|
-
);
|
|
3307
|
-
const help = screen.getByTestId('help');
|
|
3308
|
-
for (const className of lgClasses) {
|
|
3309
|
-
expect(help).toHaveClass(className);
|
|
3310
|
-
}
|
|
3311
|
-
});
|
|
3312
|
-
|
|
3313
|
-
it('honours an explicit size on SideNavCollapseButton outside the nav', () => {
|
|
3314
|
-
const lgClasses = sizeOnlyClasses('lg');
|
|
3315
|
-
render(
|
|
3316
|
-
<SideNavCollapseContext
|
|
3317
|
-
value={{isCollapsed: false, toggle: () => {}, isCollapsible: true}}>
|
|
3318
|
-
<SideNavCollapseButton size="lg" data-testid="collapse" />
|
|
3319
|
-
</SideNavCollapseContext>,
|
|
3320
|
-
);
|
|
3321
|
-
const collapse = screen.getByTestId('collapse');
|
|
3322
|
-
for (const className of lgClasses) {
|
|
3323
|
-
expect(collapse).toHaveClass(className);
|
|
3324
|
-
}
|
|
3325
|
-
});
|
|
3326
|
-
|
|
3327
|
-
it('centres the chevron instead of seating it on a text baseline', () => {
|
|
3328
|
-
renderExpanded(<SideNavCollapseButton data-testid="collapse" />);
|
|
3329
|
-
// Icon renders its own `astryx-icon` span around the svg; the RTL-mirror
|
|
3330
|
-
// wrapper this component adds is that span's parent.
|
|
3331
|
-
const iconSpan = screen
|
|
3332
|
-
.getByTestId('collapse')
|
|
3333
|
-
.querySelector('span.astryx-icon');
|
|
3334
|
-
const mirror = iconSpan?.parentElement;
|
|
3335
|
-
// The RTL-mirror wrapper is a flex item of Button's icon slot, so it
|
|
3336
|
-
// blockifies; as a block it gets a line box and the glyph sits on the
|
|
3337
|
-
// baseline, 2.42px above centre (measured in Chromium). A flex container
|
|
3338
|
-
// has no line box.
|
|
3339
|
-
expect(mirror && getComputedStyle(mirror).display).toBe('flex');
|
|
3340
|
-
});
|
|
3341
|
-
});
|
|
3342
|
-
|
|
3343
|
-
describe('SideNav row control sizing', () => {
|
|
3344
|
-
/**
|
|
3345
|
-
* The height an icon-only `Button` of this size declares — the box every
|
|
3346
|
-
* control sharing a nav row is measured against.
|
|
3347
|
-
*/
|
|
3348
|
-
function iconButtonHeight(size: 'sm' | 'md' | 'lg'): string[] {
|
|
3349
|
-
const {unmount} = render(
|
|
3350
|
-
<Button
|
|
3351
|
-
label="sized reference"
|
|
3352
|
-
size={size}
|
|
3353
|
-
isIconOnly
|
|
3354
|
-
icon={<span />}
|
|
3355
|
-
data-testid="sized-ref"
|
|
3356
|
-
/>,
|
|
3357
|
-
);
|
|
3358
|
-
const height = declaredOn(screen.getByTestId('sized-ref'), 'height');
|
|
3359
|
-
unmount();
|
|
3360
|
-
// Guard against a vacuous pass: nothing below means anything if the
|
|
3361
|
-
// reference itself declares no height.
|
|
3362
|
-
expect(height).toHaveLength(1);
|
|
3363
|
-
return height;
|
|
3364
|
-
}
|
|
3365
|
-
|
|
3366
|
-
function collapsibleRow(actions: ReactNode) {
|
|
3367
|
-
return (
|
|
3368
|
-
<SideNavItem
|
|
3369
|
-
label="Project"
|
|
3370
|
-
href="/project"
|
|
3371
|
-
collapsible
|
|
3372
|
-
actions={actions}>
|
|
3373
|
-
<SideNavItem label="Session" href="/project/session" />
|
|
3374
|
-
</SideNavItem>
|
|
3375
|
-
);
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
const iconAction = (props?: {size?: 'sm' | 'md' | 'lg'}) => (
|
|
3379
|
-
<Button
|
|
3380
|
-
label="Project actions"
|
|
3381
|
-
isIconOnly
|
|
3382
|
-
icon={<span />}
|
|
3383
|
-
data-testid="row-action"
|
|
3384
|
-
{...props}
|
|
3385
|
-
/>
|
|
3386
|
-
);
|
|
3387
|
-
|
|
3388
|
-
it('sizes the expand toggle like the icon buttons it sits beside', () => {
|
|
3389
|
-
const sm = iconButtonHeight('sm');
|
|
3390
|
-
render(collapsibleRow(iconAction()));
|
|
3391
|
-
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
3392
|
-
expect(declaredOn(toggle, 'height')).toEqual(sm);
|
|
3393
|
-
// Square, so its hover pill reads as the same box as the action's.
|
|
3394
|
-
expect(declaredOn(toggle, 'width')).toEqual(sm);
|
|
3395
|
-
});
|
|
3396
|
-
|
|
3397
|
-
it('cascades that size to an action the consumer did not size', () => {
|
|
3398
|
-
const sm = iconButtonHeight('sm');
|
|
3399
|
-
// Without the cascade an unsized Button falls back to `md`; if the two
|
|
3400
|
-
// compiled to one height the assertion below could not fail.
|
|
3401
|
-
expect(sm).not.toEqual(iconButtonHeight('md'));
|
|
3402
|
-
render(collapsibleRow(iconAction()));
|
|
3403
|
-
expect(declaredOn(screen.getByTestId('row-action'), 'height')).toEqual(sm);
|
|
3404
|
-
});
|
|
3405
|
-
|
|
3406
|
-
it('lets an explicit size on an action win over the cascade', () => {
|
|
3407
|
-
const lg = iconButtonHeight('lg');
|
|
3408
|
-
render(collapsibleRow(iconAction({size: 'lg'})));
|
|
3409
|
-
expect(declaredOn(screen.getByTestId('row-action'), 'height')).toEqual(lg);
|
|
3410
|
-
});
|
|
3411
|
-
});
|
|
3412
|
-
|
|
3413
|
-
describe('SideNavHeading hover/click guard', () => {
|
|
3414
|
-
// tabIndex={-1} matches real menu items; a bare role=menuitem div is not
|
|
3415
|
-
// focusable.
|
|
3416
|
-
const menuItems = (
|
|
3417
|
-
<>
|
|
3418
|
-
<div role="menuitem" tabIndex={-1}>
|
|
3419
|
-
Alpha
|
|
3420
|
-
</div>
|
|
3421
|
-
<div role="menuitem" tabIndex={-1}>
|
|
3422
|
-
Beta
|
|
3423
|
-
</div>
|
|
3424
|
-
</>
|
|
3425
|
-
);
|
|
3426
|
-
|
|
3427
|
-
it('keeps the menu open when a hover-open is immediately clicked', async () => {
|
|
3428
|
-
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
3429
|
-
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
3430
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
3431
|
-
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
3432
|
-
|
|
3433
|
-
await user.hover(trigger);
|
|
3434
|
-
act(() => {
|
|
3435
|
-
vi.advanceTimersByTime(50);
|
|
3436
|
-
});
|
|
3437
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
3438
|
-
|
|
3439
|
-
await user.click(trigger);
|
|
3440
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
3441
|
-
|
|
3442
|
-
vi.useRealTimers();
|
|
3443
|
-
});
|
|
3444
|
-
|
|
3445
|
-
it('closes on a click that lands well after the hover-open', async () => {
|
|
3446
|
-
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
3447
|
-
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
3448
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
3449
|
-
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
3450
|
-
|
|
3451
|
-
await user.hover(trigger);
|
|
3452
|
-
act(() => {
|
|
3453
|
-
vi.advanceTimersByTime(50);
|
|
3454
|
-
});
|
|
3455
|
-
act(() => {
|
|
3456
|
-
vi.advanceTimersByTime(1200);
|
|
3457
|
-
});
|
|
3458
|
-
|
|
3459
|
-
await user.click(trigger);
|
|
3460
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
3461
|
-
|
|
3462
|
-
vi.useRealTimers();
|
|
3463
|
-
});
|
|
3464
|
-
|
|
3465
|
-
it('leaves focus on the trigger for a hover-open, and moves it in on click', async () => {
|
|
3466
|
-
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
3467
|
-
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
3468
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
3469
|
-
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
3470
|
-
|
|
3471
|
-
await user.hover(trigger);
|
|
3472
|
-
act(() => {
|
|
3473
|
-
vi.advanceTimersByTime(50);
|
|
3474
|
-
});
|
|
3475
|
-
const firstItem = screen.getAllByRole('menuitem', {hidden: true})[0];
|
|
3476
|
-
expect(firstItem).not.toHaveFocus();
|
|
3477
|
-
|
|
3478
|
-
await user.click(trigger);
|
|
3479
|
-
expect(firstItem).toHaveFocus();
|
|
3480
|
-
|
|
3481
|
-
vi.useRealTimers();
|
|
3482
|
-
});
|
|
3483
|
-
|
|
3484
|
-
it('returns focus to the trigger on Escape', async () => {
|
|
3485
|
-
const user = userEvent.setup();
|
|
3486
|
-
render(<SideNavHeading heading="My App" menu={menuItems} />);
|
|
3487
|
-
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
3488
|
-
|
|
3489
|
-
await user.click(trigger);
|
|
3490
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
3491
|
-
|
|
3492
|
-
await user.keyboard('{Escape}');
|
|
3493
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
3494
|
-
expect(trigger).toHaveFocus();
|
|
3495
|
-
});
|
|
3496
|
-
|
|
3497
|
-
// jsdom has no light dismiss, so the guard tests above cannot fail for the
|
|
3498
|
-
// collapsed heading. In a browser, the collapsed icon button sits outside the
|
|
3499
|
-
// auto popover, and only the invoker relationship stops the press that
|
|
3500
|
-
// should confirm a hover-open from dismissing it first.
|
|
3501
|
-
it('makes the collapsed trigger the native invoker of its flyout', () => {
|
|
3502
|
-
render(
|
|
3503
|
-
<CollapsedWrapper>
|
|
3504
|
-
<SideNavHeading
|
|
3505
|
-
heading="My App"
|
|
3506
|
-
icon={<span>🏠</span>}
|
|
3507
|
-
menu={menuItems}
|
|
3508
|
-
/>
|
|
3509
|
-
</CollapsedWrapper>,
|
|
3510
|
-
);
|
|
3511
|
-
const trigger = screen.getByRole('button', {name: 'My App'});
|
|
3512
|
-
const panelId = trigger.getAttribute('aria-controls');
|
|
3513
|
-
expect(panelId).toBeTruthy();
|
|
3514
|
-
expect(trigger).toHaveAttribute('popovertarget', panelId);
|
|
3515
|
-
});
|
|
3516
|
-
|
|
3517
|
-
// Expanded, the panel covers the heading, and the trigger is the heading
|
|
3518
|
-
// root itself: an invoker there would default-prevent clicks on its links.
|
|
3519
|
-
it('adds no invoker in the expanded layouts', () => {
|
|
3520
|
-
const {container} = render(
|
|
3521
|
-
<>
|
|
3522
|
-
<SideNavHeading heading="Whole" menu={menuItems} />
|
|
3523
|
-
<SideNavHeading heading="Mixed" headingHref="/home" menu={menuItems} />
|
|
3524
|
-
</>,
|
|
3525
|
-
);
|
|
3526
|
-
expect(container.querySelector('[popovertarget]')).toBeNull();
|
|
3527
|
-
});
|
|
3528
|
-
});
|
|
3529
|
-
|
|
3530
|
-
// =============================================================================
|
|
3531
|
-
// Collapse ownership — collapsible + resizable (#4790, #5073)
|
|
3532
|
-
// =============================================================================
|
|
3533
|
-
|
|
3534
|
-
const AUTO_SAVE_ID = 'sidenav-collapse-owner';
|
|
3535
|
-
const STORAGE_KEY = `astryx-resizable:${AUTO_SAVE_ID}`;
|
|
3536
|
-
|
|
3537
|
-
function expectCollapsed(isCollapsed: boolean) {
|
|
3538
|
-
const label = isCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
|
|
3539
|
-
expect(screen.getByRole('button', {name: label})).toBeInTheDocument();
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
describe('SideNav collapse ownership', () => {
|
|
3543
|
-
beforeEach(() => {
|
|
3544
|
-
localStorage.clear();
|
|
3545
|
-
});
|
|
3546
|
-
|
|
3547
|
-
// Every row is a configuration that works on origin/main today, with the
|
|
3548
|
-
// collapse state main resolves for it. Normalizing the two props into one
|
|
3549
|
-
// owner must not move any of these — the one case whose outcome does change
|
|
3550
|
-
// is the divergence itself, pinned separately below.
|
|
3551
|
-
const preservedCases: {
|
|
3552
|
-
name: string;
|
|
3553
|
-
persisted?: string;
|
|
3554
|
-
element: React.ReactElement;
|
|
3555
|
-
isCollapsed: boolean;
|
|
3556
|
-
}[] = [
|
|
3557
|
-
{
|
|
3558
|
-
name: 'collapsible alone',
|
|
3559
|
-
element: <SideNav collapsible>Content</SideNav>,
|
|
3560
|
-
isCollapsed: false,
|
|
3561
|
-
},
|
|
3562
|
-
{
|
|
3563
|
-
name: 'collapsible with defaultIsCollapsed',
|
|
3564
|
-
element: (
|
|
3565
|
-
<SideNav collapsible={{defaultIsCollapsed: true}}>Content</SideNav>
|
|
3566
|
-
),
|
|
3567
|
-
isCollapsed: true,
|
|
3568
|
-
},
|
|
3569
|
-
{
|
|
3570
|
-
name: 'controlled collapsible (collapsed)',
|
|
3571
|
-
element: (
|
|
3572
|
-
<SideNav collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}>
|
|
3573
|
-
Content
|
|
3574
|
-
</SideNav>
|
|
3575
|
-
),
|
|
3576
|
-
isCollapsed: true,
|
|
3577
|
-
},
|
|
3578
|
-
{
|
|
3579
|
-
name: 'controlled collapsible (expanded)',
|
|
3580
|
-
element: (
|
|
3581
|
-
<SideNav
|
|
3582
|
-
collapsible={{isCollapsed: false, onCollapsedChange: () => {}}}>
|
|
3583
|
-
Content
|
|
3584
|
-
</SideNav>
|
|
3585
|
-
),
|
|
3586
|
-
isCollapsed: false,
|
|
3587
|
-
},
|
|
3588
|
-
{
|
|
3589
|
-
name: 'collapsible + resizable',
|
|
3590
|
-
element: (
|
|
3591
|
-
<SideNav collapsible resizable>
|
|
3592
|
-
Content
|
|
3593
|
-
</SideNav>
|
|
3594
|
-
),
|
|
3595
|
-
isCollapsed: false,
|
|
3596
|
-
},
|
|
3597
|
-
{
|
|
3598
|
-
name: 'defaultIsCollapsed + resizable',
|
|
3599
|
-
element: (
|
|
3600
|
-
<SideNav collapsible={{defaultIsCollapsed: true}} resizable>
|
|
3601
|
-
Content
|
|
3602
|
-
</SideNav>
|
|
3603
|
-
),
|
|
3604
|
-
isCollapsed: true,
|
|
3605
|
-
},
|
|
3606
|
-
{
|
|
3607
|
-
name: 'controlled collapsible + resizable',
|
|
3608
|
-
element: (
|
|
3609
|
-
<SideNav
|
|
3610
|
-
collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}
|
|
3611
|
-
resizable>
|
|
3612
|
-
Content
|
|
3613
|
-
</SideNav>
|
|
3614
|
-
),
|
|
3615
|
-
isCollapsed: true,
|
|
3616
|
-
},
|
|
3617
|
-
{
|
|
3618
|
-
name: 'defaultIsCollapsed + a persisted legacy width',
|
|
3619
|
-
persisted: '300',
|
|
3620
|
-
element: (
|
|
3621
|
-
<SideNav
|
|
3622
|
-
collapsible={{defaultIsCollapsed: true}}
|
|
3623
|
-
resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3624
|
-
Content
|
|
3625
|
-
</SideNav>
|
|
3626
|
-
),
|
|
3627
|
-
isCollapsed: true,
|
|
3628
|
-
},
|
|
3629
|
-
{
|
|
3630
|
-
name: 'collapsible + a persisted legacy width',
|
|
3631
|
-
persisted: '300',
|
|
3632
|
-
element: (
|
|
3633
|
-
<SideNav collapsible resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3634
|
-
Content
|
|
3635
|
-
</SideNav>
|
|
3636
|
-
),
|
|
3637
|
-
isCollapsed: false,
|
|
3638
|
-
},
|
|
3639
|
-
];
|
|
3640
|
-
|
|
3641
|
-
it.each(preservedCases)(
|
|
3642
|
-
'resolves $name the way main does',
|
|
3643
|
-
({persisted, element, isCollapsed}) => {
|
|
3644
|
-
if (persisted != null) {
|
|
3645
|
-
localStorage.setItem(STORAGE_KEY, persisted);
|
|
3646
|
-
}
|
|
3647
|
-
render(element);
|
|
3648
|
-
expectCollapsed(isCollapsed);
|
|
3649
|
-
},
|
|
3650
|
-
);
|
|
3651
|
-
|
|
3652
|
-
it('keeps resizable alone free of collapse', () => {
|
|
3653
|
-
render(<SideNav resizable>Content</SideNav>);
|
|
3654
|
-
expect(
|
|
3655
|
-
screen.queryByRole('button', {name: /sidebar/i}),
|
|
3656
|
-
).not.toBeInTheDocument();
|
|
3657
|
-
expect(screen.getByRole('navigation').style.width).toBe('260px');
|
|
3658
|
-
});
|
|
3659
|
-
|
|
3660
|
-
it('restores the collapsed rail from a legacy persisted 0 (#4790)', () => {
|
|
3661
|
-
// main renders the expanded layout here while the resize hook restores
|
|
3662
|
-
// collapsed, so the nav paints at width 0 with no way back.
|
|
3663
|
-
localStorage.setItem(STORAGE_KEY, '0');
|
|
3664
|
-
render(
|
|
3665
|
-
<SideNav collapsible resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3666
|
-
Content
|
|
3667
|
-
</SideNav>,
|
|
3668
|
-
);
|
|
3669
|
-
|
|
3670
|
-
expectCollapsed(true);
|
|
3671
|
-
expect(screen.getByRole('navigation').style.width).not.toBe('0px');
|
|
3672
|
-
});
|
|
3673
|
-
|
|
3674
|
-
it('restores the collapsed rail and the pre-collapse width across a reload', async () => {
|
|
3675
|
-
const user = userEvent.setup();
|
|
3676
|
-
const nav = (
|
|
3677
|
-
<SideNav
|
|
3678
|
-
collapsible
|
|
3679
|
-
resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 260}}>
|
|
3680
|
-
Content
|
|
3681
|
-
</SideNav>
|
|
3682
|
-
);
|
|
3683
|
-
|
|
3684
|
-
const first = render(nav);
|
|
3685
|
-
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3686
|
-
expectCollapsed(true);
|
|
3687
|
-
first.unmount();
|
|
3688
|
-
|
|
3689
|
-
render(nav);
|
|
3690
|
-
expectCollapsed(true);
|
|
3691
|
-
|
|
3692
|
-
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
3693
|
-
expect(screen.getByRole('navigation').style.width).toBe('260px');
|
|
3694
|
-
});
|
|
3695
|
-
|
|
3696
|
-
it('expands to the persisted pre-collapse width, not the default', async () => {
|
|
3697
|
-
const user = userEvent.setup();
|
|
3698
|
-
localStorage.setItem(
|
|
3699
|
-
STORAGE_KEY,
|
|
3700
|
-
JSON.stringify({size: 320, isCollapsed: true}),
|
|
3701
|
-
);
|
|
3702
|
-
render(
|
|
3703
|
-
<SideNav
|
|
3704
|
-
collapsible
|
|
3705
|
-
resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 260}}>
|
|
3706
|
-
Content
|
|
3707
|
-
</SideNav>,
|
|
3708
|
-
);
|
|
3709
|
-
|
|
3710
|
-
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
3711
|
-
expect(screen.getByRole('navigation').style.width).toBe('320px');
|
|
3712
|
-
});
|
|
3713
|
-
|
|
3714
|
-
it('reports one collapse change per toggle', async () => {
|
|
3715
|
-
const user = userEvent.setup();
|
|
3716
|
-
const onCollapsedChange = vi.fn();
|
|
3717
|
-
render(
|
|
3718
|
-
<SideNav collapsible={{onCollapsedChange}} resizable>
|
|
3719
|
-
Content
|
|
3720
|
-
</SideNav>,
|
|
3721
|
-
);
|
|
3722
|
-
|
|
3723
|
-
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3724
|
-
expect(onCollapsedChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
3725
|
-
});
|
|
3726
|
-
|
|
3727
|
-
it('lets a controlled collapsible drive a resizable nav from outside', async () => {
|
|
3728
|
-
const user = userEvent.setup();
|
|
3729
|
-
|
|
3730
|
-
function Harness() {
|
|
3731
|
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
3732
|
-
const collapsible = {isCollapsed, onCollapsedChange: setIsCollapsed};
|
|
3733
|
-
return (
|
|
3734
|
-
<>
|
|
3735
|
-
<SideNavCollapseButton collapsible={collapsible} label="Outside" />
|
|
3736
|
-
<SideNav
|
|
3737
|
-
collapsible={{...collapsible, hasButton: false}}
|
|
3738
|
-
resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3739
|
-
Content
|
|
3740
|
-
</SideNav>
|
|
3741
|
-
</>
|
|
3742
|
-
);
|
|
3743
|
-
}
|
|
3744
|
-
|
|
3745
|
-
render(<Harness />);
|
|
3746
|
-
expect(screen.getByRole('navigation').style.width).toBe('260px');
|
|
3747
|
-
|
|
3748
|
-
await user.click(screen.getByRole('button', {name: 'Outside'}));
|
|
3749
|
-
expect(screen.getByRole('navigation').style.width).toBe('');
|
|
3750
|
-
expect(
|
|
3751
|
-
screen.queryByTestId('astryx-sidenav-resize-handle'),
|
|
3752
|
-
).not.toBeInTheDocument();
|
|
3753
|
-
});
|
|
3754
|
-
|
|
3755
|
-
it('lets resizable own collapse on its own', async () => {
|
|
3756
|
-
const user = userEvent.setup();
|
|
3757
|
-
const onCollapseChange = vi.fn();
|
|
3758
|
-
render(
|
|
3759
|
-
<SideNav resizable={{defaultIsCollapsed: true, onCollapseChange}}>
|
|
3760
|
-
Content
|
|
3761
|
-
</SideNav>,
|
|
3762
|
-
);
|
|
3763
|
-
|
|
3764
|
-
expectCollapsed(true);
|
|
3765
|
-
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
3766
|
-
expect(onCollapseChange).toHaveBeenCalledExactlyOnceWith(false);
|
|
3767
|
-
});
|
|
3768
|
-
|
|
3769
|
-
it('keeps collapse state when resize is toggled off and on', async () => {
|
|
3770
|
-
const user = userEvent.setup();
|
|
3771
|
-
const {rerender} = render(
|
|
3772
|
-
<SideNav collapsible resizable>
|
|
3773
|
-
Content
|
|
3774
|
-
</SideNav>,
|
|
3775
|
-
);
|
|
3776
|
-
|
|
3777
|
-
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3778
|
-
expectCollapsed(true);
|
|
3779
|
-
|
|
3780
|
-
rerender(<SideNav collapsible>Content</SideNav>);
|
|
3781
|
-
expectCollapsed(true);
|
|
3782
|
-
|
|
3783
|
-
rerender(
|
|
3784
|
-
<SideNav collapsible resizable>
|
|
3785
|
-
Content
|
|
3786
|
-
</SideNav>,
|
|
3787
|
-
);
|
|
3788
|
-
expectCollapsed(true);
|
|
3789
|
-
});
|
|
3790
|
-
|
|
3791
|
-
it('gives resizable the winning value when both set defaultIsCollapsed', () => {
|
|
3792
|
-
render(
|
|
3793
|
-
<SideNav
|
|
3794
|
-
collapsible={{defaultIsCollapsed: false}}
|
|
3795
|
-
resizable={{defaultIsCollapsed: true}}>
|
|
3796
|
-
Content
|
|
3797
|
-
</SideNav>,
|
|
3798
|
-
);
|
|
3799
|
-
expectCollapsed(true);
|
|
3800
|
-
});
|
|
3801
|
-
});
|
|
3802
|
-
|
|
3803
|
-
describe('SideNav collapse conflict warning', () => {
|
|
3804
|
-
let warn: ReturnType<typeof vi.spyOn>;
|
|
3805
|
-
|
|
3806
|
-
beforeEach(() => {
|
|
3807
|
-
localStorage.clear();
|
|
3808
|
-
warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
3809
|
-
});
|
|
3810
|
-
|
|
3811
|
-
afterEach(() => {
|
|
3812
|
-
warn.mockRestore();
|
|
3813
|
-
});
|
|
3814
|
-
|
|
3815
|
-
it('warns when both props carry defaultIsCollapsed', () => {
|
|
3816
|
-
render(
|
|
3817
|
-
<SideNav
|
|
3818
|
-
collapsible={{defaultIsCollapsed: false}}
|
|
3819
|
-
resizable={{defaultIsCollapsed: true}}>
|
|
3820
|
-
Content
|
|
3821
|
-
</SideNav>,
|
|
3822
|
-
);
|
|
3823
|
-
|
|
3824
|
-
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
|
3825
|
-
expect.stringContaining('collapsible.defaultIsCollapsed'),
|
|
3826
|
-
);
|
|
3827
|
-
expect(warn.mock.calls[0][0]).toContain('resizable.defaultIsCollapsed');
|
|
3828
|
-
expect(warn.mock.calls[0][0]).toContain('resizable wins');
|
|
3829
|
-
});
|
|
3830
|
-
|
|
3831
|
-
it('warns when a controlled collapsible does not drive collapse', () => {
|
|
3832
|
-
render(
|
|
3833
|
-
<SideNav
|
|
3834
|
-
collapsible={{isCollapsed: false, onCollapsedChange: () => {}}}
|
|
3835
|
-
resizable={{isCollapsed: true, onCollapseChange: () => {}}}>
|
|
3836
|
-
Content
|
|
3837
|
-
</SideNav>,
|
|
3838
|
-
);
|
|
3839
|
-
|
|
3840
|
-
const message = String(warn.mock.calls[0][0]);
|
|
3841
|
-
expect(message).toContain('collapsible.isCollapsed');
|
|
3842
|
-
expect(message).toContain('collapsible.onCollapsedChange');
|
|
3843
|
-
expect(message).toContain('resizable.isCollapsed');
|
|
3844
|
-
expect(message).toContain('resizable wins');
|
|
3845
|
-
// The winner is the state that actually renders.
|
|
3846
|
-
expectCollapsed(true);
|
|
3847
|
-
});
|
|
3848
|
-
|
|
3849
|
-
it('does not split state and callback ownership across props', async () => {
|
|
3850
|
-
const user = userEvent.setup();
|
|
3851
|
-
const collapsibleChange = vi.fn();
|
|
3852
|
-
const resizableChange = vi.fn();
|
|
3853
|
-
render(
|
|
3854
|
-
<SideNav
|
|
3855
|
-
collapsible={{
|
|
3856
|
-
isCollapsed: true,
|
|
3857
|
-
onCollapsedChange: collapsibleChange,
|
|
3858
|
-
}}
|
|
3859
|
-
resizable={{onCollapseChange: resizableChange}}>
|
|
3860
|
-
Content
|
|
3861
|
-
</SideNav>,
|
|
3862
|
-
);
|
|
3863
|
-
|
|
3864
|
-
expectCollapsed(false);
|
|
3865
|
-
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3866
|
-
expect(resizableChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
3867
|
-
expect(collapsibleChange).not.toHaveBeenCalled();
|
|
3868
|
-
});
|
|
3869
|
-
|
|
3870
|
-
it('stays silent for collapsible alongside a resize config', () => {
|
|
3871
|
-
render(
|
|
3872
|
-
<SideNav
|
|
3873
|
-
collapsible
|
|
3874
|
-
resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 300}}>
|
|
3875
|
-
Content
|
|
3876
|
-
</SideNav>,
|
|
3877
|
-
);
|
|
3878
|
-
expect(warn).not.toHaveBeenCalled();
|
|
3879
|
-
});
|
|
3880
|
-
|
|
3881
|
-
it('stays silent for a controlled collapsible with a plain resize config', () => {
|
|
3882
|
-
render(
|
|
3883
|
-
<SideNav
|
|
3884
|
-
collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}
|
|
3885
|
-
resizable={{defaultWidth: 300}}>
|
|
3886
|
-
Content
|
|
3887
|
-
</SideNav>,
|
|
3888
|
-
);
|
|
3889
|
-
expect(warn).not.toHaveBeenCalled();
|
|
3890
|
-
});
|
|
3891
|
-
});
|
|
3892
|
-
|
|
3893
|
-
// =============================================================================
|
|
3894
|
-
// SideNavItem — collapsed-from-mount rail tooltip
|
|
3895
|
-
// =============================================================================
|
|
3896
|
-
|
|
3897
|
-
// Two interchangeable link components: switching between them remounts the
|
|
3898
|
-
// rendered link element.
|
|
3899
|
-
function RailLinkA({ref, ...props}: React.ComponentPropsWithRef<'a'>) {
|
|
3900
|
-
return <a ref={ref} {...props} />;
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
function RailLinkB({ref, ...props}: React.ComponentPropsWithRef<'a'>) {
|
|
3904
|
-
return <a ref={ref} {...props} />;
|
|
3905
|
-
}
|
|
3906
|
-
|
|
3907
|
-
describe('SideNavItem collapsed-from-mount rail tooltip', () => {
|
|
3908
|
-
// #7435 / #7692: a childless item that mounts inside an already-collapsed
|
|
3909
|
-
// SideNav (the persisted-collapse case) must attach its tooltip on the first
|
|
3910
|
-
// render, and the tooltip must describe the control that takes focus — the
|
|
3911
|
-
// link or button — not the row around it.
|
|
3912
|
-
function railTooltipFor(trigger: HTMLElement) {
|
|
3913
|
-
const id = trigger.getAttribute('aria-describedby');
|
|
3914
|
-
return id == null ? null : document.getElementById(id);
|
|
3915
|
-
}
|
|
3916
|
-
|
|
3917
|
-
function renderRail(item: ReactNode) {
|
|
3918
|
-
return render(
|
|
3919
|
-
<SideNav
|
|
3920
|
-
aria-label="Main"
|
|
3921
|
-
collapsible={{isCollapsed: true, hasButton: false}}>
|
|
3922
|
-
{item}
|
|
3923
|
-
</SideNav>,
|
|
3924
|
-
);
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
|
-
let showPopover: ReturnType<typeof vi.spyOn> | undefined;
|
|
3928
|
-
|
|
3929
|
-
afterEach(() => {
|
|
3930
|
-
showPopover?.mockRestore();
|
|
3931
|
-
showPopover = undefined;
|
|
3932
|
-
vi.useRealTimers();
|
|
3933
|
-
});
|
|
3934
|
-
|
|
3935
|
-
it('describes the rail link by its tooltip when the nav is collapsed at mount', () => {
|
|
3936
|
-
renderRail(
|
|
3937
|
-
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
|
|
3938
|
-
);
|
|
3939
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
3940
|
-
|
|
3941
|
-
const layer = railTooltipFor(link);
|
|
3942
|
-
expect(layer).not.toBeNull();
|
|
3943
|
-
expect(layer).toHaveAttribute('role', 'tooltip');
|
|
3944
|
-
expect(layer).toHaveTextContent('Dashboard');
|
|
3945
|
-
});
|
|
3946
|
-
|
|
3947
|
-
it('shows a disabled item its label when the pointer rests on its row', async () => {
|
|
3948
|
-
// The disabled control is `pointer-events: none`, so in a browser the
|
|
3949
|
-
// pointer lands on the row; the tooltip has to listen there.
|
|
3950
|
-
vi.useFakeTimers();
|
|
3951
|
-
const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
|
|
3952
|
-
showPopover = spy;
|
|
3953
|
-
renderRail(
|
|
3954
|
-
<SideNavItem
|
|
3955
|
-
label="Analytics (Coming Soon)"
|
|
3956
|
-
icon={StubIcon}
|
|
3957
|
-
href="/analytics"
|
|
3958
|
-
isDisabled
|
|
3959
|
-
/>,
|
|
3960
|
-
);
|
|
3961
|
-
// Let the row's tooltip attach after the commit.
|
|
3962
|
-
await act(async () => {});
|
|
3963
|
-
const button = screen.getByRole('button', {
|
|
3964
|
-
name: 'Analytics (Coming Soon)',
|
|
3965
|
-
});
|
|
3966
|
-
const row = button.parentElement as HTMLElement;
|
|
3967
|
-
const layer = screen.getByRole('tooltip', {hidden: true});
|
|
3968
|
-
expect(layer).toHaveTextContent('Analytics (Coming Soon)');
|
|
3969
|
-
|
|
3970
|
-
fireEvent.mouseEnter(row);
|
|
3971
|
-
act(() => {
|
|
3972
|
-
vi.advanceTimersByTime(200);
|
|
3973
|
-
});
|
|
3974
|
-
expect(spy.mock.contexts).toContain(layer);
|
|
3975
|
-
});
|
|
3976
|
-
|
|
3977
|
-
it('describes the link once a disabled item is enabled in the rail', () => {
|
|
3978
|
-
const item = (isDisabled: boolean) => (
|
|
3979
|
-
<SideNavItem
|
|
3980
|
-
label="Dashboard"
|
|
3981
|
-
icon={StubIcon}
|
|
3982
|
-
href="/dashboard"
|
|
3983
|
-
isDisabled={isDisabled}
|
|
3984
|
-
/>
|
|
3985
|
-
);
|
|
3986
|
-
const {rerender} = renderRail(item(true));
|
|
3987
|
-
rerender(
|
|
3988
|
-
<SideNav
|
|
3989
|
-
aria-label="Main"
|
|
3990
|
-
collapsible={{isCollapsed: true, hasButton: false}}>
|
|
3991
|
-
{item(false)}
|
|
3992
|
-
</SideNav>,
|
|
3993
|
-
);
|
|
3994
|
-
|
|
3995
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
3996
|
-
expect(railTooltipFor(link)).toHaveTextContent('Dashboard');
|
|
3997
|
-
});
|
|
3998
|
-
|
|
3999
|
-
it('describes the new control when a rail item gains an href', () => {
|
|
4000
|
-
const item = (href?: string) => (
|
|
4001
|
-
<SideNavItem
|
|
4002
|
-
label="Dashboard"
|
|
4003
|
-
icon={StubIcon}
|
|
4004
|
-
href={href}
|
|
4005
|
-
onClick={() => {}}
|
|
4006
|
-
/>
|
|
4007
|
-
);
|
|
4008
|
-
const {rerender} = renderRail(item());
|
|
4009
|
-
const button = screen.getByRole('button', {name: 'Dashboard'});
|
|
4010
|
-
expect(railTooltipFor(button)).toHaveTextContent('Dashboard');
|
|
4011
|
-
|
|
4012
|
-
rerender(
|
|
4013
|
-
<SideNav
|
|
4014
|
-
aria-label="Main"
|
|
4015
|
-
collapsible={{isCollapsed: true, hasButton: false}}>
|
|
4016
|
-
{item('/dashboard')}
|
|
4017
|
-
</SideNav>,
|
|
4018
|
-
);
|
|
4019
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
4020
|
-
expect(railTooltipFor(link)).toHaveTextContent('Dashboard');
|
|
4021
|
-
});
|
|
4022
|
-
|
|
4023
|
-
it('binds the replacement when a rail link element is replaced', () => {
|
|
4024
|
-
// Switching the link component remounts the element; the tooltip must
|
|
4025
|
-
// follow the new element.
|
|
4026
|
-
vi.useFakeTimers();
|
|
4027
|
-
const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
|
|
4028
|
-
showPopover = spy;
|
|
4029
|
-
const item = (as: typeof RailLinkA) => (
|
|
4030
|
-
<SideNavItem
|
|
4031
|
-
label="Dashboard"
|
|
4032
|
-
icon={StubIcon}
|
|
4033
|
-
href="/dashboard"
|
|
4034
|
-
as={as}
|
|
4035
|
-
/>
|
|
4036
|
-
);
|
|
4037
|
-
const {rerender} = renderRail(item(RailLinkA));
|
|
4038
|
-
const first = screen.getByRole('link', {name: 'Dashboard'});
|
|
4039
|
-
rerender(
|
|
4040
|
-
<SideNav
|
|
4041
|
-
aria-label="Main"
|
|
4042
|
-
collapsible={{isCollapsed: true, hasButton: false}}>
|
|
4043
|
-
{item(RailLinkB)}
|
|
4044
|
-
</SideNav>,
|
|
4045
|
-
);
|
|
4046
|
-
|
|
4047
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
4048
|
-
expect(link).not.toBe(first);
|
|
4049
|
-
const layer = railTooltipFor(link);
|
|
4050
|
-
expect(layer).toHaveTextContent('Dashboard');
|
|
4051
|
-
fireEvent.mouseEnter(link);
|
|
4052
|
-
act(() => {
|
|
4053
|
-
vi.advanceTimersByTime(200);
|
|
4054
|
-
});
|
|
4055
|
-
expect(spy.mock.contexts).toContain(layer);
|
|
4056
|
-
});
|
|
4057
|
-
|
|
4058
|
-
it("keeps the tooltip beside a caller's changing aria-describedby", () => {
|
|
4059
|
-
const item = (hint: string) => (
|
|
4060
|
-
<SideNavItem
|
|
4061
|
-
label="Dashboard"
|
|
4062
|
-
icon={StubIcon}
|
|
4063
|
-
href="/dashboard"
|
|
4064
|
-
aria-describedby={hint}
|
|
4065
|
-
/>
|
|
4066
|
-
);
|
|
4067
|
-
const {rerender} = renderRail(item('hint-a'));
|
|
4068
|
-
rerender(
|
|
4069
|
-
<SideNav
|
|
4070
|
-
aria-label="Main"
|
|
4071
|
-
collapsible={{isCollapsed: true, hasButton: false}}>
|
|
4072
|
-
{item('hint-b')}
|
|
4073
|
-
</SideNav>,
|
|
4074
|
-
);
|
|
4075
|
-
|
|
4076
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
4077
|
-
const tooltip = screen.getByRole('tooltip', {hidden: true});
|
|
4078
|
-
const ids = (link.getAttribute('aria-describedby') ?? '').split(' ');
|
|
4079
|
-
expect(ids).toContain('hint-b');
|
|
4080
|
-
expect(ids).toContain(tooltip.id);
|
|
4081
|
-
expect(ids).not.toContain('hint-a');
|
|
4082
|
-
});
|
|
4083
|
-
|
|
4084
|
-
it('shows the label when keyboard focus reaches a rail link', () => {
|
|
4085
|
-
const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
|
|
4086
|
-
showPopover = spy;
|
|
4087
|
-
renderRail(
|
|
4088
|
-
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
|
|
4089
|
-
);
|
|
4090
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
4091
|
-
const layer = railTooltipFor(link);
|
|
4092
|
-
// jsdom does not model `:focus-visible`; stand in for a keyboard focus.
|
|
4093
|
-
const realMatches = link.matches.bind(link);
|
|
4094
|
-
vi.spyOn(link, 'matches').mockImplementation((selector: string) =>
|
|
4095
|
-
selector === ':focus-visible' ? true : realMatches(selector),
|
|
4096
|
-
);
|
|
4097
|
-
|
|
4098
|
-
fireEvent.focusIn(link);
|
|
4099
|
-
expect(spy.mock.contexts).toContain(layer);
|
|
4100
|
-
});
|
|
4101
|
-
|
|
4102
|
-
it('keeps a tooltip that was open when the rail expanded shut on the next collapse', () => {
|
|
4103
|
-
vi.useFakeTimers();
|
|
4104
|
-
const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
|
|
4105
|
-
showPopover = spy;
|
|
4106
|
-
const rail = (isCollapsed: boolean) => (
|
|
4107
|
-
<SideNav aria-label="Main" collapsible={{isCollapsed, hasButton: false}}>
|
|
4108
|
-
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />
|
|
4109
|
-
</SideNav>
|
|
4110
|
-
);
|
|
4111
|
-
const {rerender} = render(rail(true));
|
|
4112
|
-
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
4113
|
-
fireEvent.mouseEnter(link);
|
|
4114
|
-
act(() => {
|
|
4115
|
-
vi.advanceTimersByTime(200);
|
|
4116
|
-
});
|
|
4117
|
-
expect(spy.mock.contexts).toContain(railTooltipFor(link));
|
|
4118
|
-
|
|
4119
|
-
// Expanding (from a shortcut, say) removes the trigger without a leave.
|
|
4120
|
-
rerender(rail(false));
|
|
4121
|
-
spy.mockClear();
|
|
4122
|
-
rerender(rail(true));
|
|
4123
|
-
act(() => {
|
|
4124
|
-
vi.advanceTimersByTime(500);
|
|
4125
|
-
});
|
|
4126
|
-
expect(spy).not.toHaveBeenCalled();
|
|
4127
|
-
});
|
|
4128
|
-
});
|