@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
|
@@ -15,7 +15,7 @@ export const docs = {
|
|
|
15
15
|
},
|
|
16
16
|
theming: {
|
|
17
17
|
targets: [
|
|
18
|
-
{className: 'astryx-button-group', visualProps: ['size', 'orientation']},
|
|
18
|
+
{className: 'astryx-button-group', visualProps: ['size', 'orientation', 'elevation']},
|
|
19
19
|
],
|
|
20
20
|
},
|
|
21
21
|
components: [
|
|
@@ -46,6 +46,8 @@ export const docs = {
|
|
|
46
46
|
{guidance: false, description: "Don't mix wildly different actions. A Save button next to a Delete button in the same group is confusing."},
|
|
47
47
|
{guidance: false, description: "Don't use ButtonGroup for navigation. Use SegmentedControl or TabList for switching between views."},
|
|
48
48
|
{guidance: false, description: "Don't nest ButtonGroups. If you need multiple groups, place them side by side with a gap."},
|
|
49
|
+
{guidance: true, description: 'Name the group for what its buttons act on. The label is the group\u2019s accessible name and a screen reader reads it before each member.'},
|
|
50
|
+
{guidance: false, description: "Don't disable the group to show that an action is in flight. A disabled member drops focus, so a keyboard user loses their place; leave the group enabled and show progress on the button that started the work."},
|
|
49
51
|
],
|
|
50
52
|
anatomy: [
|
|
51
53
|
{name: 'Button', required: true, description: 'One or more Button or IconButton children that form the connected group.'},
|
|
@@ -84,6 +86,8 @@ export const docsZh = {
|
|
|
84
86
|
{guidance: false, description: '不要混合差异很大的操作,将保存按钮和删除按钮放在同一组中会令人困惑。'},
|
|
85
87
|
{guidance: false, description: '不要使用 ButtonGroup 进行导航,使用 SegmentedControl 或 TabList 切换视图。'},
|
|
86
88
|
{guidance: false, description: '不要嵌套 ButtonGroup。如需多个组,请并排放置并留有间隔。'},
|
|
89
|
+
{guidance: true, description: '按钮组的标签应说明这些按钮作用于什么。该标签是按钮组的无障碍名称,屏幕阅读器会在每个成员之前朗读它。'},
|
|
90
|
+
{guidance: false, description: '不要用禁用整个按钮组来表示操作进行中。禁用的成员会失去焦点,键盘用户会丢失位置;请保持按钮组可用,并在发起操作的按钮上显示进度。'},
|
|
87
91
|
],
|
|
88
92
|
anatomy: [
|
|
89
93
|
{name: '按钮', required: true, description: '一个或多个 Button 或 IconButton 子元素,形成连接的组。'},
|
|
@@ -105,6 +109,8 @@ export const docsDense = {
|
|
|
105
109
|
{guidance: false, description: "Don't mix unrelated actions in one group."},
|
|
106
110
|
{guidance: false, description: "Don't use for navigation. Use SegmentedControl or TabList."},
|
|
107
111
|
{guidance: false, description: "Don't nest ButtonGroups."},
|
|
112
|
+
{guidance: true, description: 'Label the group for what its buttons act on; it is the accessible name.'},
|
|
113
|
+
{guidance: false, description: "Don't disable the group for an in-flight action; a disabled member drops focus."},
|
|
108
114
|
],
|
|
109
115
|
},
|
|
110
116
|
components: [
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import {describe, it, expect} from 'vitest';
|
|
15
|
-
import {render, screen} from '@testing-library/react';
|
|
15
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
16
|
+
import userEvent from '@testing-library/user-event';
|
|
16
17
|
import {transformSync} from '@babel/core';
|
|
17
18
|
import stylexBabelPlugin from '@stylexjs/babel-plugin';
|
|
18
19
|
import {readFileSync} from 'node:fs';
|
|
@@ -611,5 +612,188 @@ describe('ButtonGroup', () => {
|
|
|
611
612
|
none.querySelector('[role="group"]')!.className,
|
|
612
613
|
);
|
|
613
614
|
});
|
|
615
|
+
|
|
616
|
+
it('exposes the elevation to a theme as a data attribute', () => {
|
|
617
|
+
render(
|
|
618
|
+
<ButtonGroup label="Actions" elevation="high">
|
|
619
|
+
<Button label="One" />
|
|
620
|
+
</ButtonGroup>,
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
expect(screen.getByRole('group')).toHaveAttribute(
|
|
624
|
+
'data-elevation',
|
|
625
|
+
'high',
|
|
626
|
+
);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// ===========================================================================
|
|
631
|
+
// Keyboard navigation
|
|
632
|
+
//
|
|
633
|
+
// The group wires useListFocus, so arrow keys move focus between members.
|
|
634
|
+
// The last two tests cover the boundary: a member can own a layer, whose
|
|
635
|
+
// keys belong to that layer, and the group itself can sit inside a layer,
|
|
636
|
+
// whose keys are still the group's own. Both must hold at once.
|
|
637
|
+
// ===========================================================================
|
|
638
|
+
describe('keyboard navigation', () => {
|
|
639
|
+
const clipboard = (
|
|
640
|
+
<ButtonGroup label="Actions">
|
|
641
|
+
<Button label="Copy" />
|
|
642
|
+
<Button label="Cut" />
|
|
643
|
+
<Button label="Paste" />
|
|
644
|
+
</ButtonGroup>
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
it('moves focus between members with the arrow keys, wrapping at the end', async () => {
|
|
648
|
+
const user = userEvent.setup();
|
|
649
|
+
render(clipboard);
|
|
650
|
+
|
|
651
|
+
screen.getByRole('button', {name: 'Copy'}).focus();
|
|
652
|
+
|
|
653
|
+
await user.keyboard('{ArrowRight}');
|
|
654
|
+
expect(screen.getByRole('button', {name: 'Cut'})).toHaveFocus();
|
|
655
|
+
|
|
656
|
+
await user.keyboard('{ArrowRight}{ArrowRight}');
|
|
657
|
+
expect(screen.getByRole('button', {name: 'Copy'})).toHaveFocus();
|
|
658
|
+
|
|
659
|
+
await user.keyboard('{ArrowLeft}');
|
|
660
|
+
expect(screen.getByRole('button', {name: 'Paste'})).toHaveFocus();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('jumps to the first and last member with Home and End', async () => {
|
|
664
|
+
const user = userEvent.setup();
|
|
665
|
+
render(clipboard);
|
|
666
|
+
|
|
667
|
+
screen.getByRole('button', {name: 'Cut'}).focus();
|
|
668
|
+
|
|
669
|
+
await user.keyboard('{End}');
|
|
670
|
+
expect(screen.getByRole('button', {name: 'Paste'})).toHaveFocus();
|
|
671
|
+
|
|
672
|
+
await user.keyboard('{Home}');
|
|
673
|
+
expect(screen.getByRole('button', {name: 'Copy'})).toHaveFocus();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('uses the vertical arrows when the group is vertical', async () => {
|
|
677
|
+
const user = userEvent.setup();
|
|
678
|
+
render(
|
|
679
|
+
<ButtonGroup label="Actions" orientation="vertical">
|
|
680
|
+
<Button label="Copy" />
|
|
681
|
+
<Button label="Cut" />
|
|
682
|
+
</ButtonGroup>,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
screen.getByRole('button', {name: 'Copy'}).focus();
|
|
686
|
+
|
|
687
|
+
await user.keyboard('{ArrowDown}');
|
|
688
|
+
expect(screen.getByRole('button', {name: 'Cut'})).toHaveFocus();
|
|
689
|
+
|
|
690
|
+
await user.keyboard('{ArrowUp}');
|
|
691
|
+
expect(screen.getByRole('button', {name: 'Copy'})).toHaveFocus();
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('follows visual direction in RTL', async () => {
|
|
695
|
+
const user = userEvent.setup();
|
|
696
|
+
// dir sits on the group because jsdom resolves computed direction from
|
|
697
|
+
// the element's own attribute, not from an ancestor.
|
|
698
|
+
render(
|
|
699
|
+
<ButtonGroup label="Actions" dir="rtl">
|
|
700
|
+
<Button label="Copy" />
|
|
701
|
+
<Button label="Cut" />
|
|
702
|
+
<Button label="Paste" />
|
|
703
|
+
</ButtonGroup>,
|
|
704
|
+
);
|
|
705
|
+
screen.getByRole('button', {name: 'Copy'}).focus();
|
|
706
|
+
|
|
707
|
+
await user.keyboard('{ArrowLeft}');
|
|
708
|
+
expect(screen.getByRole('button', {name: 'Cut'})).toHaveFocus();
|
|
709
|
+
|
|
710
|
+
await user.keyboard('{ArrowRight}');
|
|
711
|
+
expect(screen.getByRole('button', {name: 'Copy'})).toHaveFocus();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('skips a disabled member', async () => {
|
|
715
|
+
const user = userEvent.setup();
|
|
716
|
+
render(
|
|
717
|
+
<ButtonGroup label="Actions">
|
|
718
|
+
<Button label="Copy" />
|
|
719
|
+
<Button label="Cut" isDisabled />
|
|
720
|
+
<Button label="Paste" />
|
|
721
|
+
</ButtonGroup>,
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
screen.getByRole('button', {name: 'Copy'}).focus();
|
|
725
|
+
|
|
726
|
+
await user.keyboard('{ArrowRight}');
|
|
727
|
+
expect(screen.getByRole('button', {name: 'Paste'})).toHaveFocus();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('leaves arrow keys to a member\u2019s open menu', async () => {
|
|
731
|
+
const user = userEvent.setup();
|
|
732
|
+
render(
|
|
733
|
+
<ButtonGroup label="Approve action">
|
|
734
|
+
<Button label="Allow once" />
|
|
735
|
+
<DropdownMenu
|
|
736
|
+
hasChevron={false}
|
|
737
|
+
button={{label: 'Allow options', isIconOnly: true}}
|
|
738
|
+
items={[{label: 'Allow for 30 minutes'}, {label: 'Always allow'}]}
|
|
739
|
+
/>
|
|
740
|
+
</ButtonGroup>,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
await user.click(screen.getByRole('button', {name: /Allow options/}));
|
|
744
|
+
const item = screen.getByRole('menuitem', {
|
|
745
|
+
name: 'Allow for 30 minutes',
|
|
746
|
+
hidden: true,
|
|
747
|
+
});
|
|
748
|
+
item.focus();
|
|
749
|
+
|
|
750
|
+
// Assert positively: the menu item keeps focus. `.not.toHaveFocus()` on
|
|
751
|
+
// the group's buttons would also pass if the group had thrown focus
|
|
752
|
+
// somewhere else entirely.
|
|
753
|
+
fireEvent.keyDown(item, {key: 'ArrowRight'});
|
|
754
|
+
expect(item).toHaveFocus();
|
|
755
|
+
|
|
756
|
+
fireEvent.keyDown(item, {key: 'ArrowLeft'});
|
|
757
|
+
expect(item).toHaveFocus();
|
|
758
|
+
|
|
759
|
+
// End belongs to the menu's own list focus, so it lands on the last menu
|
|
760
|
+
// item rather than the group's last button.
|
|
761
|
+
fireEvent.keyDown(item, {key: 'End'});
|
|
762
|
+
expect(
|
|
763
|
+
screen.getByRole('menuitem', {name: 'Always allow', hidden: true}),
|
|
764
|
+
).toHaveFocus();
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('still moves between its own members when the group sits inside a popover', async () => {
|
|
768
|
+
const user = userEvent.setup();
|
|
769
|
+
// A group rendered inside a Popover, ContextMenu, HoverCard or Toast has
|
|
770
|
+
// a `[popover]` ancestor, because that is how useLayer mounts a layer.
|
|
771
|
+
// A bare `popover` div is the same DOM fact without the layer harness.
|
|
772
|
+
render(
|
|
773
|
+
<div popover="manual">
|
|
774
|
+
<ButtonGroup label="Actions">
|
|
775
|
+
<Button label="Copy" />
|
|
776
|
+
<Button label="Cut" />
|
|
777
|
+
<Button label="Paste" />
|
|
778
|
+
</ButtonGroup>
|
|
779
|
+
</div>,
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
// A closed popover is display:none in jsdom, so its contents read as
|
|
783
|
+
// inaccessible; focus and key handling still work on them.
|
|
784
|
+
const button = (name: string) =>
|
|
785
|
+
screen.getByRole('button', {name, hidden: true});
|
|
786
|
+
|
|
787
|
+
button('Copy').focus();
|
|
788
|
+
|
|
789
|
+
await user.keyboard('{ArrowRight}');
|
|
790
|
+
expect(button('Cut')).toHaveFocus();
|
|
791
|
+
|
|
792
|
+
await user.keyboard('{End}');
|
|
793
|
+
expect(button('Paste')).toHaveFocus();
|
|
794
|
+
|
|
795
|
+
await user.keyboard('{Home}');
|
|
796
|
+
expect(button('Copy')).toHaveFocus();
|
|
797
|
+
});
|
|
614
798
|
});
|
|
615
799
|
});
|
|
@@ -28,12 +28,13 @@ import {radiusVars, shadowVars} from '../theme/tokens.stylex';
|
|
|
28
28
|
import type {Elevation} from '../utils/types';
|
|
29
29
|
import {SizeProvider, useSize} from '../SizeContext/SizeContext';
|
|
30
30
|
import {useListFocus} from '../hooks/useListFocus';
|
|
31
|
-
import {mergeProps,
|
|
31
|
+
import {mergeProps, composeEventHandlers} from '../utils';
|
|
32
32
|
import type {BaseProps} from '../BaseProps';
|
|
33
33
|
import {ButtonGroupContext} from './ButtonGroupContext';
|
|
34
34
|
import type {ButtonGroupOrientation} from './ButtonGroupContext';
|
|
35
35
|
import {themeProps} from '../utils/themeProps';
|
|
36
36
|
|
|
37
|
+
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
37
38
|
// =============================================================================
|
|
38
39
|
// Props
|
|
39
40
|
// =============================================================================
|
|
@@ -172,6 +173,13 @@ export function ButtonGroup({
|
|
|
172
173
|
|
|
173
174
|
const {listRef, handleKeyDown} = useListFocus<HTMLDivElement>({
|
|
174
175
|
itemSelector: 'button, [tabindex="0"]',
|
|
176
|
+
// A member's layer renders inline inside the group (useLayer does not
|
|
177
|
+
// portal it), so a key pressed in an open DropdownMenu bubbles here. The
|
|
178
|
+
// boundary is the group's own root or any layer, whichever is nearer to
|
|
179
|
+
// the target. Naming the group's own root is what keeps a group rendered
|
|
180
|
+
// inside a Popover or Toast working: bailing on any [popover] ancestor
|
|
181
|
+
// instead would swallow every arrow key it owns.
|
|
182
|
+
boundarySelector: '[role="group"], [popover]',
|
|
175
183
|
orientation,
|
|
176
184
|
});
|
|
177
185
|
|
|
@@ -184,10 +192,10 @@ export function ButtonGroup({
|
|
|
184
192
|
<ButtonGroupContext value={contextValue}>
|
|
185
193
|
<SizeProvider value={size}>
|
|
186
194
|
<div
|
|
187
|
-
ref={
|
|
195
|
+
ref={useMergedRefs(ref, listRef)}
|
|
188
196
|
{...props}
|
|
189
197
|
{...mergeProps(
|
|
190
|
-
themeProps('button-group', {size, orientation}),
|
|
198
|
+
themeProps('button-group', {size, orientation, elevation}),
|
|
191
199
|
stylex.props(
|
|
192
200
|
styles.group,
|
|
193
201
|
orientation === 'vertical' && styles.vertical,
|
|
@@ -83,7 +83,8 @@ export const docs = {
|
|
|
83
83
|
{
|
|
84
84
|
name: 'focusDate',
|
|
85
85
|
type: 'ISODateString',
|
|
86
|
-
description:
|
|
86
|
+
description:
|
|
87
|
+
'Controlled visible month. Unset, the calendar opens on the selected date, else on today clamped into the min/max window.',
|
|
87
88
|
},
|
|
88
89
|
{
|
|
89
90
|
name: 'onFocusDateChange',
|
|
@@ -156,7 +157,7 @@ export const docsZh = {
|
|
|
156
157
|
{name: 'dateConstraints', type: 'Array<(date: Date) => boolean>', description: '自定义约束函数。'},
|
|
157
158
|
{name: 'maxRangeSpan', type: 'number', description: '范围模式:范围最多可跨越的天数,含首尾两端(7 = 7 天窗口)。选定起始日后限制窗口大小。'},
|
|
158
159
|
{name: 'minRangeSpan', type: 'number', description: '范围模式:范围最少需跨越的天数,含首尾两端(2 表示禁止单日范围)。默认为 1。'},
|
|
159
|
-
{name: 'focusDate', type: 'ISODateString', description: '
|
|
160
|
+
{name: 'focusDate', type: 'ISODateString', description: '受控可见月份。未设置时,日历打开时显示已选日期所在月份;若无选中值,则显示今天,并将其限制在 min/max 范围内。'},
|
|
160
161
|
{name: 'onFocusDateChange', type: '(focusDate: ISODateString) => void', description: '导航回调函数。'},
|
|
161
162
|
{name: 'handleRef', type: 'React.Ref<CalendarHandle>', description: '日历导航的命令式句柄,包括 navigateTo()。'},
|
|
162
163
|
{name: 'hasOutsideDays', type: 'boolean', description: '显示相邻月份的日期。', default: 'true'},
|
|
@@ -221,7 +222,7 @@ export const docsDense = {
|
|
|
221
222
|
dateConstraints: 'custom constraint fns',
|
|
222
223
|
maxRangeSpan: 'range mode: max days a range may span, both ends counted (7 = 7-day window)',
|
|
223
224
|
minRangeSpan: 'range mode: min days a range must span, both ends counted (default 1)',
|
|
224
|
-
focusDate: 'controlled visible month',
|
|
225
|
+
focusDate: 'controlled visible month (default: selected date, else today clamped into min/max)',
|
|
225
226
|
onFocusDateChange: 'navigation callback',
|
|
226
227
|
handleRef: 'imperative navigation handle',
|
|
227
228
|
hasOutsideDays: 'show days from adjacent months',
|
|
@@ -263,6 +263,58 @@ describe('Calendar', () => {
|
|
|
263
263
|
expect(day15).not.toBeDisabled();
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
// ─── Initial Visible Month ───────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe('opens on a month inside the min/max window', () => {
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
vi.useFakeTimers({toFake: ['Date']});
|
|
271
|
+
vi.setSystemTime(new Date(2026, 7, 21, 12, 0, 0));
|
|
272
|
+
});
|
|
273
|
+
afterEach(() => {
|
|
274
|
+
vi.useRealTimers();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('opens on today when today is inside the window', () => {
|
|
278
|
+
render(<Calendar min="2026-01-01" max="2026-12-31" />);
|
|
279
|
+
|
|
280
|
+
expect(screen.getByText('August 2026')).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('opens on min when the window is entirely in the future', () => {
|
|
284
|
+
render(<Calendar min="2027-03-04" max="2027-06-30" />);
|
|
285
|
+
|
|
286
|
+
expect(screen.getByText('March 2027')).toBeInTheDocument();
|
|
287
|
+
expect(getDayButton(4, 'March', 2027)).not.toBeDisabled();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('opens on max when the window is entirely in the past', () => {
|
|
291
|
+
render(<Calendar min="2019-01-01" max="2019-04-30" />);
|
|
292
|
+
|
|
293
|
+
expect(screen.getByText('April 2019')).toBeInTheDocument();
|
|
294
|
+
expect(getDayButton(30, 'April', 2019)).not.toBeDisabled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('keeps max in the last pane in the two-month layout', () => {
|
|
298
|
+
render(<Calendar numberOfMonths={2} min="2019-01-01" max="2019-04-30" />);
|
|
299
|
+
|
|
300
|
+
expect(screen.getByText('March 2019 – April 2019')).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('still honors an explicit focusDate outside the window', () => {
|
|
304
|
+
render(
|
|
305
|
+
<Calendar focusDate="2026-01-01" min="2027-03-04" max="2027-06-30" />,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('still opens on the selected value outside the window', () => {
|
|
312
|
+
render(<Calendar defaultValue="2031-07-04" min="2019-01-01" />);
|
|
313
|
+
|
|
314
|
+
expect(screen.getByText('July 2031')).toBeInTheDocument();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
266
318
|
it('respects custom dateConstraints', () => {
|
|
267
319
|
// Only allow weekdays
|
|
268
320
|
const isWeekday = (date: Date) => {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* SYNC: When modified, update these files to stay in sync:
|
|
13
13
|
* - /packages/core/src/Calendar/Calendar.doc.mjs (props table, features, implementation notes)
|
|
14
|
+
* - /packages/core/src/Calendar/getInitialFocusDate.ts (which month the calendar opens on)
|
|
14
15
|
* - /packages/core/src/Calendar/Calendar.test.tsx (tests for new/changed behavior)
|
|
15
16
|
* - /packages/core/src/Calendar/index.ts (exports if types change)
|
|
16
17
|
* - /apps/storybook/stories/Calendar.stories.tsx (storybook stories)
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
} from '../utils/plainDate';
|
|
61
62
|
import {mergeProps, composeEventHandlers, rtlStyles} from '../utils';
|
|
62
63
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
64
|
+
import {getInitialFocusDate} from './getInitialFocusDate';
|
|
63
65
|
import {
|
|
64
66
|
computeDayCellState,
|
|
65
67
|
computeRangeRounding,
|
|
@@ -142,7 +144,9 @@ interface CalendarBaseProps extends Omit<
|
|
|
142
144
|
|
|
143
145
|
/**
|
|
144
146
|
* Controlled focus date (which month is visible).
|
|
145
|
-
* If not provided, defaults to selected date
|
|
147
|
+
* If not provided, defaults to the selected date, else today clamped into
|
|
148
|
+
* the `min`/`max` window (so a window that excludes today opens on the
|
|
149
|
+
* bound nearest to it, not on an all-disabled month).
|
|
146
150
|
*/
|
|
147
151
|
focusDate?: ISODateString;
|
|
148
152
|
|
|
@@ -277,20 +281,19 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
277
281
|
// Determine effective value
|
|
278
282
|
const effectiveValue = value !== undefined ? value : internalValue;
|
|
279
283
|
|
|
280
|
-
// Focus date state (which month is visible)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
});
|
|
284
|
+
// Focus date state (which month is visible). Falls back to today, clamped
|
|
285
|
+
// into the min/max window so a window that doesn't contain today doesn't
|
|
286
|
+
// open on an all-disabled month.
|
|
287
|
+
const [internalFocusDate, setInternalFocusDate] = useState<PlainDate>(() =>
|
|
288
|
+
getInitialFocusDate({
|
|
289
|
+
focusDate: focusDateProp,
|
|
290
|
+
value: effectiveValue,
|
|
291
|
+
min,
|
|
292
|
+
max,
|
|
293
|
+
numberOfMonths,
|
|
294
|
+
today,
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
294
297
|
|
|
295
298
|
// Use controlled focusDate if callback is provided, otherwise use internal state
|
|
296
299
|
const isControlledFocus =
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file getInitialFocusDate.test.ts
|
|
5
|
+
* @input Uses vitest
|
|
6
|
+
* @output Test suite for getInitialFocusDate
|
|
7
|
+
* @position Tests for getInitialFocusDate.ts
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When getInitialFocusDate.ts changes, update tests accordingly
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect} from 'vitest';
|
|
13
|
+
import {getInitialFocusDate} from './getInitialFocusDate';
|
|
14
|
+
import {plainDateFromISO, plainDateToISO} from '../utils/plainDate';
|
|
15
|
+
|
|
16
|
+
const TODAY = plainDateFromISO('2026-08-21');
|
|
17
|
+
|
|
18
|
+
function focusISO(options: Partial<Parameters<typeof getInitialFocusDate>[0]>) {
|
|
19
|
+
return plainDateToISO(
|
|
20
|
+
getInitialFocusDate({numberOfMonths: 1, today: TODAY, ...options}),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('getInitialFocusDate', () => {
|
|
25
|
+
it('opens on today when there are no bounds', () => {
|
|
26
|
+
expect(focusISO({})).toBe('2026-08-21');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('opens on today when today is inside the min/max window', () => {
|
|
30
|
+
expect(focusISO({min: '2026-01-01', max: '2026-12-31'})).toBe('2026-08-21');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('treats the bounds as inclusive', () => {
|
|
34
|
+
expect(focusISO({min: '2026-08-21'})).toBe('2026-08-21');
|
|
35
|
+
expect(focusISO({max: '2026-08-21'})).toBe('2026-08-21');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('opens on min when the whole window is in the future', () => {
|
|
39
|
+
expect(focusISO({min: '2027-03-04', max: '2027-06-30'})).toBe('2027-03-04');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('opens on max when the whole window is in the past', () => {
|
|
43
|
+
expect(focusISO({min: '2019-01-01', max: '2019-04-30'})).toBe('2019-04-01');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('clamps against a one-sided min', () => {
|
|
47
|
+
expect(focusISO({min: '2030-05-17'})).toBe('2030-05-17');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('clamps against a one-sided max', () => {
|
|
51
|
+
expect(focusISO({max: '2020-02-09'})).toBe('2020-02-01');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('lands max in the last pane in the two-month layout', () => {
|
|
55
|
+
// Opening on max's month would spend the second pane entirely
|
|
56
|
+
// out of bounds, so the window's end sits on the right instead.
|
|
57
|
+
expect(focusISO({max: '2020-02-09', numberOfMonths: 2})).toBe('2020-01-01');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not shift the two-month layout back past min', () => {
|
|
61
|
+
expect(
|
|
62
|
+
focusISO({min: '2020-02-03', max: '2020-02-09', numberOfMonths: 2}),
|
|
63
|
+
).toBe('2020-02-03');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('prefers the selected value over the bounds', () => {
|
|
67
|
+
expect(focusISO({value: '2031-07-04', min: '2019-01-01'})).toBe(
|
|
68
|
+
'2031-07-04',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses a range value start as the visible month', () => {
|
|
73
|
+
expect(focusISO({value: {start: '2019-03-08', end: '2019-03-19'}})).toBe(
|
|
74
|
+
'2019-03-08',
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('prefers an explicit focusDate over everything else', () => {
|
|
79
|
+
expect(
|
|
80
|
+
focusISO({
|
|
81
|
+
focusDate: '2015-11-02',
|
|
82
|
+
value: '2031-07-04',
|
|
83
|
+
min: '2026-01-01',
|
|
84
|
+
max: '2026-12-31',
|
|
85
|
+
}),
|
|
86
|
+
).toBe('2015-11-02');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('prefers min when the bounds are inverted', () => {
|
|
90
|
+
// Degenerate input (min after max) — resolve it deterministically
|
|
91
|
+
// rather than reading the second bound off a contradiction.
|
|
92
|
+
expect(focusISO({min: '2027-01-01', max: '2020-01-01'})).toBe('2027-01-01');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file getInitialFocusDate.ts
|
|
5
|
+
* @input PlainDate utilities, min/max bounds, today
|
|
6
|
+
* @output Exports getInitialFocusDate — the month Calendar opens on
|
|
7
|
+
* @position Calendar-specific helper; used by Calendar.tsx
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When modified, update:
|
|
10
|
+
* - /packages/core/src/Calendar/getInitialFocusDate.test.ts
|
|
11
|
+
* - /packages/core/src/Calendar/Calendar.tsx (the only caller)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {ISODateString, DateRange} from '../utils/dateTypes';
|
|
15
|
+
import {
|
|
16
|
+
type PlainDate,
|
|
17
|
+
plainDateFromISO,
|
|
18
|
+
plainDateSetFirstOfMonth,
|
|
19
|
+
plainDateAddMonths,
|
|
20
|
+
plainDateIsBefore,
|
|
21
|
+
plainDateIsAfter,
|
|
22
|
+
} from '../utils/plainDate';
|
|
23
|
+
|
|
24
|
+
export interface InitialFocusDateOptions {
|
|
25
|
+
/** Controlled visible month, if the consumer supplied one. */
|
|
26
|
+
focusDate?: ISODateString;
|
|
27
|
+
/** Selected value (single date or range), controlled or uncontrolled. */
|
|
28
|
+
value?: ISODateString | DateRange;
|
|
29
|
+
/** Earliest selectable date. */
|
|
30
|
+
min?: ISODateString;
|
|
31
|
+
/** Latest selectable date. */
|
|
32
|
+
max?: ISODateString;
|
|
33
|
+
/** How many month panes the calendar renders (1 or 2). */
|
|
34
|
+
numberOfMonths: number;
|
|
35
|
+
/** Today, injected so the caller keeps a single memoized source of "now". */
|
|
36
|
+
today: PlainDate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Picks the date whose month the calendar opens on.
|
|
41
|
+
*
|
|
42
|
+
* An explicit `focusDate` wins, then the selected value — both are the
|
|
43
|
+
* consumer's own instruction about where to look, so neither is second-guessed
|
|
44
|
+
* against `min`/`max`.
|
|
45
|
+
*
|
|
46
|
+
* Otherwise the calendar opens on today, clamped into the `min`/`max` window.
|
|
47
|
+
* Without the clamp a window that doesn't contain today (a 2019 audit range, a
|
|
48
|
+
* booking window opening next spring) opened on today's month with every day
|
|
49
|
+
* disabled, leaving prev/next clicking as the only way in.
|
|
50
|
+
*
|
|
51
|
+
* When clamping forward to `min`, that month leads: the whole selectable window
|
|
52
|
+
* runs ahead of it. When clamping back to `max`, the last pane lands on `max`'s
|
|
53
|
+
* month instead — with `numberOfMonths={2}` opening on `[max, max + 1]` would
|
|
54
|
+
* spend half the calendar on a month that is entirely out of bounds. That
|
|
55
|
+
* shift never crosses `min`'s month.
|
|
56
|
+
*/
|
|
57
|
+
export function getInitialFocusDate(
|
|
58
|
+
options: InitialFocusDateOptions,
|
|
59
|
+
): PlainDate {
|
|
60
|
+
const {focusDate, value, min, max, numberOfMonths, today} = options;
|
|
61
|
+
|
|
62
|
+
if (focusDate) {
|
|
63
|
+
return plainDateFromISO(focusDate);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value) {
|
|
67
|
+
return plainDateFromISO(typeof value === 'string' ? value : value.start);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const minDate = min ? plainDateFromISO(min) : null;
|
|
71
|
+
const maxDate = max ? plainDateFromISO(max) : null;
|
|
72
|
+
|
|
73
|
+
if (minDate && plainDateIsBefore(today, minDate)) {
|
|
74
|
+
return minDate;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (maxDate && plainDateIsAfter(today, maxDate)) {
|
|
78
|
+
const lastPaneOffset = Math.max(0, numberOfMonths - 1);
|
|
79
|
+
const shifted = plainDateAddMonths(
|
|
80
|
+
plainDateSetFirstOfMonth(maxDate),
|
|
81
|
+
-lastPaneOffset,
|
|
82
|
+
);
|
|
83
|
+
if (
|
|
84
|
+
minDate &&
|
|
85
|
+
plainDateIsBefore(shifted, plainDateSetFirstOfMonth(minDate))
|
|
86
|
+
) {
|
|
87
|
+
return minDate;
|
|
88
|
+
}
|
|
89
|
+
return shifted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return today;
|
|
93
|
+
}
|
|
@@ -46,11 +46,12 @@ import {useLayer} from '../Layer';
|
|
|
46
46
|
import {useScrollOverflow} from '../hooks/useScrollOverflow';
|
|
47
47
|
import {isRtlElement} from '../hooks/isRtlElement';
|
|
48
48
|
import type {BaseProps} from '../BaseProps';
|
|
49
|
-
import {mergeProps,
|
|
49
|
+
import {mergeProps, rtlStyles} from '../utils';
|
|
50
50
|
import type {SpacingStep} from '../utils/types';
|
|
51
51
|
import {themeProps} from '../utils/themeProps';
|
|
52
52
|
import {useTranslator} from '../i18n';
|
|
53
53
|
|
|
54
|
+
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
54
55
|
/**
|
|
55
56
|
* Imperative control surface for the Carousel, accessed via the `handleRef`
|
|
56
57
|
* prop. Methods drive the same native-scroll machinery as the built-in
|
|
@@ -506,7 +507,7 @@ export function Carousel({
|
|
|
506
507
|
|
|
507
508
|
return (
|
|
508
509
|
<div
|
|
509
|
-
ref={
|
|
510
|
+
ref={useMergedRefs(ref, layer.ref as React.Ref<HTMLDivElement>)}
|
|
510
511
|
data-testid={testId}
|
|
511
512
|
{...htmlProps}
|
|
512
513
|
{...mergeProps(
|
package/src/Chat/ChatLayout.tsx
CHANGED
|
@@ -33,7 +33,7 @@ import {type ReactNode, useMemo, useRef} from 'react';
|
|
|
33
33
|
import * as stylex from '@stylexjs/stylex';
|
|
34
34
|
import {spacingVars} from '../theme/tokens.stylex';
|
|
35
35
|
import type {BaseProps} from '../BaseProps';
|
|
36
|
-
import {mergeProps
|
|
36
|
+
import {mergeProps} from '../utils';
|
|
37
37
|
import {useChatStreamScroll} from './useChatStreamScroll';
|
|
38
38
|
import {useChatNewMessages} from './useChatNewMessages';
|
|
39
39
|
import {ChatLayoutScrollButton} from './ChatLayoutScrollButton';
|
|
@@ -41,6 +41,7 @@ import {ChatLayoutContext} from './ChatContext';
|
|
|
41
41
|
import {themeProps} from '../utils/themeProps';
|
|
42
42
|
import {useTranslator} from '../i18n';
|
|
43
43
|
|
|
44
|
+
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
44
45
|
// =============================================================================
|
|
45
46
|
// Types
|
|
46
47
|
// =============================================================================
|
|
@@ -343,7 +344,7 @@ export function ChatLayout({
|
|
|
343
344
|
<ChatLayoutContext value={layoutContext}>
|
|
344
345
|
<div
|
|
345
346
|
{...rest}
|
|
346
|
-
ref={
|
|
347
|
+
ref={useMergedRefs(ref, rootRef)}
|
|
347
348
|
data-testid={testId}
|
|
348
349
|
{...mergeProps(
|
|
349
350
|
themeProps('chat-layout', {density}),
|