@astryxdesign/core 0.5.0 → 0.5.1-canary.046f6ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/dist/AlertDialog/AlertDialog.d.ts +1 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +45 -20
- package/dist/AspectRatio/AspectRatio.d.ts +6 -6
- package/dist/Avatar/Avatar.d.ts +25 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +43 -9
- package/dist/Avatar/index.d.ts +2 -2
- package/dist/Avatar/index.d.ts.map +1 -1
- package/dist/Avatar/index.js +1 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts +9 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +5 -2
- package/dist/AvatarGroup/AvatarGroupContext.d.ts +2 -1
- package/dist/AvatarGroup/AvatarGroupContext.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +11 -7
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +26 -2
- package/dist/Banner/Banner.d.ts +10 -1
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +11 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +8 -7
- package/dist/Calendar/Calendar.d.ts +4 -2
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +15 -21
- package/dist/Calendar/hooks/useCalendarDays.js +4 -6
- package/dist/Calendar/hooks/useCalendarNavigation.js +4 -6
- package/dist/Calendar/styles.d.ts +5 -20
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +15 -17
- package/dist/Card/Card.d.ts +12 -12
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +17 -9
- package/dist/Card/index.d.ts +35 -1
- package/dist/Card/index.d.ts.map +1 -1
- package/dist/Card/index.js +21 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +108 -7
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +7 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +15 -4
- package/dist/CodeBlock/highlightRanges.js +1 -1
- package/dist/CodeBlock/highlightStyles.js +13 -13
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +2 -7
- package/dist/ComplexSelector/ComplexSelector.d.ts +10 -2
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +14 -7
- package/dist/DateInput/DateInput.d.ts +43 -0
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +22 -5
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +5 -3
- package/dist/DateInput/MonthYearWheels.d.ts.map +1 -1
- package/dist/DateInput/MonthYearWheels.js +6 -4
- package/dist/DateInput/NativeDateField.d.ts +10 -0
- package/dist/DateInput/NativeDateField.d.ts.map +1 -0
- package/dist/DateInput/NativeDateField.js +494 -0
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +52 -25
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +1 -0
- package/dist/DateInput/index.d.ts +1 -1
- package/dist/DateInput/index.d.ts.map +1 -1
- package/dist/DateInput/nativeDateSegments.d.ts +22 -0
- package/dist/DateInput/nativeDateSegments.d.ts.map +1 -0
- package/dist/DateInput/nativeDateSegments.js +111 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts +4 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +3 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts +23 -24
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +35 -10
- package/dist/DateTimeInput/TouchDateTimeField.d.ts +6 -0
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -0
- package/dist/DateTimeInput/TouchDateTimeField.js +1021 -0
- package/dist/Dialog/Dialog.d.ts +16 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +28 -7
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +4 -13
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +9 -1
- package/dist/Field/InputClearButton.d.ts +9 -3
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +14 -3
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +0 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +104 -3
- package/dist/HoverCard/useHoverCard.d.ts +14 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +7 -1
- package/dist/Icon/Icon.d.ts +4 -2
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/defaultIcons.d.ts +3 -2
- package/dist/Icon/defaultIcons.d.ts.map +1 -1
- package/dist/Icon/defaultIcons.js +7 -0
- package/dist/Icon/globalIconRegistry.d.ts +13 -0
- package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
- package/dist/Icon/globalIconRegistry.js +34 -6
- package/dist/Icon/index.d.ts +1 -1
- package/dist/Icon/index.d.ts.map +1 -1
- package/dist/Icon/useIcon.d.ts +5 -2
- package/dist/Icon/useIcon.d.ts.map +1 -1
- package/dist/Icon/useIcon.js +3 -0
- package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckboxIndicator.js +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +2 -2
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +2 -26
- package/dist/Layer/gestureCounter.d.ts +8 -0
- package/dist/Layer/gestureCounter.d.ts.map +1 -0
- package/dist/Layer/gestureCounter.js +53 -0
- package/dist/Layer/useLayer.d.ts +23 -13
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +146 -24
- package/dist/List/List.d.ts +1 -1
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +15 -4
- package/dist/Markdown/Markdown.d.ts +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +20 -7
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +5 -3
- package/dist/MetadataList/MetadataListItem.d.ts +1 -1
- package/dist/MetadataList/MetadataListItem.d.ts.map +1 -1
- package/dist/MetadataList/MetadataListItem.js +4 -1
- package/dist/MultiSelector/MultiSelector.d.ts +18 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +58 -11
- package/dist/MultiSelector/hooks.d.ts +7 -1
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +3 -2
- package/dist/NavIcon/NavIcon.d.ts.map +1 -1
- package/dist/NavIcon/NavIcon.js +5 -1
- package/dist/NavItem/navItemStyles.stylex.d.ts +0 -8
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +0 -2
- package/dist/NavMenu/NavHeadingMenu.d.ts +1 -1
- package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.js +7 -4
- package/dist/NumberInput/NumberInput.d.ts +6 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +95 -126
- package/dist/NumberInput/numberInputCommit.d.ts +34 -0
- package/dist/NumberInput/numberInputCommit.d.ts.map +1 -0
- package/dist/NumberInput/numberInputCommit.js +71 -0
- package/dist/NumberInput/numberParser.d.ts +64 -0
- package/dist/NumberInput/numberParser.d.ts.map +1 -0
- package/dist/NumberInput/numberParser.js +350 -0
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +2 -3
- package/dist/OverflowList/OverflowList.d.ts +41 -1
- package/dist/OverflowList/OverflowList.d.ts.map +1 -1
- package/dist/OverflowList/OverflowList.js +43 -2
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -14
- package/dist/Popover/usePopover.d.ts +15 -41
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +26 -7
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +91 -18
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +4 -2
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +18 -5
- package/dist/Resizable/useResizable.d.ts +19 -1
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +104 -38
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +9 -0
- package/dist/Selector/Selector.d.ts +18 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +92 -21
- package/dist/Selector/hooks.d.ts +7 -1
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +3 -2
- package/dist/SideNav/SideNav.d.ts +6 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +57 -27
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +5 -4
- package/dist/SideNav/SideNavItem.d.ts +29 -4
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +75 -20
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +34 -2
- package/dist/Spinner/Spinner.d.ts +3 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +199 -20
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +6 -1
- package/dist/Stepper/Step.d.ts.map +1 -1
- package/dist/Stepper/Step.js +7 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +6 -1
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +3 -6
- package/dist/Table/plugins/filtering/useTableFiltering.d.ts.map +1 -1
- package/dist/Table/plugins/filtering/useTableFiltering.js +73 -4
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +26 -0
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +34 -3
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +25 -1
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +3 -4
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +0 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +13 -8
- package/dist/Timestamp/Timestamp.d.ts +1 -1
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +10 -18
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +10 -5
- package/dist/Toast/Toast.d.ts +17 -2
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +77 -11
- package/dist/Toast/ToastViewport.d.ts +1 -0
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +76 -21
- package/dist/Toast/index.d.ts +1 -1
- package/dist/Toast/index.d.ts.map +1 -1
- package/dist/Toast/types.d.ts +38 -0
- package/dist/Toast/types.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.d.ts +26 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -0
- package/dist/Toast/useToastGesture.js +223 -0
- package/dist/Token/Token.d.ts +1 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +23 -12
- package/dist/Token/TokenLink.d.ts +1 -1
- package/dist/Token/TokenLink.d.ts.map +1 -1
- package/dist/Token/TokenLink.js +5 -2
- package/dist/Tokenizer/Tokenizer.d.ts +12 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +31 -21
- package/dist/TopNav/TopNavHeading.js +2 -2
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +3 -3
- package/dist/TopNav/TopNavMegaMenu.d.ts +1 -1
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +30 -9
- package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +3 -3
- package/dist/TopNav/TopNavMenu.d.ts +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +21 -6
- package/dist/TreeList/TreeList.d.ts +1 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +7 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +2 -2
- package/dist/Typeahead/BaseTypeahead.d.ts +33 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +51 -8
- package/dist/Typeahead/Typeahead.d.ts +8 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +2 -0
- package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/Typeahead/TypeaheadItem.js +7 -2
- package/dist/astryx.css +87 -22
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +3 -25
- package/dist/hooks/useOverflow.d.ts.map +1 -1
- package/dist/hooks/useOverflow.js +14 -4
- package/dist/i18n/InternationalizationContext.d.ts +2 -0
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +3 -1
- package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
- package/dist/i18n/InternationalizationProvider.js +3 -1
- package/dist/i18n/resolve.d.ts +1 -1
- package/dist/i18n/resolve.d.ts.map +1 -1
- package/dist/i18n/resolve.js +37 -38
- package/dist/i18n/useTranslator.d.ts.map +1 -1
- package/dist/i18n/useTranslator.js +2 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/theme/Theme.d.ts.map +1 -1
- package/dist/theme/Theme.js +46 -10
- package/dist/theme/defineTheme.d.ts +9 -3
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +5 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +24 -4
- package/dist/theme/generateThemeRules.d.ts +21 -0
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +26 -0
- package/dist/theme/syntax/tokens.d.ts +1 -1
- package/dist/theme/syntax/tokens.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.js +5 -2
- package/dist/theme/useTheme.d.ts.map +1 -1
- package/dist/theme/useTheme.js +6 -7
- package/dist/utils/dateParser.d.ts +5 -8
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +10 -11
- package/dist/utils/focusOutline.stylex.d.ts +25 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +15 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +26 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -0
- package/dist/utils/interactionOverlay.stylex.js +42 -0
- package/dist/utils/isApplePlatform.d.ts +18 -0
- package/dist/utils/isApplePlatform.d.ts.map +1 -0
- package/dist/utils/isApplePlatform.js +37 -0
- package/dist/utils/plainDate.d.ts +11 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +20 -4
- package/locales/af-ZA.json +7 -7
- package/locales/ar-SA.json +7 -7
- package/locales/ca-ES.json +6 -6
- package/locales/cs-CZ.json +7 -7
- package/locales/da-DK.json +7 -7
- package/locales/de-DE.json +7 -7
- package/locales/el-GR.json +7 -7
- package/locales/en.json +178 -6
- package/locales/es-ES.json +6 -6
- package/locales/fi-FI.json +7 -7
- package/locales/fr-FR.json +7 -7
- package/locales/he-IL.json +7 -7
- package/locales/hu-HU.json +7 -7
- package/locales/it-IT.json +7 -7
- package/locales/ja-JP.json +7 -7
- package/locales/ko-KR.json +7 -7
- package/locales/nl-NL.json +7 -7
- package/locales/no-NO.json +7 -7
- package/locales/pl-PL.json +7 -7
- package/locales/pseudo.json +130 -1
- package/locales/pt-BR.json +7 -7
- package/locales/pt-PT.json +7 -7
- package/locales/ro-RO.json +7 -7
- package/locales/ru-RU.json +7 -7
- package/locales/sr-SP.json +7 -7
- package/locales/sv-SE.json +7 -7
- package/locales/tr-TR.json +7 -7
- package/locales/uk-UA.json +7 -7
- package/locales/vi-VN.json +7 -7
- package/locales/zh-CN.json +7 -7
- package/locales/zh-TW.json +7 -7
- package/package.json +4 -5
- package/src/AlertDialog/AlertDialog.doc.mjs +114 -30
- package/src/AlertDialog/AlertDialog.test.tsx +211 -2
- package/src/AlertDialog/AlertDialog.tsx +62 -20
- package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
- package/src/AspectRatio/AspectRatio.tsx +6 -6
- package/src/Avatar/Avatar.doc.mjs +12 -2
- package/src/Avatar/Avatar.test.tsx +80 -0
- package/src/Avatar/Avatar.tsx +64 -14
- package/src/Avatar/index.ts +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +8 -2
- package/src/AvatarGroup/AvatarGroup.test.tsx +24 -0
- package/src/AvatarGroup/AvatarGroup.tsx +12 -4
- package/src/AvatarGroup/AvatarGroupContext.ts +5 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +12 -12
- package/src/Badge/Badge.doc.mjs +3 -2
- package/src/Badge/Badge.test.tsx +32 -3
- package/src/Badge/Badge.tsx +42 -1
- package/src/Banner/Banner.doc.mjs +9 -0
- package/src/Banner/Banner.test.tsx +76 -11
- package/src/Banner/Banner.tsx +21 -2
- package/src/Button/Button.doc.mjs +2 -1
- package/src/Button/Button.test.tsx +19 -4
- package/src/Button/Button.tsx +9 -29
- package/src/ButtonGroup/ButtonGroup.doc.mjs +2 -2
- package/src/ButtonGroup/ButtonGroup.test.tsx +13 -0
- package/src/Calendar/Calendar.doc.mjs +4 -4
- package/src/Calendar/Calendar.test.tsx +112 -0
- package/src/Calendar/Calendar.tsx +18 -16
- package/src/Calendar/hooks/useCalendarDays.ts +4 -4
- package/src/Calendar/hooks/useCalendarNavigation.ts +4 -4
- package/src/Calendar/styles.ts +48 -42
- package/src/Card/Card.doc.mjs +10 -7
- package/src/Card/Card.test.tsx +113 -19
- package/src/Card/Card.tsx +67 -89
- package/src/Card/index.ts +36 -1
- package/src/Carousel/Carousel.doc.mjs +3 -0
- package/src/Carousel/Carousel.test.tsx +360 -0
- package/src/Carousel/Carousel.tsx +116 -15
- package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +15 -0
- package/src/CheckboxInput/CheckboxInput.tsx +5 -0
- package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
- package/src/CodeBlock/CodeBlock.doc.mjs +10 -4
- package/src/CodeBlock/CodeBlock.test.tsx +37 -0
- package/src/CodeBlock/CodeBlock.tsx +20 -4
- package/src/CodeBlock/highlightRanges.ts +1 -1
- package/src/CodeBlock/highlightStyles.ts +13 -13
- package/src/CommandPalette/CommandPaletteItem.tsx +2 -12
- package/src/ComplexSelector/ComplexSelector.doc.mjs +9 -2
- package/src/ComplexSelector/ComplexSelector.test.tsx +183 -0
- package/src/ComplexSelector/ComplexSelector.tsx +22 -12
- package/src/DateInput/DateInput.doc.mjs +17 -1
- package/src/DateInput/DateInput.test.tsx +9 -4
- package/src/DateInput/DateInput.tsx +54 -3
- package/src/DateInput/DateInputTouch.test.tsx +146 -32
- package/src/DateInput/MonthScroller.tsx +5 -1
- package/src/DateInput/MonthYearWheels.tsx +7 -4
- package/src/DateInput/NativeDateField.test.tsx +635 -0
- package/src/DateInput/NativeDateField.tsx +641 -0
- package/src/DateInput/TouchDateField.tsx +83 -33
- package/src/DateInput/Wheel.tsx +1 -0
- package/src/DateInput/index.ts +1 -0
- package/src/DateInput/nativeDateSegments.ts +127 -0
- package/src/DateRangeInput/DateRangeInput.doc.mjs +3 -3
- package/src/DateRangeInput/DateRangeInput.test.tsx +46 -0
- package/src/DateRangeInput/DateRangeInput.tsx +8 -13
- package/src/DateTimeInput/DateTimeInput.doc.mjs +39 -22
- package/src/DateTimeInput/DateTimeInput.test.tsx +76 -0
- package/src/DateTimeInput/DateTimeInput.tsx +54 -20
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +980 -0
- package/src/DateTimeInput/TouchDateTimeField.tsx +1437 -0
- package/src/Dialog/Dialog.test.tsx +185 -18
- package/src/Dialog/Dialog.tsx +35 -11
- package/src/DropdownMenu/DropdownMenu.test.tsx +44 -10
- package/src/DropdownMenu/DropdownMenu.tsx +4 -13
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.test.tsx +29 -0
- package/src/Field/FieldLabel.tsx +7 -0
- package/src/Field/InputClearButton.test.tsx +15 -1
- package/src/Field/InputClearButton.tsx +28 -4
- package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/Heading/Heading.tsx +0 -1
- package/src/HoverCard/HoverCard.doc.mjs +8 -2
- package/src/HoverCard/HoverCard.test.tsx +154 -4
- package/src/HoverCard/HoverCard.tsx +137 -3
- package/src/HoverCard/useHoverCard.tsx +23 -3
- package/src/Icon/Icon.test.tsx +55 -1
- package/src/Icon/Icon.tsx +5 -3
- package/src/Icon/defaultIcons.tsx +11 -2
- package/src/Icon/globalIconRegistry.test.tsx +16 -3
- package/src/Icon/globalIconRegistry.tsx +37 -6
- package/src/Icon/index.ts +1 -0
- package/src/Icon/useIcon.ts +9 -2
- package/src/Indicator/CheckboxIndicator.tsx +14 -8
- package/src/Indicator/Indicator.doc.mjs +3 -1
- package/src/InputGroup/InputGroup.doc.mjs +1 -1
- package/src/Item/Item.tsx +2 -7
- package/src/Kbd/Kbd.test.tsx +0 -17
- package/src/Kbd/Kbd.tsx +2 -26
- package/src/Layer/gestureCounter.ts +57 -0
- package/src/Layer/useLayer.test.tsx +368 -16
- package/src/Layer/useLayer.tsx +208 -35
- package/src/Layout/__tests__/edgeCompensation.test.tsx +1 -1
- package/src/List/List.test.tsx +38 -0
- package/src/List/List.tsx +8 -1
- package/src/Markdown/Markdown.test.tsx +37 -0
- package/src/Markdown/Markdown.tsx +16 -5
- package/src/Markdown/parser.test.ts +33 -0
- package/src/Markdown/parser.ts +9 -3
- package/src/MetadataList/MetadataList.test.tsx +75 -2
- package/src/MetadataList/MetadataListItem.tsx +5 -2
- package/src/MultiSelector/MultiSelector.doc.mjs +23 -2
- package/src/MultiSelector/MultiSelector.test.tsx +205 -0
- package/src/MultiSelector/MultiSelector.tsx +103 -16
- package/src/MultiSelector/hooks.ts +9 -2
- package/src/NavIcon/NavIcon.doc.mjs +8 -2
- package/src/NavIcon/NavIcon.test.tsx +9 -0
- package/src/NavIcon/NavIcon.tsx +5 -1
- package/src/NavItem/navItemStyles.stylex.ts +0 -8
- package/src/NavMenu/NavHeadingMenu.test.tsx +30 -0
- package/src/NavMenu/NavHeadingMenu.tsx +7 -4
- package/src/NumberInput/NumberInput.doc.mjs +27 -8
- package/src/NumberInput/NumberInput.test.tsx +563 -8
- package/src/NumberInput/NumberInput.tsx +77 -131
- package/src/NumberInput/numberInputCommit.test.ts +72 -0
- package/src/NumberInput/numberInputCommit.ts +103 -0
- package/src/NumberInput/numberParser.docblock.test.ts +234 -0
- package/src/NumberInput/numberParser.test.ts +222 -0
- package/src/NumberInput/numberParser.ts +403 -0
- package/src/Outline/Outline.tsx +2 -4
- package/src/OverflowList/OverflowList.doc.mjs +102 -21
- package/src/OverflowList/OverflowList.test.tsx +550 -4
- package/src/OverflowList/OverflowList.tsx +100 -2
- package/src/Pagination/Pagination.test.tsx +3 -2
- package/src/Popover/Popover.doc.mjs +2 -2
- package/src/Popover/Popover.test.tsx +38 -0
- package/src/Popover/Popover.tsx +28 -66
- package/src/Popover/usePopover.tsx +44 -5
- package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +87 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +64 -0
- package/src/PowerSearch/PowerSearchValueEditor.tsx +109 -10
- package/src/PowerSearch/formatFilterValue.test.ts +16 -1
- package/src/PowerSearch/formatFilterValue.ts +2 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +42 -6
- package/src/ProgressBar/ProgressBar.test.tsx +24 -0
- package/src/ProgressBar/ProgressBar.tsx +28 -8
- package/src/RadioList/RadioList.doc.mjs +1 -1
- package/src/Resizable/Resizable.doc.mjs +2 -2
- package/src/Resizable/useResizable.doc.mjs +21 -2
- package/src/Resizable/useResizable.test.ts +421 -0
- package/src/Resizable/useResizable.ts +133 -48
- package/src/SelectableCard/SelectableCard.doc.mjs +3 -0
- package/src/SelectableCard/SelectableCard.tsx +16 -0
- package/src/Selector/Selector.doc.mjs +15 -1
- package/src/Selector/Selector.source-build.test.mjs +65 -0
- package/src/Selector/Selector.test.tsx +301 -0
- package/src/Selector/Selector.tsx +145 -32
- package/src/Selector/hooks.ts +17 -2
- package/src/SideNav/SideNav.doc.mjs +2 -2
- package/src/SideNav/SideNav.test.tsx +1069 -6
- package/src/SideNav/SideNav.tsx +109 -38
- package/src/SideNav/SideNavHeading.tsx +5 -2
- package/src/SideNav/SideNavItem.doc.mjs +24 -3
- package/src/SideNav/SideNavItem.tsx +134 -44
- package/src/Slider/Slider.doc.mjs +2 -2
- package/src/Slider/Slider.test.tsx +106 -1
- package/src/Slider/Slider.tsx +52 -1
- package/src/Spinner/Spinner.doc.mjs +17 -5
- package/src/Spinner/Spinner.test.tsx +164 -0
- package/src/Spinner/Spinner.tsx +252 -22
- package/src/StatusDot/StatusDot.doc.mjs +8 -2
- package/src/StatusDot/StatusDot.test.tsx +9 -0
- package/src/StatusDot/StatusDot.tsx +7 -1
- package/src/Stepper/Step.doc.mjs +1 -1
- package/src/Stepper/Step.tsx +7 -14
- package/src/Switch/Switch.doc.mjs +3 -1
- package/src/Switch/Switch.test.tsx +10 -0
- package/src/Switch/Switch.tsx +3 -0
- package/src/Table/BaseTable.tsx +6 -1
- package/src/Table/Table.doc.mjs +5 -3
- package/src/Table/Table.tsx +3 -8
- package/src/Table/TableBody.doc.mjs +2 -2
- package/src/Table/TableFooter.doc.mjs +1 -1
- package/src/Table/TableHeader.doc.mjs +1 -1
- package/src/Table/plugins/filtering/useTableFiltering.test.tsx +44 -6
- package/src/Table/plugins/filtering/useTableFiltering.tsx +100 -4
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +50 -4
- package/src/Table/plugins/selection/useTableSelection.test.tsx +62 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +25 -2
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +1 -2
- package/src/Table/useTableGroupedRows.doc.mjs +3 -3
- package/src/Table/useTablePagination.doc.mjs +1 -1
- package/src/Table/useTableSelection.doc.mjs +1 -1
- package/src/Text/Text.test.tsx +44 -0
- package/src/Text/Text.tsx +0 -1
- package/src/TextArea/TextArea.doc.mjs +13 -3
- package/src/TextArea/TextArea.test.tsx +11 -0
- package/src/TextArea/TextArea.tsx +34 -22
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +89 -0
- package/src/Timestamp/Timestamp.tsx +8 -8
- package/src/Timestamp/formatInstant.ts +5 -0
- package/src/Toast/Toast.doc.mjs +213 -44
- package/src/Toast/Toast.tsx +118 -23
- package/src/Toast/ToastViewport.test.tsx +1434 -8
- package/src/Toast/ToastViewport.tsx +85 -14
- package/src/Toast/index.ts +2 -0
- package/src/Toast/types.ts +43 -5
- package/src/Toast/useToastGesture.ts +361 -0
- package/src/Token/Token.test.tsx +95 -0
- package/src/Token/Token.tsx +19 -17
- package/src/Token/TokenLink.tsx +12 -3
- package/src/Tokenizer/Tokenizer.doc.mjs +17 -4
- package/src/Tokenizer/Tokenizer.test.tsx +161 -0
- package/src/Tokenizer/Tokenizer.tsx +49 -24
- package/src/TopNav/TopNavHeading.tsx +2 -2
- package/src/TopNav/TopNavItem.tsx +3 -7
- package/src/TopNav/TopNavMegaMenu.test.tsx +104 -0
- package/src/TopNav/TopNavMegaMenu.tsx +47 -4
- package/src/TopNav/TopNavMegaMenuItem.tsx +10 -9
- package/src/TopNav/TopNavMenu.test.tsx +133 -1
- package/src/TopNav/TopNavMenu.tsx +38 -5
- package/src/TreeList/TreeList.test.tsx +49 -0
- package/src/TreeList/TreeList.tsx +7 -2
- package/src/TreeList/TreeListItem.tsx +2 -7
- package/src/Typeahead/BaseTypeahead.doc.mjs +13 -0
- package/src/Typeahead/BaseTypeahead.tsx +96 -6
- package/src/Typeahead/Typeahead.doc.mjs +7 -1
- package/src/Typeahead/Typeahead.test.tsx +154 -0
- package/src/Typeahead/Typeahead.tsx +9 -0
- package/src/Typeahead/TypeaheadItem.test.tsx +43 -0
- package/src/Typeahead/TypeaheadItem.tsx +8 -1
- package/src/hooks/useAnnounce.doc.mjs +96 -0
- package/src/hooks/useDevWarning.doc.mjs +96 -0
- package/src/hooks/useHotkeys.test.ts +0 -18
- package/src/hooks/useHotkeys.ts +3 -25
- package/src/hooks/useIndicatorFocusRing.doc.mjs +104 -0
- package/src/hooks/useInputStatusIcon.doc.mjs +142 -0
- package/src/hooks/useInteractiveRole.doc.mjs +119 -0
- package/src/hooks/useLongPress.doc.mjs +123 -0
- package/src/hooks/useOverflow.doc.mjs +58 -17
- package/src/hooks/useOverflow.ts +16 -4
- package/src/hooks/useTypeahead.doc.mjs +153 -0
- package/src/i18n/InternationalizationContext.ts +3 -0
- package/src/i18n/InternationalizationProvider.tsx +8 -1
- package/src/i18n/__tests__/resolve.test.ts +23 -1
- package/src/i18n/resolve.ts +46 -49
- package/src/i18n/useTranslator.ts +2 -7
- package/src/index.ts +2 -0
- package/src/raw-import.d.ts +14 -0
- package/src/theme/Theme.test.tsx +104 -2
- package/src/theme/Theme.tsx +49 -14
- package/src/theme/defineTheme.test.ts +23 -0
- package/src/theme/defineTheme.ts +12 -3
- package/src/theme/derivedVarRegistry.test.ts +87 -3
- package/src/theme/derivedVarRegistry.ts +23 -4
- package/src/theme/generateThemeRules.test.ts +99 -1
- package/src/theme/generateThemeRules.ts +28 -0
- package/src/theme/syntax/tokens.ts +5 -2
- package/src/theme/useTheme.test.tsx +10 -0
- package/src/theme/useTheme.ts +16 -10
- package/src/utils/dateParser.test.ts +34 -2
- package/src/utils/dateParser.ts +11 -14
- package/src/utils/focusOutline.stylex.ts +50 -0
- package/src/utils/interactionOverlay.stylex.ts +74 -0
- package/src/utils/isApplePlatform.test.ts +61 -0
- package/src/utils/isApplePlatform.ts +37 -0
- package/src/utils/plainDate.test.ts +46 -0
- package/src/utils/plainDate.ts +18 -3
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import React from 'react';
|
|
13
|
-
import {describe, it, expect, vi, afterEach} from 'vitest';
|
|
14
|
-
import {
|
|
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';
|
|
15
22
|
import userEvent from '@testing-library/user-event';
|
|
16
23
|
import {useRef, useState, type ReactNode} from 'react';
|
|
17
24
|
import * as stylex from '@stylexjs/stylex';
|
|
@@ -1546,6 +1553,318 @@ describe('SideNavItem — collapsible + href', () => {
|
|
|
1546
1553
|
});
|
|
1547
1554
|
});
|
|
1548
1555
|
|
|
1556
|
+
// =============================================================================
|
|
1557
|
+
// SideNavItem — actions slot (row-level secondary controls, #4988)
|
|
1558
|
+
// =============================================================================
|
|
1559
|
+
|
|
1560
|
+
describe('SideNavItem — actions slot', () => {
|
|
1561
|
+
const rowAction = (onClick?: (e: React.MouseEvent) => void) => (
|
|
1562
|
+
<button type="button" data-testid="row-action" onClick={onClick}>
|
|
1563
|
+
⋯
|
|
1564
|
+
</button>
|
|
1565
|
+
);
|
|
1566
|
+
|
|
1567
|
+
it('renders actions outside the primary interactive element', () => {
|
|
1568
|
+
render(
|
|
1569
|
+
<SideNavItem label="Project" href="/project" actions={rowAction()} />,
|
|
1570
|
+
);
|
|
1571
|
+
const action = screen.getByTestId('row-action');
|
|
1572
|
+
const link = screen.getByRole('link', {name: 'Project'});
|
|
1573
|
+
// Sibling, not nested: the action's closest interactive element is itself.
|
|
1574
|
+
expect(within(link).queryByTestId('row-action')).toBeNull();
|
|
1575
|
+
expect(action.closest('a, button')).toBe(action);
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
it('places actions after the primary element and before nested children', () => {
|
|
1579
|
+
render(
|
|
1580
|
+
<SideNavItem
|
|
1581
|
+
label="Project"
|
|
1582
|
+
href="/project"
|
|
1583
|
+
collapsible
|
|
1584
|
+
actions={rowAction()}>
|
|
1585
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1586
|
+
</SideNavItem>,
|
|
1587
|
+
);
|
|
1588
|
+
const primary = screen.getByRole('link', {name: 'Project'});
|
|
1589
|
+
const action = screen.getByTestId('row-action');
|
|
1590
|
+
const group = screen.getByRole('group');
|
|
1591
|
+
// Compare real node positions, not serialized markup.
|
|
1592
|
+
expect(
|
|
1593
|
+
primary.compareDocumentPosition(action) &
|
|
1594
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1595
|
+
).toBeTruthy();
|
|
1596
|
+
expect(
|
|
1597
|
+
action.compareDocumentPosition(group) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1598
|
+
).toBeTruthy();
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
it('reaches every row-level control before nested items when tabbing', async () => {
|
|
1602
|
+
const user = userEvent.setup();
|
|
1603
|
+
render(
|
|
1604
|
+
<SideNavItem
|
|
1605
|
+
label="Project"
|
|
1606
|
+
href="/project"
|
|
1607
|
+
collapsible
|
|
1608
|
+
actions={rowAction()}>
|
|
1609
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1610
|
+
</SideNavItem>,
|
|
1611
|
+
);
|
|
1612
|
+
await user.tab();
|
|
1613
|
+
expect(screen.getByRole('link', {name: 'Project'})).toHaveFocus();
|
|
1614
|
+
await user.tab();
|
|
1615
|
+
expect(
|
|
1616
|
+
screen.getByRole('button', {name: /collapse project/i}),
|
|
1617
|
+
).toHaveFocus();
|
|
1618
|
+
await user.tab();
|
|
1619
|
+
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
1620
|
+
await user.tab();
|
|
1621
|
+
expect(screen.getByRole('link', {name: 'Session'})).toHaveFocus();
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it('clicking an action does not activate the item', async () => {
|
|
1625
|
+
const user = userEvent.setup();
|
|
1626
|
+
const onItemClick = vi.fn();
|
|
1627
|
+
const onActionClick = vi.fn();
|
|
1628
|
+
render(
|
|
1629
|
+
<SideNavItem
|
|
1630
|
+
label="Project"
|
|
1631
|
+
onClick={onItemClick}
|
|
1632
|
+
actions={rowAction(onActionClick)}
|
|
1633
|
+
/>,
|
|
1634
|
+
);
|
|
1635
|
+
await user.click(screen.getByTestId('row-action'));
|
|
1636
|
+
expect(onActionClick).toHaveBeenCalledTimes(1);
|
|
1637
|
+
expect(onItemClick).not.toHaveBeenCalled();
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it('clicking an action does not toggle collapse of a whole-row toggle item', async () => {
|
|
1641
|
+
const user = userEvent.setup();
|
|
1642
|
+
render(
|
|
1643
|
+
<SideNavItem label="Project" collapsible actions={rowAction()}>
|
|
1644
|
+
<SideNavItem label="Session" />
|
|
1645
|
+
</SideNavItem>,
|
|
1646
|
+
);
|
|
1647
|
+
const rowToggle = screen.getByRole('button', {name: 'Project'});
|
|
1648
|
+
expect(rowToggle).toHaveAttribute('aria-expanded', 'true');
|
|
1649
|
+
await user.click(screen.getByTestId('row-action'));
|
|
1650
|
+
expect(rowToggle).toHaveAttribute('aria-expanded', 'true');
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
it('whole-row toggle still collapses when actions are present', async () => {
|
|
1654
|
+
const user = userEvent.setup();
|
|
1655
|
+
render(
|
|
1656
|
+
<SideNavItem label="Project" collapsible actions={rowAction()}>
|
|
1657
|
+
<SideNavItem label="Session" />
|
|
1658
|
+
</SideNavItem>,
|
|
1659
|
+
);
|
|
1660
|
+
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
1661
|
+
const rowToggle = screen.getByRole('button', {name: 'Project'});
|
|
1662
|
+
await user.click(rowToggle);
|
|
1663
|
+
expect(rowToggle).toHaveAttribute('aria-expanded', 'false');
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
it('adds a row wrapper only when actions are present', () => {
|
|
1667
|
+
const {unmount} = render(<SideNavItem label="Project" href="/project" />);
|
|
1668
|
+
// Without actions, the link itself is the styled row element.
|
|
1669
|
+
expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
|
|
1670
|
+
'astryx-side-nav-item',
|
|
1671
|
+
);
|
|
1672
|
+
unmount();
|
|
1673
|
+
|
|
1674
|
+
render(
|
|
1675
|
+
<SideNavItem
|
|
1676
|
+
label="Project"
|
|
1677
|
+
href="/project"
|
|
1678
|
+
data-testid="row"
|
|
1679
|
+
actions={rowAction()}
|
|
1680
|
+
/>,
|
|
1681
|
+
);
|
|
1682
|
+
// With actions, a wrapper div is the styled row; the link sits inside it.
|
|
1683
|
+
const link = screen.getByRole('link', {name: 'Project'});
|
|
1684
|
+
expect(link).not.toHaveClass('astryx-side-nav-item');
|
|
1685
|
+
const row = screen.getByTestId('row');
|
|
1686
|
+
expect(row).toHaveClass('astryx-side-nav-item');
|
|
1687
|
+
expect(row).toContainElement(link);
|
|
1688
|
+
expect(row).toContainElement(screen.getByTestId('row-action'));
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
it('adds no row wrapper for a falsy actions value', () => {
|
|
1692
|
+
// `actions={canEdit ? <Menu /> : null}` is the ordinary consumer shape.
|
|
1693
|
+
// A wrapper there would change markup, focus order and the ring's
|
|
1694
|
+
// owner for a row that has no row controls at all.
|
|
1695
|
+
const {rerender} = render(
|
|
1696
|
+
<SideNavItem label="Project" href="/project" actions={null} />,
|
|
1697
|
+
);
|
|
1698
|
+
expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
|
|
1699
|
+
'astryx-side-nav-item',
|
|
1700
|
+
);
|
|
1701
|
+
|
|
1702
|
+
rerender(<SideNavItem label="Project" href="/project" actions={false} />);
|
|
1703
|
+
expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
|
|
1704
|
+
'astryx-side-nav-item',
|
|
1705
|
+
);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it('keeps aria wiring on the toggle when actions are present', () => {
|
|
1709
|
+
render(
|
|
1710
|
+
<SideNavItem
|
|
1711
|
+
label="Project"
|
|
1712
|
+
href="/project"
|
|
1713
|
+
collapsible
|
|
1714
|
+
actions={rowAction()}>
|
|
1715
|
+
<SideNavItem label="Session" />
|
|
1716
|
+
</SideNavItem>,
|
|
1717
|
+
);
|
|
1718
|
+
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
1719
|
+
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
1720
|
+
const group = screen.getByRole('group');
|
|
1721
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
1722
|
+
expect(toggle).toHaveAttribute('aria-controls', group.id);
|
|
1723
|
+
expect(screen.getByRole('link', {name: 'Project'})).not.toHaveAttribute(
|
|
1724
|
+
'aria-expanded',
|
|
1725
|
+
);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
it('keeps endContent inside the primary element when actions are present', () => {
|
|
1729
|
+
render(
|
|
1730
|
+
<SideNavItem
|
|
1731
|
+
label="Project"
|
|
1732
|
+
href="/project"
|
|
1733
|
+
endContent={<span data-testid="badge">3</span>}
|
|
1734
|
+
actions={rowAction()}
|
|
1735
|
+
/>,
|
|
1736
|
+
);
|
|
1737
|
+
const link = screen.getByRole('link', {name: /Project/});
|
|
1738
|
+
expect(within(link).getByTestId('badge')).toBeInTheDocument();
|
|
1739
|
+
expect(within(link).queryByTestId('row-action')).toBeNull();
|
|
1740
|
+
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
it('keeps collapsed children inert while the action stays in the row', () => {
|
|
1744
|
+
render(
|
|
1745
|
+
<SideNavItem
|
|
1746
|
+
label="Project"
|
|
1747
|
+
collapsible={{defaultIsCollapsed: true}}
|
|
1748
|
+
actions={rowAction()}>
|
|
1749
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1750
|
+
</SideNavItem>,
|
|
1751
|
+
);
|
|
1752
|
+
const rowToggle = screen.getByRole('button', {name: 'Project'});
|
|
1753
|
+
const group = document.getElementById(
|
|
1754
|
+
rowToggle.getAttribute('aria-controls')!,
|
|
1755
|
+
);
|
|
1756
|
+
expect(group).toHaveAttribute('inert');
|
|
1757
|
+
expect(screen.getByTestId('row-action')).toBeInTheDocument();
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
it('does not render actions in the collapsed rail', () => {
|
|
1761
|
+
const {unmount} = renderCollapsed(
|
|
1762
|
+
<SideNavItem
|
|
1763
|
+
label="Project"
|
|
1764
|
+
icon={StubIcon}
|
|
1765
|
+
href="/project"
|
|
1766
|
+
actions={rowAction()}
|
|
1767
|
+
/>,
|
|
1768
|
+
);
|
|
1769
|
+
expect(screen.getByLabelText('Project')).toBeInTheDocument();
|
|
1770
|
+
expect(screen.queryByTestId('row-action')).toBeNull();
|
|
1771
|
+
unmount();
|
|
1772
|
+
|
|
1773
|
+
renderCollapsed(
|
|
1774
|
+
<SideNavItem label="Project" icon={StubIcon} actions={rowAction()}>
|
|
1775
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1776
|
+
</SideNavItem>,
|
|
1777
|
+
);
|
|
1778
|
+
expect(screen.getByLabelText('Project')).toBeInTheDocument();
|
|
1779
|
+
expect(screen.queryByTestId('row-action')).toBeNull();
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
it('keeps actions clickable on a disabled item', async () => {
|
|
1783
|
+
const user = userEvent.setup();
|
|
1784
|
+
const onActionClick = vi.fn();
|
|
1785
|
+
render(
|
|
1786
|
+
<SideNavItem
|
|
1787
|
+
label="Project"
|
|
1788
|
+
href="/project"
|
|
1789
|
+
isDisabled
|
|
1790
|
+
actions={rowAction(onActionClick)}
|
|
1791
|
+
/>,
|
|
1792
|
+
);
|
|
1793
|
+
// The row's disabled styling must not swallow the sibling action:
|
|
1794
|
+
// the action control owns its own disabled state.
|
|
1795
|
+
await user.click(screen.getByTestId('row-action'));
|
|
1796
|
+
expect(onActionClick).toHaveBeenCalledTimes(1);
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
it('places actions after the toggle when endContent is also present', () => {
|
|
1800
|
+
render(
|
|
1801
|
+
<SideNavItem
|
|
1802
|
+
label="Project"
|
|
1803
|
+
href="/project"
|
|
1804
|
+
collapsible
|
|
1805
|
+
endContent={<span data-testid="badge">3</span>}
|
|
1806
|
+
actions={rowAction()}>
|
|
1807
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1808
|
+
</SideNavItem>,
|
|
1809
|
+
);
|
|
1810
|
+
const link = screen.getByRole('link', {name: /Project/});
|
|
1811
|
+
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
1812
|
+
const action = screen.getByTestId('row-action');
|
|
1813
|
+
expect(within(link).getByTestId('badge')).toBeInTheDocument();
|
|
1814
|
+
expect(
|
|
1815
|
+
link.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1816
|
+
).toBeTruthy();
|
|
1817
|
+
expect(
|
|
1818
|
+
toggle.compareDocumentPosition(action) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1819
|
+
).toBeTruthy();
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
it('reaches the action after the whole-row toggle and before nested items', async () => {
|
|
1823
|
+
const user = userEvent.setup();
|
|
1824
|
+
render(
|
|
1825
|
+
<SideNavItem label="Project" collapsible actions={rowAction()}>
|
|
1826
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1827
|
+
</SideNavItem>,
|
|
1828
|
+
);
|
|
1829
|
+
await user.tab();
|
|
1830
|
+
expect(screen.getByRole('button', {name: 'Project'})).toHaveFocus();
|
|
1831
|
+
await user.tab();
|
|
1832
|
+
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
1833
|
+
await user.tab();
|
|
1834
|
+
expect(screen.getByRole('link', {name: 'Session'})).toHaveFocus();
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
it('renders the custom link component inside the row wrapper', () => {
|
|
1838
|
+
render(
|
|
1839
|
+
<LinkProvider component={CustomLink}>
|
|
1840
|
+
<SideNavItem label="Project" href="/project" actions={rowAction()} />
|
|
1841
|
+
</LinkProvider>,
|
|
1842
|
+
);
|
|
1843
|
+
const link = screen.getByRole('link', {name: 'Project'});
|
|
1844
|
+
expect(link).toHaveAttribute('data-custom-link');
|
|
1845
|
+
expect(link.parentElement).toHaveClass('astryx-side-nav-item');
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it('controlled collapse reports toggle intent without flipping until the prop changes', async () => {
|
|
1849
|
+
const user = userEvent.setup();
|
|
1850
|
+
const onCollapsedChange = vi.fn();
|
|
1851
|
+
render(
|
|
1852
|
+
<SideNavItem
|
|
1853
|
+
label="Project"
|
|
1854
|
+
href="/project"
|
|
1855
|
+
collapsible={{isCollapsed: false, onCollapsedChange}}
|
|
1856
|
+
actions={rowAction()}>
|
|
1857
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
1858
|
+
</SideNavItem>,
|
|
1859
|
+
);
|
|
1860
|
+
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
1861
|
+
await user.click(toggle);
|
|
1862
|
+
expect(onCollapsedChange).toHaveBeenCalledWith(true);
|
|
1863
|
+
// Controlled mode: stays expanded until the consumer flips the prop.
|
|
1864
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1549
1868
|
import {SideNavRenderContext} from './SideNavRenderContext';
|
|
1550
1869
|
import {AppShellMobileContext} from '../AppShell/AppShellMobileContext';
|
|
1551
1870
|
|
|
@@ -1625,6 +1944,28 @@ describe('SideNavItem — mobile drawer close-on-activate', () => {
|
|
|
1625
1944
|
await user.click(screen.getByTestId('item'));
|
|
1626
1945
|
expect(closeMobileNav).not.toHaveBeenCalled();
|
|
1627
1946
|
});
|
|
1947
|
+
|
|
1948
|
+
it('does not close the mobile nav when a row action is clicked', async () => {
|
|
1949
|
+
const user = userEvent.setup();
|
|
1950
|
+
const onActionClick = vi.fn();
|
|
1951
|
+
const {closeMobileNav} = renderInDrawer(
|
|
1952
|
+
<SideNavItem
|
|
1953
|
+
label="Home"
|
|
1954
|
+
href="/"
|
|
1955
|
+
actions={
|
|
1956
|
+
<button
|
|
1957
|
+
type="button"
|
|
1958
|
+
data-testid="drawer-row-action"
|
|
1959
|
+
onClick={onActionClick}>
|
|
1960
|
+
⋯
|
|
1961
|
+
</button>
|
|
1962
|
+
}
|
|
1963
|
+
/>,
|
|
1964
|
+
);
|
|
1965
|
+
await user.click(screen.getByTestId('drawer-row-action'));
|
|
1966
|
+
expect(onActionClick).toHaveBeenCalledTimes(1);
|
|
1967
|
+
expect(closeMobileNav).not.toHaveBeenCalled();
|
|
1968
|
+
});
|
|
1628
1969
|
});
|
|
1629
1970
|
|
|
1630
1971
|
// =============================================================================
|
|
@@ -1899,10 +2240,18 @@ describe('SideNav audit regressions', () => {
|
|
|
1899
2240
|
// =============================================================================
|
|
1900
2241
|
// A15 — the shared focus ring
|
|
1901
2242
|
//
|
|
1902
|
-
// jsdom does not derive `:focus-visible` from `.focus()`
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
//
|
|
2243
|
+
// jsdom does not derive `:focus-visible` from `.focus()` for an element
|
|
2244
|
+
// matched directly, so the ring a tab stop draws on itself cannot be read
|
|
2245
|
+
// back here; the geometry is measured in a real browser instead. These pin
|
|
2246
|
+
// the composition — that the focusable element carries the classes
|
|
2247
|
+
// `focusOutlineStyles.focusVisible` compiles to.
|
|
2248
|
+
//
|
|
2249
|
+
// Composition is only proof for a tab stop. A row wrapper is not one, so
|
|
2250
|
+
// `:focus-visible` atoms sitting on it can never match, and asserting them
|
|
2251
|
+
// there proves nothing: the actions-row tests below passed with the
|
|
2252
|
+
// wrapper's ring deleted outright. A wrapper's ring is guarded by `:has()`,
|
|
2253
|
+
// which jsdom *does* re-evaluate against live focus, so those rules are read
|
|
2254
|
+
// out of the stylesheet and matched against the DOM instead.
|
|
1906
2255
|
// =============================================================================
|
|
1907
2256
|
|
|
1908
2257
|
const sharedFocusRingClasses = stylex
|
|
@@ -1923,6 +2272,120 @@ function expectNoSharedFocusRing(el: Element) {
|
|
|
1923
2272
|
}
|
|
1924
2273
|
}
|
|
1925
2274
|
|
|
2275
|
+
/** Every style rule in the live sheet, flattened out of any grouping rule. */
|
|
2276
|
+
function allStyleRules(): CSSStyleRule[] {
|
|
2277
|
+
const out: CSSStyleRule[] = [];
|
|
2278
|
+
const walk = (rules: CSSRuleList) => {
|
|
2279
|
+
for (const rule of Array.from(rules)) {
|
|
2280
|
+
const nested = (rule as CSSGroupingRule).cssRules;
|
|
2281
|
+
if (nested) {
|
|
2282
|
+
walk(nested);
|
|
2283
|
+
}
|
|
2284
|
+
if ((rule as CSSStyleRule).selectorText) {
|
|
2285
|
+
out.push(rule as CSSStyleRule);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
2290
|
+
try {
|
|
2291
|
+
walk(sheet.cssRules);
|
|
2292
|
+
} catch {
|
|
2293
|
+
// A sheet we cannot read cannot be one StyleX injected.
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
return out;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* A rule's declared value for `prop`, read out of its text.
|
|
2301
|
+
*
|
|
2302
|
+
* Not `rule.style`: these values are `var()`s, and jsdom's CSS object model
|
|
2303
|
+
* drops longhands it cannot parse, so they read back as an empty string there.
|
|
2304
|
+
*/
|
|
2305
|
+
function declaredValue(rule: CSSStyleRule, prop: string): string | undefined {
|
|
2306
|
+
const body = /\{([^}]*)\}/.exec(rule.cssText)?.[1] ?? '';
|
|
2307
|
+
return new RegExp(`(?:^|;)\\s*${prop}:\\s*([^;]+)`).exec(body)?.[1].trim();
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/** A rule's `outline-style` — the discriminator for a painting focus ring. */
|
|
2311
|
+
function outlineStyleOf(rule: CSSStyleRule): string | undefined {
|
|
2312
|
+
return declaredValue(rule, 'outline-style');
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/**
|
|
2316
|
+
* What the live sheet unconditionally declares for `prop` on `el`, gathered
|
|
2317
|
+
* from every bare-class rule the element carries. StyleX emits one atomic
|
|
2318
|
+
* class per property, so a well-formed element yields exactly one value —
|
|
2319
|
+
* comparing those values compares real CSS without pinning a generated hash.
|
|
2320
|
+
*/
|
|
2321
|
+
function declaredOn(el: Element, prop: string): string[] {
|
|
2322
|
+
const classes = new Set(el.className.split(' '));
|
|
2323
|
+
return allStyleRules()
|
|
2324
|
+
.filter(rule => {
|
|
2325
|
+
const bare = rule.selectorText.replace(/:not\(#\\?#\)/g, '');
|
|
2326
|
+
const owner = /^\.([^:\s]+)$/.exec(bare)?.[1];
|
|
2327
|
+
return owner != null && classes.has(owner);
|
|
2328
|
+
})
|
|
2329
|
+
.map(rule => declaredValue(rule, prop))
|
|
2330
|
+
.filter(value => value != null);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* The selectors that actually paint the shared ring on `el`, read out of the
|
|
2335
|
+
* live stylesheet with StyleX's `:not(#\#)` specificity padding stripped so
|
|
2336
|
+
* jsdom can evaluate them.
|
|
2337
|
+
*
|
|
2338
|
+
* `outline-style` is the discriminator: StyleX emits one rule per property,
|
|
2339
|
+
* and no outline paints while that one resolves to `none`.
|
|
2340
|
+
*/
|
|
2341
|
+
function ringSelectorsFor(el: Element): string[] {
|
|
2342
|
+
const classes = new Set(el.className.split(' '));
|
|
2343
|
+
return allStyleRules()
|
|
2344
|
+
.filter(rule => {
|
|
2345
|
+
const painted = outlineStyleOf(rule);
|
|
2346
|
+
if (!painted || painted === 'none') {
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
const owner = /^\.([^:\s]+)/.exec(rule.selectorText)?.[1];
|
|
2350
|
+
return owner != null && classes.has(owner);
|
|
2351
|
+
})
|
|
2352
|
+
.map(rule => rule.selectorText.replace(/:not\(#\\?#\)/g, ''));
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* Whether `el`'s ring is painting for whatever holds focus right now.
|
|
2357
|
+
*
|
|
2358
|
+
* `:focus-visible` is substituted with `:focus` before matching. jsdom
|
|
2359
|
+
* derives `:focus-visible` from a heuristic over the last interaction, so it
|
|
2360
|
+
* answers differently depending on which tests ran before — the same
|
|
2361
|
+
* assertion passes alone and fails in the file. `:focus` is unambiguous, and
|
|
2362
|
+
* the ring's modality is pinned separately, on the selector text, by the
|
|
2363
|
+
* keyboard-only test below.
|
|
2364
|
+
*/
|
|
2365
|
+
function ringIsPainting(el: Element): boolean {
|
|
2366
|
+
return ringSelectorsFor(el).some(selector =>
|
|
2367
|
+
el.matches(selector.replace(/:focus-visible/g, ':focus')),
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/**
|
|
2372
|
+
* Whether `el` unconditionally declares `outline-style: none`, which is what
|
|
2373
|
+
* keeps the UA's own focus ring off an element whose ring an ancestor paints.
|
|
2374
|
+
* Computed style cannot answer this: `none` is also the CSS initial value.
|
|
2375
|
+
*/
|
|
2376
|
+
function suppressesOwnOutline(el: Element): boolean {
|
|
2377
|
+
const classes = new Set(el.className.split(' '));
|
|
2378
|
+
return allStyleRules().some(rule => {
|
|
2379
|
+
if (outlineStyleOf(rule) !== 'none') {
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
// Unconditional only — a bare class once the padding is stripped.
|
|
2383
|
+
const bare = rule.selectorText.replace(/:not\(#\\?#\)/g, '');
|
|
2384
|
+
const owner = /^\.([^:\s]+)$/.exec(bare)?.[1];
|
|
2385
|
+
return owner != null && classes.has(owner);
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
|
|
1926
2389
|
describe('SideNav focus ring (A15)', () => {
|
|
1927
2390
|
it('draws the shared ring on a link item', () => {
|
|
1928
2391
|
render(<SideNavItem label="Dashboard" href="/dashboard" />);
|
|
@@ -1970,6 +2433,173 @@ describe('SideNav focus ring (A15)', () => {
|
|
|
1970
2433
|
expectNoSharedFocusRing(row);
|
|
1971
2434
|
});
|
|
1972
2435
|
|
|
2436
|
+
it('rings the row wrapper of an actions row, not the truncated primary', async () => {
|
|
2437
|
+
const user = userEvent.setup();
|
|
2438
|
+
render(
|
|
2439
|
+
<SideNavItem
|
|
2440
|
+
label="Project"
|
|
2441
|
+
href="/project"
|
|
2442
|
+
actions={
|
|
2443
|
+
<button type="button" data-testid="row-action">
|
|
2444
|
+
More
|
|
2445
|
+
</button>
|
|
2446
|
+
}
|
|
2447
|
+
/>,
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
const link = screen.getByRole('link', {name: 'Project'});
|
|
2451
|
+
const row = link.parentElement!;
|
|
2452
|
+
expect(row.tagName).toBe('DIV');
|
|
2453
|
+
expect(row).toContainElement(screen.getByTestId('row-action'));
|
|
2454
|
+
|
|
2455
|
+
// Same pill as an ordinary row: the ring lives on the wrapper, whose box
|
|
2456
|
+
// includes the actions. Putting it on the primary would clip to the
|
|
2457
|
+
// truncated split-action element (square, not full-row).
|
|
2458
|
+
expect(ringIsPainting(row)).toBe(false);
|
|
2459
|
+
await user.tab();
|
|
2460
|
+
expect(link).toHaveFocus();
|
|
2461
|
+
expect(ringIsPainting(row)).toBe(true);
|
|
2462
|
+
// And exactly one ring: nothing paints a second one on the primary.
|
|
2463
|
+
expect(ringSelectorsFor(link)).toEqual([]);
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
it('leaves the wrapper ring to the primary, not the chevron toggle', async () => {
|
|
2467
|
+
const user = userEvent.setup();
|
|
2468
|
+
render(
|
|
2469
|
+
<SideNavItem
|
|
2470
|
+
label="Settings"
|
|
2471
|
+
href="/settings"
|
|
2472
|
+
collapsible
|
|
2473
|
+
actions={
|
|
2474
|
+
<button type="button" data-testid="row-action">
|
|
2475
|
+
More
|
|
2476
|
+
</button>
|
|
2477
|
+
}>
|
|
2478
|
+
<SideNavItem label="General" href="/settings/general" />
|
|
2479
|
+
</SideNavItem>,
|
|
2480
|
+
);
|
|
2481
|
+
|
|
2482
|
+
const link = screen.getByRole('link', {name: 'Settings'});
|
|
2483
|
+
const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
|
|
2484
|
+
const row = link.parentElement!;
|
|
2485
|
+
expect(row).toContainElement(screen.getByTestId('row-action'));
|
|
2486
|
+
|
|
2487
|
+
await user.tab();
|
|
2488
|
+
expect(link).toHaveFocus();
|
|
2489
|
+
expect(ringIsPainting(row)).toBe(true);
|
|
2490
|
+
|
|
2491
|
+
// The toggle owns its own ring, as it does in a split-action row with no
|
|
2492
|
+
// actions. Keeping the wrapper lit too would stack a full-row outline
|
|
2493
|
+
// around the chevron's own — two rings for one tab stop.
|
|
2494
|
+
await user.tab();
|
|
2495
|
+
expect(toggle).toHaveFocus();
|
|
2496
|
+
expectSharedFocusRing(toggle);
|
|
2497
|
+
expect(ringIsPainting(row)).toBe(false);
|
|
2498
|
+
});
|
|
2499
|
+
|
|
2500
|
+
it('does not ring the row wrapper when a supplied action takes focus', async () => {
|
|
2501
|
+
const user = userEvent.setup();
|
|
2502
|
+
render(
|
|
2503
|
+
<SideNavItem
|
|
2504
|
+
label="Project"
|
|
2505
|
+
href="/project"
|
|
2506
|
+
actions={
|
|
2507
|
+
<button type="button" data-testid="row-action">
|
|
2508
|
+
More
|
|
2509
|
+
</button>
|
|
2510
|
+
}
|
|
2511
|
+
/>,
|
|
2512
|
+
);
|
|
2513
|
+
|
|
2514
|
+
const row = screen.getByRole('link', {name: 'Project'}).parentElement!;
|
|
2515
|
+
await user.tab();
|
|
2516
|
+
expect(ringIsPainting(row)).toBe(true);
|
|
2517
|
+
|
|
2518
|
+
// An action draws whatever ring its own component draws. Focus is not on
|
|
2519
|
+
// the row, so the row must not light up around it.
|
|
2520
|
+
await user.tab();
|
|
2521
|
+
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
2522
|
+
expect(ringIsPainting(row)).toBe(false);
|
|
2523
|
+
});
|
|
2524
|
+
|
|
2525
|
+
it('rings the wrapper of a whole-row toggle carrying actions', async () => {
|
|
2526
|
+
const user = userEvent.setup();
|
|
2527
|
+
render(
|
|
2528
|
+
<SideNavItem
|
|
2529
|
+
label="Project"
|
|
2530
|
+
collapsible
|
|
2531
|
+
actions={
|
|
2532
|
+
<button type="button" data-testid="row-action">
|
|
2533
|
+
More
|
|
2534
|
+
</button>
|
|
2535
|
+
}>
|
|
2536
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
2537
|
+
</SideNavItem>,
|
|
2538
|
+
);
|
|
2539
|
+
|
|
2540
|
+
// No href and no onClick, so the primary *is* the collapse toggle and
|
|
2541
|
+
// keeps the chevron inside it — a different row shape from the split.
|
|
2542
|
+
const primary = screen.getByRole('button', {name: 'Project'});
|
|
2543
|
+
const row = primary.parentElement!;
|
|
2544
|
+
|
|
2545
|
+
await user.tab();
|
|
2546
|
+
expect(primary).toHaveFocus();
|
|
2547
|
+
expect(ringIsPainting(row)).toBe(true);
|
|
2548
|
+
|
|
2549
|
+
await user.tab();
|
|
2550
|
+
expect(screen.getByTestId('row-action')).toHaveFocus();
|
|
2551
|
+
expect(ringIsPainting(row)).toBe(false);
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
it('keeps the UA focus ring off the primary the wrapper rings for', () => {
|
|
2555
|
+
render(
|
|
2556
|
+
<SideNavItem
|
|
2557
|
+
label="Project"
|
|
2558
|
+
href="/project"
|
|
2559
|
+
actions={
|
|
2560
|
+
<button type="button" data-testid="row-action">
|
|
2561
|
+
More
|
|
2562
|
+
</button>
|
|
2563
|
+
}
|
|
2564
|
+
/>,
|
|
2565
|
+
);
|
|
2566
|
+
|
|
2567
|
+
const link = screen.getByRole('link', {name: 'Project'});
|
|
2568
|
+
// Dropping the shared ring off the primary also drops the
|
|
2569
|
+
// `outline-style: none` default that came with it, and a bare <a> keeps
|
|
2570
|
+
// the browser's own focus ring — which would paint inside the wrapper's.
|
|
2571
|
+
expect(suppressesOwnOutline(link)).toBe(true);
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
it('paints the actions-row ring for keyboard focus only', () => {
|
|
2575
|
+
render(
|
|
2576
|
+
<SideNavItem
|
|
2577
|
+
label="Project"
|
|
2578
|
+
href="/project"
|
|
2579
|
+
actions={
|
|
2580
|
+
<button type="button" data-testid="row-action">
|
|
2581
|
+
More
|
|
2582
|
+
</button>
|
|
2583
|
+
}
|
|
2584
|
+
/>,
|
|
2585
|
+
);
|
|
2586
|
+
|
|
2587
|
+
const link = screen.getByRole('link', {name: 'Project'});
|
|
2588
|
+
const row = link.parentElement!;
|
|
2589
|
+
// The wrapper is not a tab stop, so its ring has to be guarded by a
|
|
2590
|
+
// relational selector. `:focus-within` or `:focus` would light the row
|
|
2591
|
+
// for mouse users too; only `:focus-visible` keeps it to the keyboard.
|
|
2592
|
+
const selectors = ringSelectorsFor(row);
|
|
2593
|
+
expect(selectors.length).toBeGreaterThan(0);
|
|
2594
|
+
for (const selector of selectors) {
|
|
2595
|
+
expect(selector).toContain(':focus-visible');
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// The guard is scoped to the wrapper's first child. Reordering the row
|
|
2599
|
+
// would hand the ring to the toggle or the actions instead.
|
|
2600
|
+
expect(row.firstElementChild).toBe(link);
|
|
2601
|
+
});
|
|
2602
|
+
|
|
1973
2603
|
it('draws the shared ring on a heading rendered as one link', () => {
|
|
1974
2604
|
render(<SideNavHeading heading="My App" headingHref="/" />);
|
|
1975
2605
|
expectSharedFocusRing(screen.getByRole('link', {name: /My App/}));
|
|
@@ -2165,6 +2795,76 @@ describe('SideNav footer icon row sizing', () => {
|
|
|
2165
2795
|
});
|
|
2166
2796
|
});
|
|
2167
2797
|
|
|
2798
|
+
describe('SideNav row control sizing', () => {
|
|
2799
|
+
/**
|
|
2800
|
+
* The height an icon-only `Button` of this size declares — the box every
|
|
2801
|
+
* control sharing a nav row is measured against.
|
|
2802
|
+
*/
|
|
2803
|
+
function iconButtonHeight(size: 'sm' | 'md' | 'lg'): string[] {
|
|
2804
|
+
const {unmount} = render(
|
|
2805
|
+
<Button
|
|
2806
|
+
label="sized reference"
|
|
2807
|
+
size={size}
|
|
2808
|
+
isIconOnly
|
|
2809
|
+
icon={<span />}
|
|
2810
|
+
data-testid="sized-ref"
|
|
2811
|
+
/>,
|
|
2812
|
+
);
|
|
2813
|
+
const height = declaredOn(screen.getByTestId('sized-ref'), 'height');
|
|
2814
|
+
unmount();
|
|
2815
|
+
// Guard against a vacuous pass: nothing below means anything if the
|
|
2816
|
+
// reference itself declares no height.
|
|
2817
|
+
expect(height).toHaveLength(1);
|
|
2818
|
+
return height;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function collapsibleRow(actions: ReactNode) {
|
|
2822
|
+
return (
|
|
2823
|
+
<SideNavItem
|
|
2824
|
+
label="Project"
|
|
2825
|
+
href="/project"
|
|
2826
|
+
collapsible
|
|
2827
|
+
actions={actions}>
|
|
2828
|
+
<SideNavItem label="Session" href="/project/session" />
|
|
2829
|
+
</SideNavItem>
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
const iconAction = (props?: {size?: 'sm' | 'md' | 'lg'}) => (
|
|
2834
|
+
<Button
|
|
2835
|
+
label="Project actions"
|
|
2836
|
+
isIconOnly
|
|
2837
|
+
icon={<span />}
|
|
2838
|
+
data-testid="row-action"
|
|
2839
|
+
{...props}
|
|
2840
|
+
/>
|
|
2841
|
+
);
|
|
2842
|
+
|
|
2843
|
+
it('sizes the expand toggle like the icon buttons it sits beside', () => {
|
|
2844
|
+
const sm = iconButtonHeight('sm');
|
|
2845
|
+
render(collapsibleRow(iconAction()));
|
|
2846
|
+
const toggle = screen.getByRole('button', {name: /collapse project/i});
|
|
2847
|
+
expect(declaredOn(toggle, 'height')).toEqual(sm);
|
|
2848
|
+
// Square, so its hover pill reads as the same box as the action's.
|
|
2849
|
+
expect(declaredOn(toggle, 'width')).toEqual(sm);
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
it('cascades that size to an action the consumer did not size', () => {
|
|
2853
|
+
const sm = iconButtonHeight('sm');
|
|
2854
|
+
// Without the cascade an unsized Button falls back to `md`; if the two
|
|
2855
|
+
// compiled to one height the assertion below could not fail.
|
|
2856
|
+
expect(sm).not.toEqual(iconButtonHeight('md'));
|
|
2857
|
+
render(collapsibleRow(iconAction()));
|
|
2858
|
+
expect(declaredOn(screen.getByTestId('row-action'), 'height')).toEqual(sm);
|
|
2859
|
+
});
|
|
2860
|
+
|
|
2861
|
+
it('lets an explicit size on an action win over the cascade', () => {
|
|
2862
|
+
const lg = iconButtonHeight('lg');
|
|
2863
|
+
render(collapsibleRow(iconAction({size: 'lg'})));
|
|
2864
|
+
expect(declaredOn(screen.getByTestId('row-action'), 'height')).toEqual(lg);
|
|
2865
|
+
});
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2168
2868
|
describe('SideNavHeading hover/click guard', () => {
|
|
2169
2869
|
// tabIndex={-1} matches real menu items; a bare role=menuitem div is not
|
|
2170
2870
|
// focusable.
|
|
@@ -2249,3 +2949,366 @@ describe('SideNavHeading hover/click guard', () => {
|
|
|
2249
2949
|
expect(trigger).toHaveFocus();
|
|
2250
2950
|
});
|
|
2251
2951
|
});
|
|
2952
|
+
|
|
2953
|
+
// =============================================================================
|
|
2954
|
+
// Collapse ownership — collapsible + resizable (#4790, #5073)
|
|
2955
|
+
// =============================================================================
|
|
2956
|
+
|
|
2957
|
+
const AUTO_SAVE_ID = 'sidenav-collapse-owner';
|
|
2958
|
+
const STORAGE_KEY = `astryx-resizable:${AUTO_SAVE_ID}`;
|
|
2959
|
+
|
|
2960
|
+
function expectCollapsed(isCollapsed: boolean) {
|
|
2961
|
+
const label = isCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
|
|
2962
|
+
expect(screen.getByRole('button', {name: label})).toBeInTheDocument();
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
describe('SideNav collapse ownership', () => {
|
|
2966
|
+
beforeEach(() => {
|
|
2967
|
+
localStorage.clear();
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
// Every row is a configuration that works on origin/main today, with the
|
|
2971
|
+
// collapse state main resolves for it. Normalizing the two props into one
|
|
2972
|
+
// owner must not move any of these — the one case whose outcome does change
|
|
2973
|
+
// is the divergence itself, pinned separately below.
|
|
2974
|
+
const preservedCases: {
|
|
2975
|
+
name: string;
|
|
2976
|
+
persisted?: string;
|
|
2977
|
+
element: React.ReactElement;
|
|
2978
|
+
isCollapsed: boolean;
|
|
2979
|
+
}[] = [
|
|
2980
|
+
{
|
|
2981
|
+
name: 'collapsible alone',
|
|
2982
|
+
element: <SideNav collapsible>Content</SideNav>,
|
|
2983
|
+
isCollapsed: false,
|
|
2984
|
+
},
|
|
2985
|
+
{
|
|
2986
|
+
name: 'collapsible with defaultIsCollapsed',
|
|
2987
|
+
element: (
|
|
2988
|
+
<SideNav collapsible={{defaultIsCollapsed: true}}>Content</SideNav>
|
|
2989
|
+
),
|
|
2990
|
+
isCollapsed: true,
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
name: 'controlled collapsible (collapsed)',
|
|
2994
|
+
element: (
|
|
2995
|
+
<SideNav collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}>
|
|
2996
|
+
Content
|
|
2997
|
+
</SideNav>
|
|
2998
|
+
),
|
|
2999
|
+
isCollapsed: true,
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
name: 'controlled collapsible (expanded)',
|
|
3003
|
+
element: (
|
|
3004
|
+
<SideNav
|
|
3005
|
+
collapsible={{isCollapsed: false, onCollapsedChange: () => {}}}>
|
|
3006
|
+
Content
|
|
3007
|
+
</SideNav>
|
|
3008
|
+
),
|
|
3009
|
+
isCollapsed: false,
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
name: 'collapsible + resizable',
|
|
3013
|
+
element: (
|
|
3014
|
+
<SideNav collapsible resizable>
|
|
3015
|
+
Content
|
|
3016
|
+
</SideNav>
|
|
3017
|
+
),
|
|
3018
|
+
isCollapsed: false,
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
name: 'defaultIsCollapsed + resizable',
|
|
3022
|
+
element: (
|
|
3023
|
+
<SideNav collapsible={{defaultIsCollapsed: true}} resizable>
|
|
3024
|
+
Content
|
|
3025
|
+
</SideNav>
|
|
3026
|
+
),
|
|
3027
|
+
isCollapsed: true,
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
name: 'controlled collapsible + resizable',
|
|
3031
|
+
element: (
|
|
3032
|
+
<SideNav
|
|
3033
|
+
collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}
|
|
3034
|
+
resizable>
|
|
3035
|
+
Content
|
|
3036
|
+
</SideNav>
|
|
3037
|
+
),
|
|
3038
|
+
isCollapsed: true,
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
name: 'defaultIsCollapsed + a persisted legacy width',
|
|
3042
|
+
persisted: '300',
|
|
3043
|
+
element: (
|
|
3044
|
+
<SideNav
|
|
3045
|
+
collapsible={{defaultIsCollapsed: true}}
|
|
3046
|
+
resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3047
|
+
Content
|
|
3048
|
+
</SideNav>
|
|
3049
|
+
),
|
|
3050
|
+
isCollapsed: true,
|
|
3051
|
+
},
|
|
3052
|
+
{
|
|
3053
|
+
name: 'collapsible + a persisted legacy width',
|
|
3054
|
+
persisted: '300',
|
|
3055
|
+
element: (
|
|
3056
|
+
<SideNav collapsible resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3057
|
+
Content
|
|
3058
|
+
</SideNav>
|
|
3059
|
+
),
|
|
3060
|
+
isCollapsed: false,
|
|
3061
|
+
},
|
|
3062
|
+
];
|
|
3063
|
+
|
|
3064
|
+
it.each(preservedCases)(
|
|
3065
|
+
'resolves $name the way main does',
|
|
3066
|
+
({persisted, element, isCollapsed}) => {
|
|
3067
|
+
if (persisted != null) {
|
|
3068
|
+
localStorage.setItem(STORAGE_KEY, persisted);
|
|
3069
|
+
}
|
|
3070
|
+
render(element);
|
|
3071
|
+
expectCollapsed(isCollapsed);
|
|
3072
|
+
},
|
|
3073
|
+
);
|
|
3074
|
+
|
|
3075
|
+
it('keeps resizable alone free of collapse', () => {
|
|
3076
|
+
render(<SideNav resizable>Content</SideNav>);
|
|
3077
|
+
expect(
|
|
3078
|
+
screen.queryByRole('button', {name: /sidebar/i}),
|
|
3079
|
+
).not.toBeInTheDocument();
|
|
3080
|
+
expect(screen.getByRole('navigation').style.width).toBe('260px');
|
|
3081
|
+
});
|
|
3082
|
+
|
|
3083
|
+
it('restores the collapsed rail from a legacy persisted 0 (#4790)', () => {
|
|
3084
|
+
// main renders the expanded layout here while the resize hook restores
|
|
3085
|
+
// collapsed, so the nav paints at width 0 with no way back.
|
|
3086
|
+
localStorage.setItem(STORAGE_KEY, '0');
|
|
3087
|
+
render(
|
|
3088
|
+
<SideNav collapsible resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3089
|
+
Content
|
|
3090
|
+
</SideNav>,
|
|
3091
|
+
);
|
|
3092
|
+
|
|
3093
|
+
expectCollapsed(true);
|
|
3094
|
+
expect(screen.getByRole('navigation').style.width).not.toBe('0px');
|
|
3095
|
+
});
|
|
3096
|
+
|
|
3097
|
+
it('restores the collapsed rail and the pre-collapse width across a reload', async () => {
|
|
3098
|
+
const user = userEvent.setup();
|
|
3099
|
+
const nav = (
|
|
3100
|
+
<SideNav
|
|
3101
|
+
collapsible
|
|
3102
|
+
resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 260}}>
|
|
3103
|
+
Content
|
|
3104
|
+
</SideNav>
|
|
3105
|
+
);
|
|
3106
|
+
|
|
3107
|
+
const first = render(nav);
|
|
3108
|
+
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3109
|
+
expectCollapsed(true);
|
|
3110
|
+
first.unmount();
|
|
3111
|
+
|
|
3112
|
+
render(nav);
|
|
3113
|
+
expectCollapsed(true);
|
|
3114
|
+
|
|
3115
|
+
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
3116
|
+
expect(screen.getByRole('navigation').style.width).toBe('260px');
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
it('expands to the persisted pre-collapse width, not the default', async () => {
|
|
3120
|
+
const user = userEvent.setup();
|
|
3121
|
+
localStorage.setItem(
|
|
3122
|
+
STORAGE_KEY,
|
|
3123
|
+
JSON.stringify({size: 320, isCollapsed: true}),
|
|
3124
|
+
);
|
|
3125
|
+
render(
|
|
3126
|
+
<SideNav
|
|
3127
|
+
collapsible
|
|
3128
|
+
resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 260}}>
|
|
3129
|
+
Content
|
|
3130
|
+
</SideNav>,
|
|
3131
|
+
);
|
|
3132
|
+
|
|
3133
|
+
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
3134
|
+
expect(screen.getByRole('navigation').style.width).toBe('320px');
|
|
3135
|
+
});
|
|
3136
|
+
|
|
3137
|
+
it('reports one collapse change per toggle', async () => {
|
|
3138
|
+
const user = userEvent.setup();
|
|
3139
|
+
const onCollapsedChange = vi.fn();
|
|
3140
|
+
render(
|
|
3141
|
+
<SideNav collapsible={{onCollapsedChange}} resizable>
|
|
3142
|
+
Content
|
|
3143
|
+
</SideNav>,
|
|
3144
|
+
);
|
|
3145
|
+
|
|
3146
|
+
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3147
|
+
expect(onCollapsedChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
3148
|
+
});
|
|
3149
|
+
|
|
3150
|
+
it('lets a controlled collapsible drive a resizable nav from outside', async () => {
|
|
3151
|
+
const user = userEvent.setup();
|
|
3152
|
+
|
|
3153
|
+
function Harness() {
|
|
3154
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
3155
|
+
const collapsible = {isCollapsed, onCollapsedChange: setIsCollapsed};
|
|
3156
|
+
return (
|
|
3157
|
+
<>
|
|
3158
|
+
<SideNavCollapseButton collapsible={collapsible} label="Outside" />
|
|
3159
|
+
<SideNav
|
|
3160
|
+
collapsible={{...collapsible, hasButton: false}}
|
|
3161
|
+
resizable={{autoSaveId: AUTO_SAVE_ID}}>
|
|
3162
|
+
Content
|
|
3163
|
+
</SideNav>
|
|
3164
|
+
</>
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
render(<Harness />);
|
|
3169
|
+
expect(screen.getByRole('navigation').style.width).toBe('260px');
|
|
3170
|
+
|
|
3171
|
+
await user.click(screen.getByRole('button', {name: 'Outside'}));
|
|
3172
|
+
expect(screen.getByRole('navigation').style.width).toBe('');
|
|
3173
|
+
expect(
|
|
3174
|
+
screen.queryByTestId('astryx-sidenav-resize-handle'),
|
|
3175
|
+
).not.toBeInTheDocument();
|
|
3176
|
+
});
|
|
3177
|
+
|
|
3178
|
+
it('lets resizable own collapse on its own', async () => {
|
|
3179
|
+
const user = userEvent.setup();
|
|
3180
|
+
const onCollapseChange = vi.fn();
|
|
3181
|
+
render(
|
|
3182
|
+
<SideNav resizable={{defaultIsCollapsed: true, onCollapseChange}}>
|
|
3183
|
+
Content
|
|
3184
|
+
</SideNav>,
|
|
3185
|
+
);
|
|
3186
|
+
|
|
3187
|
+
expectCollapsed(true);
|
|
3188
|
+
await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
|
|
3189
|
+
expect(onCollapseChange).toHaveBeenCalledExactlyOnceWith(false);
|
|
3190
|
+
});
|
|
3191
|
+
|
|
3192
|
+
it('keeps collapse state when resize is toggled off and on', async () => {
|
|
3193
|
+
const user = userEvent.setup();
|
|
3194
|
+
const {rerender} = render(
|
|
3195
|
+
<SideNav collapsible resizable>
|
|
3196
|
+
Content
|
|
3197
|
+
</SideNav>,
|
|
3198
|
+
);
|
|
3199
|
+
|
|
3200
|
+
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3201
|
+
expectCollapsed(true);
|
|
3202
|
+
|
|
3203
|
+
rerender(<SideNav collapsible>Content</SideNav>);
|
|
3204
|
+
expectCollapsed(true);
|
|
3205
|
+
|
|
3206
|
+
rerender(
|
|
3207
|
+
<SideNav collapsible resizable>
|
|
3208
|
+
Content
|
|
3209
|
+
</SideNav>,
|
|
3210
|
+
);
|
|
3211
|
+
expectCollapsed(true);
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
it('gives resizable the winning value when both set defaultIsCollapsed', () => {
|
|
3215
|
+
render(
|
|
3216
|
+
<SideNav
|
|
3217
|
+
collapsible={{defaultIsCollapsed: false}}
|
|
3218
|
+
resizable={{defaultIsCollapsed: true}}>
|
|
3219
|
+
Content
|
|
3220
|
+
</SideNav>,
|
|
3221
|
+
);
|
|
3222
|
+
expectCollapsed(true);
|
|
3223
|
+
});
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
describe('SideNav collapse conflict warning', () => {
|
|
3227
|
+
let warn: ReturnType<typeof vi.spyOn>;
|
|
3228
|
+
|
|
3229
|
+
beforeEach(() => {
|
|
3230
|
+
localStorage.clear();
|
|
3231
|
+
warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
afterEach(() => {
|
|
3235
|
+
warn.mockRestore();
|
|
3236
|
+
});
|
|
3237
|
+
|
|
3238
|
+
it('warns when both props carry defaultIsCollapsed', () => {
|
|
3239
|
+
render(
|
|
3240
|
+
<SideNav
|
|
3241
|
+
collapsible={{defaultIsCollapsed: false}}
|
|
3242
|
+
resizable={{defaultIsCollapsed: true}}>
|
|
3243
|
+
Content
|
|
3244
|
+
</SideNav>,
|
|
3245
|
+
);
|
|
3246
|
+
|
|
3247
|
+
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
|
3248
|
+
expect.stringContaining('collapsible.defaultIsCollapsed'),
|
|
3249
|
+
);
|
|
3250
|
+
expect(warn.mock.calls[0][0]).toContain('resizable.defaultIsCollapsed');
|
|
3251
|
+
expect(warn.mock.calls[0][0]).toContain('resizable wins');
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
it('warns when a controlled collapsible does not drive collapse', () => {
|
|
3255
|
+
render(
|
|
3256
|
+
<SideNav
|
|
3257
|
+
collapsible={{isCollapsed: false, onCollapsedChange: () => {}}}
|
|
3258
|
+
resizable={{isCollapsed: true, onCollapseChange: () => {}}}>
|
|
3259
|
+
Content
|
|
3260
|
+
</SideNav>,
|
|
3261
|
+
);
|
|
3262
|
+
|
|
3263
|
+
const message = String(warn.mock.calls[0][0]);
|
|
3264
|
+
expect(message).toContain('collapsible.isCollapsed');
|
|
3265
|
+
expect(message).toContain('collapsible.onCollapsedChange');
|
|
3266
|
+
expect(message).toContain('resizable.isCollapsed');
|
|
3267
|
+
expect(message).toContain('resizable wins');
|
|
3268
|
+
// The winner is the state that actually renders.
|
|
3269
|
+
expectCollapsed(true);
|
|
3270
|
+
});
|
|
3271
|
+
|
|
3272
|
+
it('does not split state and callback ownership across props', async () => {
|
|
3273
|
+
const user = userEvent.setup();
|
|
3274
|
+
const collapsibleChange = vi.fn();
|
|
3275
|
+
const resizableChange = vi.fn();
|
|
3276
|
+
render(
|
|
3277
|
+
<SideNav
|
|
3278
|
+
collapsible={{
|
|
3279
|
+
isCollapsed: true,
|
|
3280
|
+
onCollapsedChange: collapsibleChange,
|
|
3281
|
+
}}
|
|
3282
|
+
resizable={{onCollapseChange: resizableChange}}>
|
|
3283
|
+
Content
|
|
3284
|
+
</SideNav>,
|
|
3285
|
+
);
|
|
3286
|
+
|
|
3287
|
+
expectCollapsed(false);
|
|
3288
|
+
await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
|
|
3289
|
+
expect(resizableChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
3290
|
+
expect(collapsibleChange).not.toHaveBeenCalled();
|
|
3291
|
+
});
|
|
3292
|
+
|
|
3293
|
+
it('stays silent for collapsible alongside a resize config', () => {
|
|
3294
|
+
render(
|
|
3295
|
+
<SideNav
|
|
3296
|
+
collapsible
|
|
3297
|
+
resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 300}}>
|
|
3298
|
+
Content
|
|
3299
|
+
</SideNav>,
|
|
3300
|
+
);
|
|
3301
|
+
expect(warn).not.toHaveBeenCalled();
|
|
3302
|
+
});
|
|
3303
|
+
|
|
3304
|
+
it('stays silent for a controlled collapsible with a plain resize config', () => {
|
|
3305
|
+
render(
|
|
3306
|
+
<SideNav
|
|
3307
|
+
collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}
|
|
3308
|
+
resizable={{defaultWidth: 300}}>
|
|
3309
|
+
Content
|
|
3310
|
+
</SideNav>,
|
|
3311
|
+
);
|
|
3312
|
+
expect(warn).not.toHaveBeenCalled();
|
|
3313
|
+
});
|
|
3314
|
+
});
|