@astryxdesign/core 0.4.6 → 0.4.7-canary.20bae6c
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 +36 -0
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +3 -2
- package/dist/AspectRatio/AspectRatio.d.ts +24 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +23 -4
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +3 -2
- package/dist/BottomSheet/BottomSheet.d.ts +1 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +54 -15
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +13 -6
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +23 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
- package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/ButtonGroup/ButtonGroup.js +12 -4
- package/dist/Calendar/Calendar.d.ts +3 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +13 -14
- package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
- package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
- package/dist/Calendar/getInitialFocusDate.js +62 -0
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +3 -3
- package/dist/Chat/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayout.js +3 -3
- package/dist/Chat/ChatMessageList.d.ts +22 -3
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +6 -3
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +3 -3
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +3 -2
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +3 -2
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -3
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +3 -2
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +34 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +3 -2
- package/dist/Dialog/Dialog.d.ts +7 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +89 -58
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +3 -2
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +6 -2
- package/dist/HoverCard/HoverCard.d.ts +6 -0
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.d.ts +5 -0
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +67 -25
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +4 -2
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +10 -3
- package/dist/Layer/LayerDepthContext.d.ts +52 -0
- package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
- package/dist/Layer/LayerDepthContext.js +66 -0
- package/dist/Layer/index.d.ts +3 -0
- package/dist/Layer/index.d.ts.map +1 -1
- package/dist/Layer/index.js +4 -0
- package/dist/Layer/layerStack.d.ts +98 -0
- package/dist/Layer/layerStack.d.ts.map +1 -0
- package/dist/Layer/layerStack.js +306 -0
- package/dist/Layer/useLayerDismissal.d.ts +93 -0
- package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
- package/dist/Layer/useLayerDismissal.js +91 -0
- package/dist/Lightbox/Lightbox.d.ts +1 -0
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +103 -81
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +38 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +149 -23
- package/dist/MobileNav/MobileNav.d.ts +6 -1
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +61 -36
- package/dist/MultiSelector/MultiSelector.d.ts +15 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +17 -6
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.js +3 -2
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +3 -2
- package/dist/OverflowList/OverflowList.d.ts.map +1 -1
- package/dist/OverflowList/OverflowList.js +3 -2
- package/dist/Overlay/Overlay.d.ts.map +1 -1
- package/dist/Overlay/Overlay.js +3 -2
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +20 -17
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +3 -4
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +3 -2
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +3 -2
- package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
- package/dist/SelectableCard/SelectableCard.js +3 -3
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +5 -0
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +4 -3
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +4 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +3 -3
- package/dist/Stepper/Step.d.ts +140 -0
- package/dist/Stepper/Step.d.ts.map +1 -0
- package/dist/Stepper/Step.js +1045 -0
- package/dist/Stepper/StepStatus.d.ts +24 -0
- package/dist/Stepper/StepStatus.d.ts.map +1 -0
- package/dist/Stepper/StepStatus.js +1 -0
- package/dist/Stepper/Stepper.d.ts +93 -0
- package/dist/Stepper/Stepper.d.ts.map +1 -0
- package/dist/Stepper/Stepper.js +184 -0
- package/dist/Stepper/StepperContext.d.ts +41 -0
- package/dist/Stepper/StepperContext.d.ts.map +1 -0
- package/dist/Stepper/StepperContext.js +34 -0
- package/dist/Stepper/index.d.ts +8 -0
- package/dist/Stepper/index.d.ts.map +1 -0
- package/dist/Stepper/index.js +7 -0
- package/dist/Stepper/stepper.stylex.d.ts +15 -0
- package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
- package/dist/Stepper/stepper.stylex.js +20 -0
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +3 -2
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +5 -1
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +3 -3
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +4 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
- package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
- package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
- package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +19 -5
- package/dist/Table/types.d.ts +22 -4
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +5 -0
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +6 -2
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +3 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +3 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +3 -2
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +5 -3
- package/dist/Token/TokenLink.d.ts.map +1 -1
- package/dist/Token/TokenLink.js +2 -2
- package/dist/Tooltip/Tooltip.d.ts +5 -0
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +5 -0
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +39 -24
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -3
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +3 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +3 -2
- package/dist/astryx.css +43 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useFocusTrap.d.ts +7 -4
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +52 -91
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +10 -3
- package/dist/hooks/useListFocus.d.ts +5 -2
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +12 -6
- package/dist/hooks/useMergedRefs.d.ts +18 -0
- package/dist/hooks/useMergedRefs.d.ts.map +1 -0
- package/dist/hooks/useMergedRefs.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/utils/timeParser.d.ts +1 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +26 -12
- package/locales/en.json +27 -7
- package/locales/pseudo.json +15 -0
- package/package.json +8 -3
- package/src/AppShell/AppShell.tsx +3 -2
- package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
- package/src/AspectRatio/AspectRatio.test.tsx +57 -10
- package/src/AspectRatio/AspectRatio.tsx +40 -1
- package/src/Avatar/Avatar.doc.mjs +2 -1
- package/src/AvatarGroup/AvatarGroup.tsx +3 -2
- package/src/BottomSheet/BottomSheet.test.tsx +123 -0
- package/src/BottomSheet/BottomSheet.tsx +36 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +16 -2
- package/src/BottomSheet/useSheetGestures.test.ts +27 -0
- package/src/BottomSheet/useSheetGestures.ts +25 -5
- package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
- package/src/Button/Button.doc.mjs +22 -0
- package/src/ButtonGroup/ButtonGroup.doc.mjs +7 -1
- package/src/ButtonGroup/ButtonGroup.test.tsx +185 -1
- package/src/ButtonGroup/ButtonGroup.tsx +11 -3
- package/src/Calendar/Calendar.doc.mjs +4 -3
- package/src/Calendar/Calendar.test.tsx +52 -0
- package/src/Calendar/Calendar.tsx +18 -15
- package/src/Calendar/getInitialFocusDate.test.ts +94 -0
- package/src/Calendar/getInitialFocusDate.ts +93 -0
- package/src/Carousel/Carousel.tsx +3 -2
- package/src/Chat/ChatLayout.tsx +3 -2
- package/src/Chat/ChatMessageList.doc.mjs +9 -1
- package/src/Chat/ChatMessageList.test.tsx +46 -0
- package/src/Chat/ChatMessageList.tsx +28 -4
- package/src/CheckboxInput/CheckboxInput.tsx +3 -2
- package/src/ClickableCard/ClickableCard.tsx +3 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
- package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
- package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
- package/src/ContextMenu/ContextMenu.tsx +4 -3
- package/src/DateInput/DateInput.tsx +3 -2
- package/src/DateInput/DateInputTouch.test.tsx +36 -0
- package/src/DateInput/TouchDateField.tsx +35 -1
- package/src/DateTimeInput/DateTimeInput.tsx +2 -2
- package/src/Dialog/Dialog.doc.mjs +8 -3
- package/src/Dialog/Dialog.test.tsx +261 -1
- package/src/Dialog/Dialog.tsx +109 -58
- package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
- package/src/Field/Field.doc.mjs +13 -2
- package/src/FileInput/FileInput.tsx +3 -2
- package/src/Heading/Heading.test.tsx +23 -1
- package/src/Heading/Heading.tsx +6 -2
- package/src/HoverCard/HoverCard.doc.mjs +5 -0
- package/src/HoverCard/HoverCard.tsx +6 -0
- package/src/HoverCard/useHoverCard.tsx +81 -30
- package/src/Item/Item.tsx +5 -4
- package/src/Kbd/Kbd.test.tsx +17 -0
- package/src/Kbd/Kbd.tsx +10 -3
- package/src/Layer/LayerDepthContext.tsx +62 -0
- package/src/Layer/index.ts +9 -0
- package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
- package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
- package/src/Layer/layerStack.ts +362 -0
- package/src/Layer/useLayerDismissal.test.tsx +469 -0
- package/src/Layer/useLayerDismissal.ts +173 -0
- package/src/Layout/Layout.doc.mjs +2 -2
- package/src/Lightbox/Lightbox.doc.mjs +12 -0
- package/src/Lightbox/Lightbox.tsx +124 -102
- package/src/Link/Link.doc.mjs +39 -0
- package/src/Markdown/index.ts +1 -0
- package/src/Markdown/parser.perf.test.ts +71 -1
- package/src/Markdown/parser.test.ts +145 -2
- package/src/Markdown/parser.ts +208 -22
- package/src/MobileNav/MobileNav.tsx +62 -36
- package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
- package/src/MultiSelector/MultiSelector.test.tsx +105 -0
- package/src/MultiSelector/MultiSelector.tsx +50 -6
- package/src/MultiSelector/index.ts +1 -0
- package/src/NavMenu/NavHeadingMenu.tsx +3 -2
- package/src/NumberInput/NumberInput.doc.mjs +5 -0
- package/src/Outline/Outline.tsx +3 -2
- package/src/OverflowList/OverflowList.tsx +3 -2
- package/src/Overlay/Overlay.tsx +3 -2
- package/src/Popover/Popover.test.tsx +27 -1
- package/src/Popover/usePopover.tsx +31 -28
- package/src/PowerSearch/PowerSearch.doc.mjs +26 -0
- package/src/PowerSearch/PowerSearch.tsx +5 -2
- package/src/Resizable/ResizeHandle.tsx +3 -2
- package/src/SegmentedControl/SegmentedControl.tsx +3 -2
- package/src/SelectableCard/SelectableCard.tsx +3 -2
- package/src/Selector/Selector.doc.mjs +12 -1
- package/src/Selector/Selector.test.tsx +21 -0
- package/src/Selector/Selector.tsx +11 -1
- package/src/SideNav/SideNav.doc.mjs +6 -6
- package/src/SideNav/SideNav.tsx +4 -2
- package/src/SideNav/SideNavItem.tsx +4 -2
- package/src/Slider/Slider.tsx +3 -2
- package/src/Stepper/Step.doc.mjs +98 -0
- package/src/Stepper/Step.tsx +1638 -0
- package/src/Stepper/StepStatus.ts +26 -0
- package/src/Stepper/Stepper.doc.mjs +370 -0
- package/src/Stepper/Stepper.test.tsx +1024 -0
- package/src/Stepper/Stepper.tsx +258 -0
- package/src/Stepper/StepperContext.ts +71 -0
- package/src/Stepper/index.ts +18 -0
- package/src/Stepper/stepper.stylex.ts +19 -0
- package/src/Switch/Switch.tsx +3 -2
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.test.tsx +21 -4
- package/src/TabList/TabList.tsx +3 -2
- package/src/Table/BaseTable.tsx +6 -3
- package/src/Table/Table.test.tsx +35 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
- package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
- package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
- package/src/Table/types.ts +22 -4
- package/src/Table/useBaseTablePlugins.ts +5 -0
- package/src/Table/useTableGroupedRows.doc.mjs +5 -4
- package/src/Table/useTablePagination.doc.mjs +7 -0
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/Table/useTableSelection.doc.mjs +31 -0
- package/src/Text/Text.test.tsx +23 -1
- package/src/Text/Text.tsx +6 -2
- package/src/TextArea/TextArea.tsx +3 -2
- package/src/TextInput/TextInput.doc.mjs +10 -0
- package/src/TextInput/TextInput.tsx +3 -2
- package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.tsx +2 -2
- package/src/Timestamp/Timestamp.tsx +4 -2
- package/src/Toast/Toast.doc.mjs +6 -0
- package/src/Token/TokenLink.tsx +2 -2
- package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
- package/src/Toolbar/Toolbar.doc.mjs +5 -0
- package/src/Tooltip/Tooltip.doc.mjs +5 -0
- package/src/Tooltip/Tooltip.test.tsx +32 -0
- package/src/Tooltip/Tooltip.tsx +5 -0
- package/src/Tooltip/useTooltip.tsx +47 -24
- package/src/TopNav/TopNavMegaMenu.tsx +3 -2
- package/src/Typeahead/BaseTypeahead.tsx +3 -2
- package/src/Typeahead/Typeahead.doc.mjs +5 -0
- package/src/Typeahead/Typeahead.tsx +3 -2
- package/src/__tests__/complexComponentContract.test.tsx +81 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useContainerReveal.doc.mjs +2 -2
- package/src/hooks/useFocusTrap.ts +54 -107
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
- package/src/hooks/useHotkeys.test.ts +18 -0
- package/src/hooks/useHotkeys.ts +10 -3
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +65 -3
- package/src/hooks/useListFocus.ts +15 -7
- package/src/hooks/useMergedRefs.doc.mjs +79 -0
- package/src/hooks/useMergedRefs.test.tsx +62 -0
- package/src/hooks/useMergedRefs.ts +36 -0
- package/src/index.ts +1 -0
- package/src/theme/MediaTheme.doc.mjs +5 -5
- package/src/utils/timeParser.test.ts +6 -1
- package/src/utils/timeParser.ts +35 -13
- package/src/__tests__/inputWidthContract.test.tsx +0 -69
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {docs as DropdownMenuDocs} from '../DropdownMenu/DropdownMenu.doc.mjs';
|
|
6
|
+
import {docs as HoverCardDocs} from '../HoverCard/HoverCard.doc.mjs';
|
|
7
|
+
import {docs as LayoutDocs} from '../Layout/Layout.doc.mjs';
|
|
8
|
+
import {docs as LightboxDocs} from '../Lightbox/Lightbox.doc.mjs';
|
|
9
|
+
import {docs as LinkDocs} from '../Link/Link.doc.mjs';
|
|
10
|
+
import {docs as MultiSelectorDocs} from '../MultiSelector/MultiSelector.doc.mjs';
|
|
11
|
+
import {docs as PowerSearchDocs} from '../PowerSearch/PowerSearch.doc.mjs';
|
|
12
|
+
import {docs as TokenizerDocs} from '../Tokenizer/Tokenizer.doc.mjs';
|
|
13
|
+
import {docs as TooltipDocs} from '../Tooltip/Tooltip.doc.mjs';
|
|
14
|
+
import {docs as TypeaheadDocs} from '../Typeahead/Typeahead.doc.mjs';
|
|
15
|
+
|
|
16
|
+
// `docs` is the ComponentDoc union (single vs multi component); narrow it
|
|
17
|
+
// for structural access to either the top-level props or the first
|
|
18
|
+
// sub-component's props.
|
|
19
|
+
function getProps(docs: unknown): {name: string}[] {
|
|
20
|
+
const doc = docs as {
|
|
21
|
+
props?: {name: string}[];
|
|
22
|
+
components?: {props?: {name: string}[]}[];
|
|
23
|
+
};
|
|
24
|
+
return doc.props || doc.components?.[0]?.props || [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('Complex Component API Contract Drift (#4163)', () => {
|
|
28
|
+
it('documents search and tokenizer props across input components', () => {
|
|
29
|
+
const multiProps = getProps(MultiSelectorDocs).map(p => p.name);
|
|
30
|
+
const tokProps = getProps(TokenizerDocs).map(p => p.name);
|
|
31
|
+
const powerProps = getProps(PowerSearchDocs).map(p => p.name);
|
|
32
|
+
const typeProps = getProps(TypeaheadDocs).map(p => p.name);
|
|
33
|
+
|
|
34
|
+
expect(multiProps).toContain('startIcon');
|
|
35
|
+
expect(multiProps).toContain('hasClear');
|
|
36
|
+
expect(multiProps).toContain('isDefaultOpen');
|
|
37
|
+
|
|
38
|
+
expect(tokProps).toContain('startIcon');
|
|
39
|
+
expect(tokProps).toContain('tokenOverflowBehavior');
|
|
40
|
+
|
|
41
|
+
expect(powerProps).toContain('startIcon');
|
|
42
|
+
expect(powerProps).toContain('menuWidth');
|
|
43
|
+
expect(powerProps).toContain('maxOperatorMenuItems');
|
|
44
|
+
expect(powerProps).toContain('tokenOverflowBehavior');
|
|
45
|
+
|
|
46
|
+
expect(typeProps).toContain('startIcon');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('documents Layout and DropdownMenu layout props', () => {
|
|
50
|
+
const layoutProps = getProps(LayoutDocs).map(p => p.name);
|
|
51
|
+
const dropProps = getProps(DropdownMenuDocs).map(p => p.name);
|
|
52
|
+
|
|
53
|
+
expect(layoutProps).toContain('contentWidth');
|
|
54
|
+
expect(layoutProps).toContain('padding');
|
|
55
|
+
expect(layoutProps).toContain('defaultHasDividers');
|
|
56
|
+
|
|
57
|
+
expect(dropProps).toContain('placement');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('documents HoverCard and Tooltip open state props', () => {
|
|
61
|
+
const hoverProps = getProps(HoverCardDocs).map(p => p.name);
|
|
62
|
+
const toolProps = getProps(TooltipDocs).map(p => p.name);
|
|
63
|
+
|
|
64
|
+
expect(hoverProps).toContain('isOpen');
|
|
65
|
+
expect(toolProps).toContain('isOpen');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('documents Link typography and Lightbox gallery props', () => {
|
|
69
|
+
const linkProps = getProps(LinkDocs).map(p => p.name);
|
|
70
|
+
const lightProps = getProps(LightboxDocs).map(p => p.name);
|
|
71
|
+
|
|
72
|
+
expect(linkProps).toContain('size');
|
|
73
|
+
expect(linkProps).toContain('weight');
|
|
74
|
+
expect(linkProps).toContain('color');
|
|
75
|
+
expect(linkProps).toContain('display');
|
|
76
|
+
expect(linkProps).toContain('maxLines');
|
|
77
|
+
|
|
78
|
+
expect(lightProps).toContain('defaultIndex');
|
|
79
|
+
expect(lightProps).toContain('hasAutoPlay');
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type {UseTypeaheadOptions, UseTypeaheadReturn} from './useTypeahead';
|
|
|
56
56
|
|
|
57
57
|
export {useMediaQuery} from './useMediaQuery';
|
|
58
58
|
|
|
59
|
+
export {useMergedRefs} from './useMergedRefs';
|
|
60
|
+
|
|
59
61
|
export {useOverflow} from './useOverflow';
|
|
60
62
|
export type {UseOverflowOptions, UseOverflowReturn} from './useOverflow';
|
|
61
63
|
|
|
@@ -24,7 +24,7 @@ export const docs = {
|
|
|
24
24
|
{
|
|
25
25
|
name: 'getContainerProps',
|
|
26
26
|
type: '(options?: ContainerRevealOptions) => {className?: string; style?: CSSProperties}',
|
|
27
|
-
description: 'Spread onto the container whose hover/focus-within drives the reveal. Accepts hoverDelay (ms the pointer must dwell before the reveal starts
|
|
27
|
+
description: 'Spread onto the container whose hover/focus-within drives the reveal. Accepts hoverDelay (ms the pointer must dwell before the reveal starts: a hover-intent gate like Tooltip\'s and HoverCard\'s delay, so a cursor sweeping across a list leaves nothing painted behind it) and forceState ("active" | "inactive") to pin the trigger state when a caller owns it: a motion gate, a scroll, or a row whose menu is open. "inactive" still yields to keyboard focus and coarse pointers.',
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: 'getContentRevealProps',
|
|
@@ -61,7 +61,7 @@ export const docsDense = {
|
|
|
61
61
|
'options.isEnabled': 'when false hook is inert: no container styles, content getters return no styles, content always shown. Read every render, so it can flip after mount.',
|
|
62
62
|
},
|
|
63
63
|
returnDescriptions: {
|
|
64
|
-
getContainerProps: 'spread onto container whose hover/focus-within drives reveal. Accepts hoverDelay (ms dwell before reveal starts
|
|
64
|
+
getContainerProps: 'spread onto container whose hover/focus-within drives reveal. Accepts hoverDelay (ms dwell before reveal starts: hover-intent gate like Tooltip / HoverCard delay) + forceState ("active" | "inactive") to pin trigger state when a caller owns it. "inactive" yields to keyboard focus + coarse pointers.',
|
|
65
65
|
getContentRevealProps: 'spread onto each revealed / concealed child. Accepts isRevealInverted (conceal-on-hover), isLayoutPreserved (reserve layout box while hidden) + forceVisibility ("shown" | "hidden") to pin this element regardless of container. "hidden" yields to focus.',
|
|
66
66
|
},
|
|
67
67
|
usage: {
|
|
@@ -19,80 +19,26 @@
|
|
|
19
19
|
import {useCallback, useEffect, useRef} from 'react';
|
|
20
20
|
|
|
21
21
|
import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
22
|
-
import {
|
|
22
|
+
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* traps in a shared stack lets only the top-most trap respond to Escape.
|
|
31
|
-
*
|
|
32
|
-
* "Top-most" is resolved by DOM containment first, push order second. Push
|
|
33
|
-
* order alone is not reliable: React runs child effects before parent
|
|
34
|
-
* effects, so when an outer and an inner (DOM-nested) trap mount in the SAME
|
|
35
|
-
* commit, the inner trap pushes first and the outer trap would wrongly win a
|
|
36
|
-
* pure last-pushed comparison.
|
|
37
|
-
*/
|
|
38
|
-
interface EscapeStackEntry {
|
|
39
|
-
handler: () => void;
|
|
40
|
-
getContainer: () => HTMLElement | null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const escapeStack: EscapeStackEntry[] = [];
|
|
44
|
-
|
|
45
|
-
function pushEscapeHandler(entry: EscapeStackEntry): void {
|
|
46
|
-
escapeStack.push(entry);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function removeEscapeHandler(handler: () => void): void {
|
|
50
|
-
for (let i = escapeStack.length - 1; i >= 0; i--) {
|
|
51
|
-
if (escapeStack[i].handler === handler) {
|
|
52
|
-
escapeStack.splice(i, 1);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Resolve the top-most trap: walk the stack in push order, keeping the
|
|
60
|
-
* deepest container by DOM containment. When a later entry's container
|
|
61
|
-
* contains the current candidate's container, the candidate is nested inside
|
|
62
|
-
* it and stays on top; otherwise the later push wins (containment for nested
|
|
63
|
-
* traps, push order as the tiebreaker for unrelated ones).
|
|
64
|
-
*/
|
|
65
|
-
function isTopEscapeHandler(handler: () => void): boolean {
|
|
66
|
-
if (escapeStack.length === 0) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
let top = escapeStack[0];
|
|
70
|
-
for (let i = 1; i < escapeStack.length; i++) {
|
|
71
|
-
const entry = escapeStack[i];
|
|
72
|
-
const topContainer = top.getContainer();
|
|
73
|
-
const entryContainer = entry.getContainer();
|
|
74
|
-
if (
|
|
75
|
-
topContainer != null &&
|
|
76
|
-
entryContainer != null &&
|
|
77
|
-
entryContainer !== topContainer &&
|
|
78
|
-
entryContainer.contains(topContainer)
|
|
79
|
-
) {
|
|
80
|
-
// The current top is nested inside this entry — it stays on top.
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
top = entry;
|
|
84
|
-
}
|
|
85
|
-
return top.handler === handler;
|
|
86
|
-
}
|
|
24
|
+
// Escape-dismissible focus traps currently mounted. This is the whole state
|
|
25
|
+
// behind `hasActiveFocusTrapEscape`, which predates the shared stack and must
|
|
26
|
+
// keep answering about focus traps alone — the stack now carries families that
|
|
27
|
+
// never trapped focus (tooltips, hover cards), and counting those would tell
|
|
28
|
+
// callers a trap is above them when none is.
|
|
29
|
+
let activeEscapeTrapCount = 0;
|
|
87
30
|
|
|
88
31
|
/**
|
|
89
|
-
* Whether
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
32
|
+
* Whether an Escape-dismissible focus trap is currently active — a Popover,
|
|
33
|
+
* menu or other trapped layer that would take an Escape press.
|
|
34
|
+
*
|
|
35
|
+
* @deprecated The focus trap no longer owns Escape coordination — every overlay
|
|
36
|
+
* family shares one stack (`useLayerDismissal`), which routes each press to
|
|
37
|
+
* the top-most layer. A layer that wants the same ordering should join the
|
|
38
|
+
* stack rather than ask whether a trap exists.
|
|
93
39
|
*/
|
|
94
40
|
export function hasActiveFocusTrapEscape(): boolean {
|
|
95
|
-
return
|
|
41
|
+
return activeEscapeTrapCount > 0;
|
|
96
42
|
}
|
|
97
43
|
|
|
98
44
|
/**
|
|
@@ -243,6 +189,38 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
243
189
|
// Track if focus change was triggered by keyboard (Tab key)
|
|
244
190
|
const isKeyboardNavigationRef = useRef(false);
|
|
245
191
|
|
|
192
|
+
// Join the shared layer dismissal stack. The trap no longer listens for
|
|
193
|
+
// Escape itself: the stack owns one listener and routes each press to the
|
|
194
|
+
// top-most layer, so a popover inside a Dialog, a submenu inside a menu, and
|
|
195
|
+
// a modal inside a modal all peel off one at a time. A trap with no
|
|
196
|
+
// `onEscape` is not dismissible and stays off the stack, so a press flows
|
|
197
|
+
// past it to whatever is underneath.
|
|
198
|
+
// One expression drives both the stack registration and the deprecated
|
|
199
|
+
// `hasActiveFocusTrapEscape` count, so the two can never disagree about
|
|
200
|
+
// whether this trap is active.
|
|
201
|
+
const isEscapeTrap = isActive && onEscape != null;
|
|
202
|
+
|
|
203
|
+
useLayerDismissal({
|
|
204
|
+
isActive: isEscapeTrap,
|
|
205
|
+
onDismiss: () => {
|
|
206
|
+
onEscape?.();
|
|
207
|
+
},
|
|
208
|
+
// The trap renders nothing, so it cannot push a depth provider around its
|
|
209
|
+
// content; hand the stack the container instead so two DOM-nested traps
|
|
210
|
+
// still resolve in the right order.
|
|
211
|
+
getContainer: () => containerRef.current,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!isEscapeTrap) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
activeEscapeTrapCount += 1;
|
|
219
|
+
return () => {
|
|
220
|
+
activeEscapeTrapCount -= 1;
|
|
221
|
+
};
|
|
222
|
+
}, [isEscapeTrap]);
|
|
223
|
+
|
|
246
224
|
/**
|
|
247
225
|
* Focus the first focusable element.
|
|
248
226
|
*/
|
|
@@ -353,53 +331,25 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
353
331
|
}, [isActive]);
|
|
354
332
|
|
|
355
333
|
/**
|
|
356
|
-
* Handle Tab key to wrap focus at boundaries
|
|
357
|
-
*
|
|
334
|
+
* Handle Tab key to wrap focus at boundaries. Also tracks that keyboard
|
|
335
|
+
* navigation is occurring.
|
|
336
|
+
*
|
|
337
|
+
* No Escape here, and no IME guard: the shared stack owns the press, claims
|
|
338
|
+
* a composing Escape so no close request follows, and dismisses the trap
|
|
339
|
+
* through `onEscape` above. The trap renders no element of its own, so it
|
|
340
|
+
* has no `cancel` to answer either.
|
|
358
341
|
*/
|
|
359
342
|
useEffect(() => {
|
|
360
343
|
if (!isActive) {
|
|
361
344
|
return;
|
|
362
345
|
}
|
|
363
346
|
|
|
364
|
-
// Register this trap on the shared Escape stack so only the top-most
|
|
365
|
-
// active trap responds to Escape. A stable identity per active period is
|
|
366
|
-
// enough — we push on activate and remove on cleanup. The container is
|
|
367
|
-
// read lazily: the ref may not be attached yet at effect time, and the
|
|
368
|
-
// stack resolves top-most by DOM containment at keydown time.
|
|
369
|
-
const escapeHandler = () => {
|
|
370
|
-
onEscape?.();
|
|
371
|
-
};
|
|
372
|
-
if (onEscape) {
|
|
373
|
-
pushEscapeHandler({
|
|
374
|
-
handler: escapeHandler,
|
|
375
|
-
getContainer: () => containerRef.current,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
347
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
380
348
|
const container = containerRef.current;
|
|
381
349
|
if (!container) {
|
|
382
350
|
return;
|
|
383
351
|
}
|
|
384
352
|
|
|
385
|
-
if (event.key === 'Escape' && onEscape) {
|
|
386
|
-
// Ignore Escape that is cancelling an IME composition, already handled
|
|
387
|
-
// by a nested handler, or not targeting the top-most trap.
|
|
388
|
-
if (
|
|
389
|
-
event.defaultPrevented ||
|
|
390
|
-
isImeKeyEvent(event) ||
|
|
391
|
-
!isTopEscapeHandler(escapeHandler)
|
|
392
|
-
) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// Mark handled and stop propagation so an outer layer (e.g. a Dialog
|
|
396
|
-
// hosting this popover) does not also dismiss on the same press.
|
|
397
|
-
event.preventDefault();
|
|
398
|
-
event.stopPropagation();
|
|
399
|
-
onEscape();
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
353
|
if (event.key === 'Tab') {
|
|
404
354
|
// Mark that keyboard navigation is happening
|
|
405
355
|
isKeyboardNavigationRef.current = true;
|
|
@@ -441,9 +391,6 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
|
|
|
441
391
|
|
|
442
392
|
return () => {
|
|
443
393
|
document.removeEventListener('keydown', handleKeyDown);
|
|
444
|
-
if (onEscape) {
|
|
445
|
-
removeEscapeHandler(escapeHandler);
|
|
446
|
-
}
|
|
447
394
|
};
|
|
448
395
|
}, [isActive, onEscape]);
|
|
449
396
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useFocusTrapEscapeShim.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, useFocusTrap, Dialog, Lightbox,
|
|
6
|
+
* MobileNav, Tooltip, Popover
|
|
7
|
+
* @output Tests that hasActiveFocusTrapEscape still means what it meant in
|
|
8
|
+
* 0.4.2 — an Escape-dismissible FOCUS TRAP is active, not "a layer is open"
|
|
9
|
+
* @position Testing; guards the deprecated public shim in useFocusTrap.ts
|
|
10
|
+
*
|
|
11
|
+
* The shim is public API. Its answer must not change now that Escape moved to
|
|
12
|
+
* the shared stack: `BottomSheetSwitcher` gates its own dismissal on it, so a
|
|
13
|
+
* shim that also counts tooltips, hover cards and dialogs tells the sheet a
|
|
14
|
+
* trap is above it when none is, and the sheet stops closing.
|
|
15
|
+
*
|
|
16
|
+
* SYNC: When useFocusTrap.ts changes, update tests to match new behavior
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
20
|
+
import {render} from '@testing-library/react';
|
|
21
|
+
|
|
22
|
+
import {hasActiveFocusTrapEscape, useFocusTrap} from './useFocusTrap';
|
|
23
|
+
import {Dialog} from '../Dialog/Dialog';
|
|
24
|
+
import {Lightbox} from '../Lightbox/Lightbox';
|
|
25
|
+
import {MobileNav} from '../MobileNav/MobileNav';
|
|
26
|
+
import {Popover} from '../Popover/Popover';
|
|
27
|
+
import {Tooltip} from '../Tooltip/Tooltip';
|
|
28
|
+
import {resetLayerStackForTests} from '../Layer/layerStack';
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
32
|
+
this: HTMLDialogElement,
|
|
33
|
+
) {
|
|
34
|
+
this.setAttribute('open', '');
|
|
35
|
+
});
|
|
36
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
37
|
+
this.removeAttribute('open');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
resetLayerStackForTests();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function Trap({
|
|
46
|
+
isActive = true,
|
|
47
|
+
hasEscape = true,
|
|
48
|
+
children,
|
|
49
|
+
}: {
|
|
50
|
+
isActive?: boolean;
|
|
51
|
+
hasEscape?: boolean;
|
|
52
|
+
children?: React.ReactNode;
|
|
53
|
+
}) {
|
|
54
|
+
const {containerRef} = useFocusTrap<HTMLDivElement>({
|
|
55
|
+
isActive,
|
|
56
|
+
onEscape: hasEscape ? () => {} : undefined,
|
|
57
|
+
});
|
|
58
|
+
return <div ref={containerRef}>{children}</div>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('hasActiveFocusTrapEscape', () => {
|
|
62
|
+
it('is false with nothing mounted', () => {
|
|
63
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('is true for an active trap with an Escape handler', () => {
|
|
67
|
+
render(<Trap />);
|
|
68
|
+
expect(hasActiveFocusTrapEscape()).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('is false for a trap with no Escape handler', () => {
|
|
72
|
+
render(<Trap hasEscape={false} />);
|
|
73
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('is false for an inactive trap', () => {
|
|
77
|
+
render(<Trap isActive={false} />);
|
|
78
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('is true for nested traps, and false again once both unmount', () => {
|
|
82
|
+
const {unmount} = render(
|
|
83
|
+
<Trap>
|
|
84
|
+
<Trap />
|
|
85
|
+
</Trap>,
|
|
86
|
+
);
|
|
87
|
+
expect(hasActiveFocusTrapEscape()).toBe(true);
|
|
88
|
+
unmount();
|
|
89
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('is true for an open Popover, which traps focus', () => {
|
|
93
|
+
render(
|
|
94
|
+
<Popover
|
|
95
|
+
isOpen={true}
|
|
96
|
+
onOpenChange={() => {}}
|
|
97
|
+
content="Popover body"
|
|
98
|
+
label="Popover">
|
|
99
|
+
<button type="button">Open</button>
|
|
100
|
+
</Popover>,
|
|
101
|
+
);
|
|
102
|
+
expect(hasActiveFocusTrapEscape()).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// The families below are all on the shared dismissal stack and none of them
|
|
106
|
+
// traps focus. Answering `true` for these is what broke the bottom sheet.
|
|
107
|
+
it('is false for an open Dialog', () => {
|
|
108
|
+
render(
|
|
109
|
+
<Dialog isOpen={true} onOpenChange={() => {}} aria-label="Dialog">
|
|
110
|
+
Body
|
|
111
|
+
</Dialog>,
|
|
112
|
+
);
|
|
113
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('is false for an open Lightbox', () => {
|
|
117
|
+
render(
|
|
118
|
+
<Lightbox
|
|
119
|
+
isOpen={true}
|
|
120
|
+
onOpenChange={() => {}}
|
|
121
|
+
media={{src: '/photo.jpg', alt: 'A photo'}}
|
|
122
|
+
/>,
|
|
123
|
+
);
|
|
124
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('is false for an open MobileNav', () => {
|
|
128
|
+
render(
|
|
129
|
+
<MobileNav isOpen={true} onOpenChange={() => {}} label="Drawer">
|
|
130
|
+
<span>Nav</span>
|
|
131
|
+
</MobileNav>,
|
|
132
|
+
);
|
|
133
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('is false for a showing Tooltip', () => {
|
|
137
|
+
render(
|
|
138
|
+
<Tooltip content="Tip" isDefaultOpen={true}>
|
|
139
|
+
<button type="button">Trigger</button>
|
|
140
|
+
</Tooltip>,
|
|
141
|
+
);
|
|
142
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('is true for a Popover inside a Dialog, and false once the Popover closes', () => {
|
|
146
|
+
function Scene({isPopoverOpen}: {isPopoverOpen: boolean}) {
|
|
147
|
+
return (
|
|
148
|
+
<Dialog isOpen={true} onOpenChange={() => {}} aria-label="Dialog">
|
|
149
|
+
<Popover
|
|
150
|
+
isOpen={isPopoverOpen}
|
|
151
|
+
onOpenChange={() => {}}
|
|
152
|
+
content="Popover body"
|
|
153
|
+
label="Popover">
|
|
154
|
+
<button type="button">Open</button>
|
|
155
|
+
</Popover>
|
|
156
|
+
</Dialog>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const {rerender} = render(<Scene isPopoverOpen={true} />);
|
|
160
|
+
expect(hasActiveFocusTrapEscape()).toBe(true);
|
|
161
|
+
|
|
162
|
+
rerender(<Scene isPopoverOpen={false} />);
|
|
163
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -67,6 +67,24 @@ describe('useHotkeys', () => {
|
|
|
67
67
|
expect(onPress).toHaveBeenCalledTimes(1);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it('reads a blank userAgentData.platform as unknown, not as non-Apple', () => {
|
|
71
|
+
// Builds that rewrite their client-hints identity expose the key with an
|
|
72
|
+
// empty value; navigator.platform is the only surface left that answers.
|
|
73
|
+
vi.stubGlobal('navigator', {
|
|
74
|
+
userAgentData: {platform: ''},
|
|
75
|
+
platform: 'MacIntel',
|
|
76
|
+
});
|
|
77
|
+
const onPress = vi.fn();
|
|
78
|
+
renderHook(() => useHotkeys([{keys: 'mod+k', onPress}]));
|
|
79
|
+
|
|
80
|
+
press('k', {ctrlKey: true});
|
|
81
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
82
|
+
|
|
83
|
+
const event = press('k', {metaKey: true});
|
|
84
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(onPress).toHaveBeenCalledWith(event);
|
|
86
|
+
});
|
|
87
|
+
|
|
70
88
|
it('does not fire a bare key when modifiers are held', () => {
|
|
71
89
|
stubApplePlatform();
|
|
72
90
|
const onPress = vi.fn();
|
package/src/hooks/useHotkeys.ts
CHANGED
|
@@ -73,8 +73,9 @@ const KEY_ALIASES: Record<string, string> = {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Detects whether the current platform is macOS/iOS.
|
|
76
|
-
* Prefers the User-Agent Client Hints API when
|
|
77
|
-
* falls back to navigator.platform (deprecated but universally
|
|
76
|
+
* Prefers the User-Agent Client Hints API when it names a platform (modern
|
|
77
|
+
* Chrome/Edge), falls back to navigator.platform (deprecated but universally
|
|
78
|
+
* supported) when it is absent or blank.
|
|
78
79
|
* Mirrors the detection used by Kbd so displayed and handled shortcuts agree.
|
|
79
80
|
*/
|
|
80
81
|
function isApplePlatform(): boolean {
|
|
@@ -83,7 +84,13 @@ function isApplePlatform(): boolean {
|
|
|
83
84
|
}
|
|
84
85
|
const uaData = 'userAgentData' in navigator ? navigator.userAgentData : null;
|
|
85
86
|
if (uaData && typeof uaData === 'object' && 'platform' in uaData) {
|
|
86
|
-
|
|
87
|
+
const uaPlatform = (uaData as {platform?: unknown}).platform;
|
|
88
|
+
// A blank platform is no answer, not a negative one. Builds that rewrite
|
|
89
|
+
// their client-hints identity ship '', so fall through rather than
|
|
90
|
+
// reading it as "not Apple".
|
|
91
|
+
if (typeof uaPlatform === 'string' && uaPlatform.trim() !== '') {
|
|
92
|
+
return /mac/i.test(uaPlatform);
|
|
93
|
+
}
|
|
87
94
|
}
|
|
88
95
|
return /Mac|iPhone|iPad|iPod/.test(navigator.platform ?? '');
|
|
89
96
|
}
|
|
@@ -36,7 +36,7 @@ export const docs = {
|
|
|
36
36
|
{
|
|
37
37
|
name: 'options.onEscape',
|
|
38
38
|
type: '() => void',
|
|
39
|
-
description: 'Callback when Escape key is pressed (e.g., close menu).',
|
|
39
|
+
description: 'Callback when Escape key is pressed (e.g., close menu). Supplying it also consumes the key (preventDefault); without it Escape passes through to the surrounding layer.',
|
|
40
40
|
required: false,
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -144,7 +144,7 @@ export const docsDense = {
|
|
|
144
144
|
'options.itemSelector': 'selector for focusable items in list.',
|
|
145
145
|
'options.boundarySelector': "boundary selector for lists that contain nested lists (e.g. submenu flyouts); scopes items + key handling to this level.",
|
|
146
146
|
'options.wrap': 'whether arrow navigation wraps around at ends.',
|
|
147
|
-
'options.onEscape': 'callback when Escape key pressed (e.g. close menu).',
|
|
147
|
+
'options.onEscape': 'callback when Escape key pressed (e.g. close menu). Also consumes the key; without it Escape passes through to the surrounding layer.',
|
|
148
148
|
'options.orientation': "navigation orientation. 'horizontal' uses ArrowLeft/ArrowRight, 'vertical' uses ArrowUp/ArrowDown, 'both' accepts all four arrows.",
|
|
149
149
|
'options.hasHomeEnd': 'whether Home/End jump to first/last enabled item.',
|
|
150
150
|
'options.isRtl': 'ArrowLeft/ArrowRight swap for horizontal nav (RTL). default: auto-detect from container computed direction; explicit boolean wins.',
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file useListFocus.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, useListFocus hook
|
|
6
|
-
* @output Unit tests for useListFocus disabled-item skipping, navigation,
|
|
7
|
-
* RTL auto-detection
|
|
6
|
+
* @output Unit tests for useListFocus disabled-item skipping, navigation,
|
|
7
|
+
* Escape consumption, and RTL auto-detection
|
|
8
8
|
* @position Testing; validates useListFocus.ts keyboard navigation
|
|
9
9
|
*
|
|
10
10
|
* SYNC: When useListFocus.ts changes, update tests to match new behavior
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {describe, it, expect} from 'vitest';
|
|
13
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
14
14
|
import type {KeyboardEvent as ReactKeyboardEvent} from 'react';
|
|
15
15
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
16
16
|
import {useListFocus} from './useListFocus';
|
|
@@ -525,3 +525,65 @@ describe('useListFocus boundarySelector (nested lists)', () => {
|
|
|
525
525
|
expect(innerProbe).toHaveAttribute('data-owns', 'false');
|
|
526
526
|
});
|
|
527
527
|
});
|
|
528
|
+
|
|
529
|
+
// A list inside a host that dismisses on Escape. The host's guard mirrors
|
|
530
|
+
// `useFocusTrap`: it acts only on a key no inner handler has consumed.
|
|
531
|
+
function EscapeHost({
|
|
532
|
+
onEscape,
|
|
533
|
+
onHostEscape,
|
|
534
|
+
}: {
|
|
535
|
+
onEscape?: () => void;
|
|
536
|
+
onHostEscape: () => void;
|
|
537
|
+
}) {
|
|
538
|
+
const {listRef, handleKeyDown} = useListFocus<HTMLDivElement>({onEscape});
|
|
539
|
+
return (
|
|
540
|
+
<div
|
|
541
|
+
data-testid="host"
|
|
542
|
+
onKeyDown={e => {
|
|
543
|
+
if (e.key === 'Escape' && !e.defaultPrevented) {
|
|
544
|
+
onHostEscape();
|
|
545
|
+
}
|
|
546
|
+
}}>
|
|
547
|
+
<div ref={listRef} role="menu" onKeyDown={handleKeyDown}>
|
|
548
|
+
<div role="menuitem" tabIndex={-1} data-testid="One">
|
|
549
|
+
One
|
|
550
|
+
</div>
|
|
551
|
+
<div role="menuitem" tabIndex={-1} data-testid="Two">
|
|
552
|
+
Two
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
describe('useListFocus Escape', () => {
|
|
560
|
+
it('leaves Escape to the host when no onEscape is supplied', () => {
|
|
561
|
+
const onHostEscape = vi.fn();
|
|
562
|
+
render(<EscapeHost onHostEscape={onHostEscape} />);
|
|
563
|
+
|
|
564
|
+
fireEvent.keyDown(screen.getByRole('menu'), {key: 'Escape'});
|
|
565
|
+
expect(onHostEscape).toHaveBeenCalledTimes(1);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('consumes Escape and runs onEscape when one is supplied', () => {
|
|
569
|
+
const onEscape = vi.fn();
|
|
570
|
+
const onHostEscape = vi.fn();
|
|
571
|
+
render(<EscapeHost onEscape={onEscape} onHostEscape={onHostEscape} />);
|
|
572
|
+
|
|
573
|
+
fireEvent.keyDown(screen.getByRole('menu'), {key: 'Escape'});
|
|
574
|
+
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
575
|
+
expect(onHostEscape).not.toHaveBeenCalled();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('still consumes arrow keys with no onEscape (page-scroll suppression)', () => {
|
|
579
|
+
render(<EscapeHost onHostEscape={() => {}} />);
|
|
580
|
+
screen.getByTestId('One').focus();
|
|
581
|
+
|
|
582
|
+
// fireEvent returns false when a handler cancelled the event.
|
|
583
|
+
const wasCancelled = !fireEvent.keyDown(screen.getByRole('menu'), {
|
|
584
|
+
key: 'ArrowDown',
|
|
585
|
+
});
|
|
586
|
+
expect(wasCancelled).toBe(true);
|
|
587
|
+
expect(screen.getByTestId('Two')).toHaveFocus();
|
|
588
|
+
});
|
|
589
|
+
});
|