@astryxdesign/core 0.4.7 → 0.5.0-canary.055e75f
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 +161 -1
- package/dist/AlertDialog/AlertDialog.d.ts +1 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +45 -20
- package/dist/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/Avatar/Avatar.d.ts +25 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +58 -19
- package/dist/Avatar/index.d.ts +2 -2
- package/dist/Avatar/index.d.ts.map +1 -1
- package/dist/Avatar/index.js +1 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts +9 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +8 -4
- package/dist/AvatarGroup/AvatarGroupContext.d.ts +2 -1
- package/dist/AvatarGroup/AvatarGroupContext.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +10 -6
- package/dist/Banner/Banner.d.ts +40 -15
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +51 -18
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +17 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +3 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -6
- package/dist/ButtonGroup/ButtonGroup.d.ts +5 -1
- package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/ButtonGroup/ButtonGroup.js +26 -7
- package/dist/Calendar/Calendar.d.ts +3 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +14 -15
- 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/Calendar/styles.d.ts +5 -14
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +15 -15
- package/dist/Card/Card.d.ts +6 -9
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +6 -6
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +3 -3
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -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/Chat/ChatTokenizedText.d.ts +2 -0
- package/dist/Chat/ChatTokenizedText.d.ts.map +1 -1
- package/dist/Chat/ChatTokenizedText.js +11 -6
- package/dist/Chat/useChatComposerTokens.js +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +10 -3
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +3 -3
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +15 -4
- package/dist/CodeBlock/highlightRanges.js +1 -1
- package/dist/CodeBlock/highlightStyles.js +13 -13
- package/dist/CommandPalette/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 +43 -0
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +22 -2
- package/dist/DateInput/NativeDateField.d.ts +10 -0
- package/dist/DateInput/NativeDateField.d.ts.map +1 -0
- package/dist/DateInput/NativeDateField.js +494 -0
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +82 -23
- package/dist/DateInput/index.d.ts +1 -1
- package/dist/DateInput/index.d.ts.map +1 -1
- package/dist/DateInput/nativeDateSegments.d.ts +22 -0
- package/dist/DateInput/nativeDateSegments.d.ts.map +1 -0
- package/dist/DateInput/nativeDateSegments.js +111 -0
- package/dist/DateTimeInput/DateTimeInput.d.ts +21 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +406 -12
- package/dist/DateTimeInput/index.d.ts +1 -1
- package/dist/DateTimeInput/index.d.ts.map +1 -1
- package/dist/Dialog/Dialog.d.ts +21 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +110 -58
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +6 -5
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +9 -1
- 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/HoverCard.js +104 -3
- package/dist/HoverCard/useHoverCard.d.ts +19 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +74 -26
- package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckboxIndicator.js +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +4 -2
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +2 -19
- 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/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +9 -4
- 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 +182 -32
- 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 +32 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +63 -11
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/NavIcon/NavIcon.d.ts.map +1 -1
- package/dist/NavIcon/NavIcon.js +5 -1
- package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.js +3 -2
- package/dist/NumberInput/NumberInput.d.ts +6 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +54 -104
- package/dist/NumberInput/numberInputCommit.d.ts +34 -0
- package/dist/NumberInput/numberInputCommit.d.ts.map +1 -0
- package/dist/NumberInput/numberInputCommit.js +71 -0
- package/dist/NumberInput/numberParser.d.ts +64 -0
- package/dist/NumberInput/numberParser.d.ts.map +1 -0
- package/dist/NumberInput/numberParser.js +350 -0
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +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 +8 -2
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +19 -6
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts +3 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +28 -6
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -1
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +110 -27
- package/dist/PowerSearch/types.d.ts +2 -0
- package/dist/PowerSearch/types.d.ts.map +1 -1
- package/dist/PowerSearch/usePowerSearchSource.d.ts +6 -1
- package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
- package/dist/PowerSearch/usePowerSearchSource.js +14 -5
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +18 -5
- 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 +17 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +81 -14
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +4 -3
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +8 -7
- 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 +37 -5
- package/dist/Spinner/Spinner.d.ts +3 -3
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +130 -78
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +6 -1
- 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 +8 -3
- package/dist/TabList/Tab.d.ts +15 -2
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +32 -7
- package/dist/TabList/TabList.d.ts +46 -7
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +353 -23
- package/dist/TabList/TabListContext.d.ts +11 -1
- package/dist/TabList/TabListContext.d.ts.map +1 -1
- package/dist/TabList/TabListContext.js +11 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +3 -2
- package/dist/TabList/index.d.ts +2 -2
- package/dist/TabList/index.d.ts.map +1 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +10 -2
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +3 -6
- package/dist/Table/TableRow.d.ts +16 -5
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +16 -5
- package/dist/Table/plugins/filtering/useTableFiltering.d.ts.map +1 -1
- package/dist/Table/plugins/filtering/useTableFiltering.js +73 -4
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.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/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +3 -4
- 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 +9 -3
- 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/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +27 -8
- package/dist/Token/TokenLink.d.ts.map +1 -1
- package/dist/Token/TokenLink.js +2 -2
- package/dist/Tokenizer/Tokenizer.d.ts +3 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +2 -0
- 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/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +5 -4
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +4 -3
- package/dist/Typeahead/BaseTypeahead.d.ts +2 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +68 -29
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +3 -2
- package/dist/astryx.css +98 -7
- 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 +60 -95
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +3 -18
- 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/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +24 -4
- package/dist/theme/syntax/tokens.d.ts +1 -1
- package/dist/theme/syntax/tokens.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.js +5 -2
- package/dist/utils/groupItems.d.ts +5 -1
- package/dist/utils/groupItems.d.ts.map +1 -1
- package/dist/utils/groupItems.js +9 -9
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/isApplePlatform.d.ts +18 -0
- package/dist/utils/isApplePlatform.d.ts.map +1 -0
- package/dist/utils/isApplePlatform.js +37 -0
- package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
- package/dist/utils/sharedResizeObserver.js +10 -1
- 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 +154 -10
- package/locales/pseudo.json +108 -0
- package/package.json +8 -3
- package/src/AlertDialog/AlertDialog.doc.mjs +114 -30
- package/src/AlertDialog/AlertDialog.test.tsx +211 -2
- package/src/AlertDialog/AlertDialog.tsx +62 -20
- package/src/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 +14 -3
- package/src/Avatar/Avatar.test.tsx +97 -0
- package/src/Avatar/Avatar.tsx +125 -52
- package/src/Avatar/index.ts +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +8 -2
- package/src/AvatarGroup/AvatarGroup.test.tsx +24 -0
- package/src/AvatarGroup/AvatarGroup.tsx +15 -6
- package/src/AvatarGroup/AvatarGroupContext.ts +5 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +10 -4
- package/src/Banner/Banner.doc.mjs +13 -11
- package/src/Banner/Banner.test.tsx +102 -15
- package/src/Banner/Banner.tsx +72 -25
- package/src/BottomSheet/BottomSheet.test.tsx +123 -0
- package/src/BottomSheet/BottomSheet.tsx +17 -4
- package/src/BottomSheet/BottomSheetSwitcher.tsx +3 -2
- package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
- package/src/Button/Button.doc.mjs +24 -1
- package/src/Button/Button.test.tsx +32 -4
- package/src/Button/Button.tsx +12 -5
- package/src/ButtonGroup/ButtonGroup.doc.mjs +10 -1
- package/src/ButtonGroup/ButtonGroup.test.tsx +404 -1
- package/src/ButtonGroup/ButtonGroup.tsx +23 -5
- package/src/Calendar/Calendar.doc.mjs +4 -3
- package/src/Calendar/Calendar.test.tsx +103 -0
- package/src/Calendar/Calendar.tsx +19 -16
- package/src/Calendar/getInitialFocusDate.test.ts +94 -0
- package/src/Calendar/getInitialFocusDate.ts +93 -0
- package/src/Calendar/styles.ts +49 -31
- package/src/Card/Card.doc.mjs +10 -7
- package/src/Card/Card.test.tsx +44 -19
- package/src/Card/Card.tsx +12 -32
- package/src/Carousel/Carousel.tsx +3 -2
- package/src/Chat/ChatComposerInput.tsx +5 -1
- 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/Chat/ChatTokenizedText.tsx +17 -10
- package/src/Chat/useChatComposerTokens.ts +1 -1
- package/src/CheckboxInput/CheckboxInput.doc.mjs +2 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +15 -0
- package/src/CheckboxInput/CheckboxInput.tsx +8 -2
- package/src/ClickableCard/ClickableCard.tsx +3 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +16 -4
- package/src/CodeBlock/CodeBlock.test.tsx +37 -0
- package/src/CodeBlock/CodeBlock.tsx +20 -4
- package/src/CodeBlock/highlightRanges.ts +1 -1
- package/src/CodeBlock/highlightStyles.ts +13 -13
- package/src/CommandPalette/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.doc.mjs +16 -0
- package/src/DateInput/DateInput.tsx +55 -2
- package/src/DateInput/DateInputTouch.test.tsx +146 -32
- package/src/DateInput/NativeDateField.test.tsx +635 -0
- package/src/DateInput/NativeDateField.tsx +641 -0
- package/src/DateInput/TouchDateField.tsx +108 -24
- package/src/DateInput/index.ts +1 -0
- package/src/DateInput/nativeDateSegments.ts +127 -0
- package/src/DateTimeInput/DateTimeInput.doc.mjs +18 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +1088 -0
- package/src/DateTimeInput/DateTimeInput.tsx +556 -24
- package/src/DateTimeInput/index.ts +1 -0
- package/src/Dialog/Dialog.doc.mjs +8 -3
- package/src/Dialog/Dialog.test.tsx +429 -2
- package/src/Dialog/Dialog.tsx +133 -58
- package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +8 -5
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +8 -11
- package/src/Field/Field.doc.mjs +13 -2
- package/src/Field/FieldLabel.test.tsx +29 -0
- package/src/Field/FieldLabel.tsx +7 -0
- 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 +13 -2
- package/src/HoverCard/HoverCard.test.tsx +154 -4
- package/src/HoverCard/HoverCard.tsx +143 -3
- package/src/HoverCard/useHoverCard.tsx +104 -33
- package/src/Indicator/CheckboxIndicator.tsx +14 -8
- package/src/Indicator/Indicator.doc.mjs +2 -0
- package/src/Item/Item.tsx +5 -4
- package/src/Kbd/Kbd.tsx +2 -19
- 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/useLayer.test.tsx +62 -14
- 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/Markdown.doc.mjs +5 -0
- package/src/Markdown/Markdown.tsx +9 -4
- package/src/Markdown/incremental.test.ts +16 -2
- package/src/Markdown/index.ts +1 -0
- package/src/Markdown/parser.perf.test.ts +96 -1
- package/src/Markdown/parser.test.ts +178 -2
- package/src/Markdown/parser.ts +253 -32
- package/src/MobileNav/MobileNav.tsx +62 -36
- package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +49 -1
- package/src/MultiSelector/MultiSelector.test.tsx +277 -0
- package/src/MultiSelector/MultiSelector.tsx +142 -11
- package/src/MultiSelector/index.ts +1 -0
- package/src/NavIcon/NavIcon.doc.mjs +8 -2
- package/src/NavIcon/NavIcon.test.tsx +9 -0
- package/src/NavIcon/NavIcon.tsx +5 -1
- package/src/NavMenu/NavHeadingMenu.tsx +3 -2
- package/src/NumberInput/NumberInput.doc.mjs +31 -7
- package/src/NumberInput/NumberInput.test.tsx +436 -8
- package/src/NumberInput/NumberInput.tsx +62 -114
- package/src/NumberInput/numberInputCommit.test.ts +72 -0
- package/src/NumberInput/numberInputCommit.ts +103 -0
- package/src/NumberInput/numberParser.docblock.test.ts +234 -0
- package/src/NumberInput/numberParser.test.ts +222 -0
- package/src/NumberInput/numberParser.ts +403 -0
- package/src/Outline/Outline.tsx +3 -2
- package/src/OverflowList/OverflowList.tsx +3 -2
- package/src/Overlay/Overlay.tsx +3 -2
- package/src/Pagination/Pagination.test.tsx +3 -2
- package/src/Popover/usePopover.tsx +31 -28
- package/src/PowerSearch/PowerSearch.doc.mjs +160 -28
- package/src/PowerSearch/PowerSearch.test.tsx +240 -1
- package/src/PowerSearch/PowerSearch.tsx +34 -5
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +288 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +29 -2
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +111 -1
- package/src/PowerSearch/PowerSearchValueEditor.tsx +124 -10
- package/src/PowerSearch/types.ts +2 -0
- package/src/PowerSearch/usePowerSearchSource.test.ts +82 -2
- package/src/PowerSearch/usePowerSearchSource.ts +13 -3
- package/src/ProgressBar/ProgressBar.doc.mjs +42 -6
- package/src/ProgressBar/ProgressBar.test.tsx +24 -0
- package/src/ProgressBar/ProgressBar.tsx +28 -8
- package/src/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 +26 -1
- package/src/Selector/Selector.test.tsx +311 -1
- package/src/Selector/Selector.tsx +139 -16
- package/src/SideNav/SideNav.doc.mjs +6 -6
- package/src/SideNav/SideNav.tsx +4 -2
- package/src/SideNav/SideNavHeading.tsx +12 -12
- package/src/SideNav/SideNavItem.tsx +4 -2
- package/src/Slider/Slider.test.tsx +106 -1
- package/src/Slider/Slider.tsx +55 -3
- package/src/Spinner/Spinner.test.tsx +147 -1
- package/src/Spinner/Spinner.tsx +120 -101
- package/src/StatusDot/StatusDot.doc.mjs +8 -2
- package/src/StatusDot/StatusDot.test.tsx +9 -0
- package/src/StatusDot/StatusDot.tsx +7 -1
- package/src/Stepper/Step.doc.mjs +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.doc.mjs +2 -0
- package/src/Switch/Switch.test.tsx +10 -0
- package/src/Switch/Switch.tsx +6 -2
- package/src/TabList/Tab.doc.mjs +13 -1
- package/src/TabList/Tab.tsx +67 -7
- package/src/TabList/TabList.doc.mjs +22 -6
- package/src/TabList/TabList.test.tsx +559 -0
- package/src/TabList/TabList.tsx +490 -22
- package/src/TabList/TabListContext.ts +13 -2
- package/src/TabList/TabMenu.tsx +3 -2
- package/src/TabList/index.ts +6 -2
- package/src/Table/BaseTable.tsx +12 -4
- package/src/Table/Table.doc.mjs +9 -2
- package/src/Table/Table.test.tsx +132 -14
- package/src/Table/Table.tsx +3 -8
- package/src/Table/TableBody.doc.mjs +40 -0
- package/src/Table/TableFooter.doc.mjs +40 -0
- package/src/Table/TableHeader.doc.mjs +40 -0
- package/src/Table/TableRow.tsx +16 -5
- package/src/Table/plugins/filtering/useTableFiltering.test.tsx +44 -6
- package/src/Table/plugins/filtering/useTableFiltering.tsx +100 -4
- package/src/Table/plugins/groupedRows/useTableGroupedRows-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/plugins/stickyColumns/useTableStickyColumns.tsx +1 -2
- 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.doc.mjs +8 -2
- package/src/TextArea/TextArea.test.tsx +11 -0
- package/src/TextArea/TextArea.tsx +15 -8
- 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.test.tsx +18 -0
- package/src/Timestamp/Timestamp.tsx +4 -2
- package/src/Toast/Toast.doc.mjs +6 -0
- package/src/Toast/ToastViewport.tsx +17 -1
- package/src/Token/TokenLink.tsx +2 -2
- package/src/Tokenizer/Tokenizer.doc.mjs +27 -0
- package/src/Tokenizer/Tokenizer.tsx +4 -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/TopNavHeading.tsx +5 -4
- package/src/TopNav/TopNavMegaMenu.tsx +3 -2
- package/src/TopNav/TopNavMenu.tsx +4 -3
- package/src/Typeahead/BaseTypeahead.doc.mjs +11 -0
- package/src/Typeahead/BaseTypeahead.tsx +85 -33
- package/src/Typeahead/Typeahead.doc.mjs +5 -0
- package/src/Typeahead/Typeahead.test.tsx +49 -0
- package/src/Typeahead/Typeahead.tsx +3 -2
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useContainerReveal.doc.mjs +2 -2
- package/src/hooks/useFocusTrap.test.tsx +14 -0
- package/src/hooks/useFocusTrap.ts +62 -111
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
- package/src/hooks/useHotkeys.ts +3 -18
- 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/raw-import.d.ts +14 -0
- package/src/theme/MediaTheme.doc.mjs +5 -5
- package/src/theme/derivedVarRegistry.test.ts +19 -3
- package/src/theme/derivedVarRegistry.ts +23 -4
- package/src/theme/generateThemeRules.test.ts +37 -0
- package/src/theme/syntax/tokens.ts +5 -2
- package/src/utils/groupItems.ts +12 -6
- package/src/utils/index.ts +1 -1
- package/src/utils/isApplePlatform.test.ts +61 -0
- package/src/utils/isApplePlatform.ts +37 -0
- package/src/utils/sharedResizeObserver.ts +10 -2
- package/src/utils/timeParser.test.ts +6 -1
- package/src/utils/timeParser.ts +35 -13
- package/src/__tests__/inputWidthContract.test.tsx +0 -69
|
@@ -942,6 +942,1094 @@ describe('DateTimeInput', () => {
|
|
|
942
942
|
});
|
|
943
943
|
});
|
|
944
944
|
|
|
945
|
+
// ===========================================================================
|
|
946
|
+
// Time-option dropdown (#2727)
|
|
947
|
+
// ===========================================================================
|
|
948
|
+
|
|
949
|
+
describe('timeOptionInterval', () => {
|
|
950
|
+
// The dropdown renders through the same popover as the calendar, so its
|
|
951
|
+
// content lands in the top layer: jsdom keeps the nodes in the DOM but
|
|
952
|
+
// role queries skip them. Read the options off the container directly,
|
|
953
|
+
// exactly as the weekStartsOn tests read columnheaders.
|
|
954
|
+
const optionsIn = (container: HTMLElement): HTMLElement[] =>
|
|
955
|
+
Array.from(container.querySelectorAll('[role="option"]'));
|
|
956
|
+
|
|
957
|
+
const labelsIn = (container: HTMLElement): (string | null)[] =>
|
|
958
|
+
optionsIn(container).map(o => o.textContent);
|
|
959
|
+
|
|
960
|
+
// ---- opt-in gate --------------------------------------------------------
|
|
961
|
+
|
|
962
|
+
it('adds no listbox and no second combobox without timeOptionInterval', () => {
|
|
963
|
+
const {container} = render(
|
|
964
|
+
<DateTimeInput
|
|
965
|
+
label="Meeting"
|
|
966
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
967
|
+
onChange={() => {}}
|
|
968
|
+
/>,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// The date input stays the only combobox on the field. 30+ existing
|
|
972
|
+
// assertions use the singular getByRole('combobox'); a second one would
|
|
973
|
+
// break every one of them, and every downstream consumer's too.
|
|
974
|
+
expect(screen.getAllByRole('combobox')).toHaveLength(1);
|
|
975
|
+
|
|
976
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
977
|
+
expect(timeInput).not.toHaveAttribute('role', 'combobox');
|
|
978
|
+
fireEvent.click(timeInput);
|
|
979
|
+
expect(optionsIn(container)).toHaveLength(0);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// ---- option generation --------------------------------------------------
|
|
983
|
+
|
|
984
|
+
it('renders one option per interval step across the day', () => {
|
|
985
|
+
const {container} = render(
|
|
986
|
+
<DateTimeInput
|
|
987
|
+
label="Meeting"
|
|
988
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
989
|
+
timeOptionInterval={30}
|
|
990
|
+
onChange={() => {}}
|
|
991
|
+
/>,
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
995
|
+
// A full day at 30-minute cadence.
|
|
996
|
+
expect(optionsIn(container)).toHaveLength(48);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('renders 24 options at an hourly interval', () => {
|
|
1000
|
+
const {container} = render(
|
|
1001
|
+
<DateTimeInput
|
|
1002
|
+
label="Meeting"
|
|
1003
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1004
|
+
timeOptionInterval={60}
|
|
1005
|
+
onChange={() => {}}
|
|
1006
|
+
/>,
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1010
|
+
const labels = labelsIn(container);
|
|
1011
|
+
expect(labels).toHaveLength(24);
|
|
1012
|
+
// The 12 AM - 11 PM list the issue asks for.
|
|
1013
|
+
expect(labels[0]).toBe('12:00 AM');
|
|
1014
|
+
expect(labels[23]).toBe('11:00 PM');
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('labels options in 24-hour format when hourFormat is 24h', () => {
|
|
1018
|
+
const {container} = render(
|
|
1019
|
+
<DateTimeInput
|
|
1020
|
+
label="Meeting"
|
|
1021
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1022
|
+
timeOptionInterval={60}
|
|
1023
|
+
hourFormat="24h"
|
|
1024
|
+
onChange={() => {}}
|
|
1025
|
+
/>,
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1029
|
+
const labels = labelsIn(container);
|
|
1030
|
+
expect(labels[0]).toBe('00:00');
|
|
1031
|
+
expect(labels[13]).toBe('13:00');
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('includes seconds in option labels when hasSeconds is set', () => {
|
|
1035
|
+
const {container} = render(
|
|
1036
|
+
<DateTimeInput
|
|
1037
|
+
label="Meeting"
|
|
1038
|
+
value={'2026-03-15T14:30:00' as ISODateTimeString}
|
|
1039
|
+
timeOptionInterval={60}
|
|
1040
|
+
hasSeconds
|
|
1041
|
+
onChange={() => {}}
|
|
1042
|
+
/>,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1046
|
+
expect(labelsIn(container)[0]).toBe('12:00:00 AM');
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// ---- committing ---------------------------------------------------------
|
|
1050
|
+
|
|
1051
|
+
it('commits the clicked option and keeps the selected date', () => {
|
|
1052
|
+
const onChange = vi.fn();
|
|
1053
|
+
const {container} = render(
|
|
1054
|
+
<DateTimeInput
|
|
1055
|
+
label="Meeting"
|
|
1056
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1057
|
+
timeOptionInterval={60}
|
|
1058
|
+
onChange={onChange}
|
|
1059
|
+
/>,
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1063
|
+
// 09:00 is the tenth hourly option.
|
|
1064
|
+
fireEvent.click(optionsIn(container)[9]);
|
|
1065
|
+
|
|
1066
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1067
|
+
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T09:00');
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it('marks the committed option as selected, not merely highlighted', () => {
|
|
1071
|
+
const {container} = render(
|
|
1072
|
+
<DateTimeInput
|
|
1073
|
+
label="Meeting"
|
|
1074
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1075
|
+
timeOptionInterval={60}
|
|
1076
|
+
onChange={() => {}}
|
|
1077
|
+
/>,
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1081
|
+
const selected = optionsIn(container).filter(
|
|
1082
|
+
o => o.getAttribute('aria-selected') === 'true',
|
|
1083
|
+
);
|
|
1084
|
+
expect(selected).toHaveLength(1);
|
|
1085
|
+
expect(selected[0].textContent).toBe('2:00 PM');
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// ---- keyboard: the arrow-key collision ----------------------------------
|
|
1089
|
+
|
|
1090
|
+
it('keeps ArrowUp stepping by timeIncrement while the list is closed', () => {
|
|
1091
|
+
const onChange = vi.fn();
|
|
1092
|
+
render(
|
|
1093
|
+
<DateTimeInput
|
|
1094
|
+
label="Meeting"
|
|
1095
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1096
|
+
timeIncrement={15}
|
|
1097
|
+
timeOptionInterval={30}
|
|
1098
|
+
onChange={onChange}
|
|
1099
|
+
/>,
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1103
|
+
// Closed list: arrows still step the value, as they always have.
|
|
1104
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1105
|
+
|
|
1106
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1107
|
+
expect(onChange.mock.calls[0][0]).toContain('14:45');
|
|
1108
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('opens the list on Alt+ArrowDown without changing the value', () => {
|
|
1112
|
+
const onChange = vi.fn();
|
|
1113
|
+
const {container} = render(
|
|
1114
|
+
<DateTimeInput
|
|
1115
|
+
label="Meeting"
|
|
1116
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1117
|
+
timeOptionInterval={30}
|
|
1118
|
+
onChange={onChange}
|
|
1119
|
+
/>,
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1123
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1124
|
+
|
|
1125
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1126
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1127
|
+
expect(optionsIn(container)).toHaveLength(48);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it('moves the active option with ArrowDown while open, without committing', () => {
|
|
1131
|
+
const onChange = vi.fn();
|
|
1132
|
+
const {container} = render(
|
|
1133
|
+
<DateTimeInput
|
|
1134
|
+
label="Meeting"
|
|
1135
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1136
|
+
timeOptionInterval={60}
|
|
1137
|
+
onChange={onChange}
|
|
1138
|
+
/>,
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1142
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1143
|
+
// Opening starts the highlight on the committed value (14:00).
|
|
1144
|
+
expect(timeInput).toHaveAttribute(
|
|
1145
|
+
'aria-activedescendant',
|
|
1146
|
+
optionsIn(container)[14].id,
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1150
|
+
expect(timeInput).toHaveAttribute(
|
|
1151
|
+
'aria-activedescendant',
|
|
1152
|
+
optionsIn(container)[15].id,
|
|
1153
|
+
);
|
|
1154
|
+
// Navigating is not selecting.
|
|
1155
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it('commits the active option on Enter and closes the list', () => {
|
|
1159
|
+
const onChange = vi.fn();
|
|
1160
|
+
render(
|
|
1161
|
+
<DateTimeInput
|
|
1162
|
+
label="Meeting"
|
|
1163
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1164
|
+
timeOptionInterval={60}
|
|
1165
|
+
onChange={onChange}
|
|
1166
|
+
/>,
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1170
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1171
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1172
|
+
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1173
|
+
|
|
1174
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1175
|
+
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T15:00');
|
|
1176
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it('jumps to the first and last option with Home and End', () => {
|
|
1180
|
+
const {container} = render(
|
|
1181
|
+
<DateTimeInput
|
|
1182
|
+
label="Meeting"
|
|
1183
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1184
|
+
timeOptionInterval={60}
|
|
1185
|
+
onChange={() => {}}
|
|
1186
|
+
/>,
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1190
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1191
|
+
|
|
1192
|
+
fireEvent.keyDown(timeInput, {key: 'Home'});
|
|
1193
|
+
expect(timeInput).toHaveAttribute(
|
|
1194
|
+
'aria-activedescendant',
|
|
1195
|
+
optionsIn(container)[0].id,
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1199
|
+
expect(timeInput).toHaveAttribute(
|
|
1200
|
+
'aria-activedescendant',
|
|
1201
|
+
optionsIn(container)[23].id,
|
|
1202
|
+
);
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
it('closes on Escape without committing', () => {
|
|
1206
|
+
const onChange = vi.fn();
|
|
1207
|
+
render(
|
|
1208
|
+
<DateTimeInput
|
|
1209
|
+
label="Meeting"
|
|
1210
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1211
|
+
timeOptionInterval={60}
|
|
1212
|
+
onChange={onChange}
|
|
1213
|
+
/>,
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1217
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1218
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1219
|
+
fireEvent.keyDown(timeInput, {key: 'Escape'});
|
|
1220
|
+
|
|
1221
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1222
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// ---- typed entry keeps working -----------------------------------------
|
|
1226
|
+
|
|
1227
|
+
it('moves the highlight to the closest option as the user types, without dropping options', () => {
|
|
1228
|
+
const {container} = render(
|
|
1229
|
+
<DateTimeInput
|
|
1230
|
+
label="Meeting"
|
|
1231
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1232
|
+
timeOptionInterval={60}
|
|
1233
|
+
onChange={() => {}}
|
|
1234
|
+
/>,
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1238
|
+
fireEvent.click(timeInput);
|
|
1239
|
+
fireEvent.change(timeInput, {target: {value: '9:00 PM'}});
|
|
1240
|
+
|
|
1241
|
+
// Free-form entry is the contract here: the list narrows nothing, it
|
|
1242
|
+
// follows the typed value so Enter still lands on something sensible.
|
|
1243
|
+
expect(optionsIn(container)).toHaveLength(24);
|
|
1244
|
+
expect(timeInput).toHaveAttribute(
|
|
1245
|
+
'aria-activedescendant',
|
|
1246
|
+
optionsIn(container)[21].id,
|
|
1247
|
+
);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// ---- constraints --------------------------------------------------------
|
|
1251
|
+
|
|
1252
|
+
it('drops options outside min/max on the boundary date', () => {
|
|
1253
|
+
const {container} = render(
|
|
1254
|
+
<DateTimeInput
|
|
1255
|
+
label="Meeting"
|
|
1256
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1257
|
+
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1258
|
+
max={'2026-03-15T17:00' as ISODateTimeString}
|
|
1259
|
+
timeOptionInterval={60}
|
|
1260
|
+
onChange={() => {}}
|
|
1261
|
+
/>,
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1265
|
+
const labels = labelsIn(container);
|
|
1266
|
+
// 09:00 through 17:00 inclusive.
|
|
1267
|
+
expect(labels).toHaveLength(9);
|
|
1268
|
+
expect(labels[0]).toBe('9:00 AM');
|
|
1269
|
+
expect(labels[8]).toBe('5:00 PM');
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
it('offers the whole day when the selected date is inside the min/max range', () => {
|
|
1273
|
+
const {container} = render(
|
|
1274
|
+
<DateTimeInput
|
|
1275
|
+
label="Meeting"
|
|
1276
|
+
value={'2026-03-16T14:00' as ISODateTimeString}
|
|
1277
|
+
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1278
|
+
max={'2026-03-17T17:00' as ISODateTimeString}
|
|
1279
|
+
timeOptionInterval={60}
|
|
1280
|
+
onChange={() => {}}
|
|
1281
|
+
/>,
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1285
|
+
// The time bound only bites on the boundary date itself.
|
|
1286
|
+
expect(optionsIn(container)).toHaveLength(24);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// ---- a11y wiring + disabled --------------------------------------------
|
|
1290
|
+
|
|
1291
|
+
it('wires the time input as a combobox onto the listbox', () => {
|
|
1292
|
+
const {container} = render(
|
|
1293
|
+
<DateTimeInput
|
|
1294
|
+
label="Meeting"
|
|
1295
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1296
|
+
timeOptionInterval={60}
|
|
1297
|
+
onChange={() => {}}
|
|
1298
|
+
/>,
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1302
|
+
expect(timeInput).toHaveAttribute('role', 'combobox');
|
|
1303
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1304
|
+
expect(timeInput).toHaveAttribute('aria-autocomplete', 'list');
|
|
1305
|
+
|
|
1306
|
+
fireEvent.click(timeInput);
|
|
1307
|
+
const listbox = container.querySelector('[role="listbox"]');
|
|
1308
|
+
expect(listbox).not.toBeNull();
|
|
1309
|
+
expect(timeInput).toHaveAttribute('aria-controls', listbox?.id);
|
|
1310
|
+
// Named, so the list is not an anonymous group to a screen reader.
|
|
1311
|
+
expect(listbox).toHaveAttribute('aria-label');
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it('does not open the list when disabled', () => {
|
|
1315
|
+
render(
|
|
1316
|
+
<DateTimeInput
|
|
1317
|
+
label="Meeting"
|
|
1318
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1319
|
+
timeOptionInterval={60}
|
|
1320
|
+
isDisabled
|
|
1321
|
+
onChange={() => {}}
|
|
1322
|
+
/>,
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1325
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1326
|
+
fireEvent.click(timeInput);
|
|
1327
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1328
|
+
|
|
1329
|
+
// The layer mounts its content whether or not it is open — visibility is
|
|
1330
|
+
// the popover attribute's job, which jsdom does not apply. So assert the
|
|
1331
|
+
// open state itself, not the presence of option nodes.
|
|
1332
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1333
|
+
expect(timeInput).not.toHaveAttribute('aria-controls');
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('does not open the list for a focusable-disabled field', () => {
|
|
1337
|
+
// With disabledMessage the input drops the native `disabled` attribute so
|
|
1338
|
+
// the reason stays discoverable, which means clicks and keys really do
|
|
1339
|
+
// reach it. This is the case the guards have to catch — a natively
|
|
1340
|
+
// disabled input never delivers the events in the first place.
|
|
1341
|
+
render(
|
|
1342
|
+
<DateTimeInput
|
|
1343
|
+
label="Meeting"
|
|
1344
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1345
|
+
timeOptionInterval={60}
|
|
1346
|
+
isDisabled
|
|
1347
|
+
disabledMessage="Pick a project first"
|
|
1348
|
+
onChange={() => {}}
|
|
1349
|
+
/>,
|
|
1350
|
+
);
|
|
1351
|
+
|
|
1352
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1353
|
+
expect(timeInput).not.toBeDisabled();
|
|
1354
|
+
|
|
1355
|
+
fireEvent.click(timeInput);
|
|
1356
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1357
|
+
|
|
1358
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1359
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
// -- edge cases ----------------------------------------------------------
|
|
1363
|
+
|
|
1364
|
+
it('does not fire a change when the already-selected option is clicked', () => {
|
|
1365
|
+
// The marker compares a seconds-normalised time while the commit path
|
|
1366
|
+
// compared the raw one, so with a stray-seconds value the option that
|
|
1367
|
+
// renders as selected still emitted a change when clicked.
|
|
1368
|
+
const onChange = vi.fn();
|
|
1369
|
+
const {container} = render(
|
|
1370
|
+
<DateTimeInput
|
|
1371
|
+
label="Meeting"
|
|
1372
|
+
value={'2026-03-15T14:00:00' as ISODateTimeString}
|
|
1373
|
+
timeOptionInterval={60}
|
|
1374
|
+
onChange={onChange}
|
|
1375
|
+
/>,
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1379
|
+
fireEvent.click(timeInput);
|
|
1380
|
+
const selected = optionsIn(container).find(
|
|
1381
|
+
o => o.getAttribute('aria-selected') === 'true',
|
|
1382
|
+
);
|
|
1383
|
+
expect(selected).toBeDefined();
|
|
1384
|
+
|
|
1385
|
+
fireEvent.click(selected as HTMLElement);
|
|
1386
|
+
|
|
1387
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1388
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
it('does not open an empty list when min/max leave no valid option', () => {
|
|
1392
|
+
// A window narrower than the cadence survives no option at all. Opening
|
|
1393
|
+
// then shows a blank popover with nothing to pick.
|
|
1394
|
+
render(
|
|
1395
|
+
<DateTimeInput
|
|
1396
|
+
label="Meeting"
|
|
1397
|
+
value={'2026-03-15T09:15' as ISODateTimeString}
|
|
1398
|
+
min={'2026-03-15T09:10' as ISODateTimeString}
|
|
1399
|
+
max={'2026-03-15T09:20' as ISODateTimeString}
|
|
1400
|
+
timeOptionInterval={30}
|
|
1401
|
+
onChange={() => {}}
|
|
1402
|
+
/>,
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1406
|
+
fireEvent.click(timeInput);
|
|
1407
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1408
|
+
|
|
1409
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1410
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it('commits the active option on Enter once the user arrows away from what they typed', () => {
|
|
1414
|
+
// Typed text beats the highlight only while the highlight is still the
|
|
1415
|
+
// one typing derived. After an arrow key the highlight is the user's own
|
|
1416
|
+
// choice, and Enter must honour what the field says is active.
|
|
1417
|
+
const onChange = vi.fn();
|
|
1418
|
+
const {container} = render(
|
|
1419
|
+
<DateTimeInput
|
|
1420
|
+
label="Meeting"
|
|
1421
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1422
|
+
timeOptionInterval={60}
|
|
1423
|
+
onChange={onChange}
|
|
1424
|
+
/>,
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1428
|
+
fireEvent.click(timeInput);
|
|
1429
|
+
fireEvent.change(timeInput, {target: {value: '9:15 AM'}});
|
|
1430
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1431
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1432
|
+
|
|
1433
|
+
expect(timeInput).toHaveAttribute(
|
|
1434
|
+
'aria-activedescendant',
|
|
1435
|
+
optionsIn(container)[11].id,
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1439
|
+
expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toBe(
|
|
1440
|
+
'2026-03-15T11:00',
|
|
1441
|
+
);
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('closes a list that empties underneath it', () => {
|
|
1445
|
+
// The open-time guard cannot help here: the window narrows after the
|
|
1446
|
+
// list is already up. Left open, the keyboard switch is gated off and
|
|
1447
|
+
// the arrows fall back to stepping a value the user cannot see.
|
|
1448
|
+
const onChange = vi.fn();
|
|
1449
|
+
const {container, rerender} = render(
|
|
1450
|
+
<DateTimeInput
|
|
1451
|
+
label="Meeting"
|
|
1452
|
+
value={'2026-03-16T14:00' as ISODateTimeString}
|
|
1453
|
+
min={'2026-03-15T09:10' as ISODateTimeString}
|
|
1454
|
+
max={'2026-03-15T09:20' as ISODateTimeString}
|
|
1455
|
+
timeOptionInterval={60}
|
|
1456
|
+
onChange={onChange}
|
|
1457
|
+
/>,
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1461
|
+
fireEvent.click(timeInput);
|
|
1462
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1463
|
+
|
|
1464
|
+
rerender(
|
|
1465
|
+
<DateTimeInput
|
|
1466
|
+
label="Meeting"
|
|
1467
|
+
value={'2026-03-15T09:15' as ISODateTimeString}
|
|
1468
|
+
min={'2026-03-15T09:10' as ISODateTimeString}
|
|
1469
|
+
max={'2026-03-15T09:20' as ISODateTimeString}
|
|
1470
|
+
timeOptionInterval={60}
|
|
1471
|
+
onChange={onChange}
|
|
1472
|
+
/>,
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
expect(optionsIn(container)).toHaveLength(0);
|
|
1476
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
it('closes the list when focus leaves the field entirely', () => {
|
|
1480
|
+
// Light dismiss covers outside clicks and Escape, never a programmatic
|
|
1481
|
+
// focus move. Without this the listbox is orphaned in the top layer.
|
|
1482
|
+
render(
|
|
1483
|
+
<>
|
|
1484
|
+
<DateTimeInput
|
|
1485
|
+
label="Meeting"
|
|
1486
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1487
|
+
timeOptionInterval={60}
|
|
1488
|
+
onChange={() => {}}
|
|
1489
|
+
/>
|
|
1490
|
+
<button type="button">after</button>
|
|
1491
|
+
</>,
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1495
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1496
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1497
|
+
|
|
1498
|
+
fireEvent.blur(timeInput, {relatedTarget: getButton('after')});
|
|
1499
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
it('closes an open list when the field goes busy', () => {
|
|
1503
|
+
const {rerender} = render(
|
|
1504
|
+
<DateTimeInput
|
|
1505
|
+
label="Meeting"
|
|
1506
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1507
|
+
timeOptionInterval={60}
|
|
1508
|
+
onChange={() => {}}
|
|
1509
|
+
/>,
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1513
|
+
fireEvent.click(timeInput);
|
|
1514
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1515
|
+
|
|
1516
|
+
rerender(
|
|
1517
|
+
<DateTimeInput
|
|
1518
|
+
label="Meeting"
|
|
1519
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1520
|
+
timeOptionInterval={60}
|
|
1521
|
+
isLoading
|
|
1522
|
+
onChange={() => {}}
|
|
1523
|
+
/>,
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
// An open dropdown over a control that no longer takes input.
|
|
1527
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
it('scales option padding with the field size', () => {
|
|
1531
|
+
const small = render(
|
|
1532
|
+
<DateTimeInput
|
|
1533
|
+
label="Small"
|
|
1534
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1535
|
+
timeOptionInterval={60}
|
|
1536
|
+
size="sm"
|
|
1537
|
+
onChange={() => {}}
|
|
1538
|
+
/>,
|
|
1539
|
+
);
|
|
1540
|
+
fireEvent.click(screen.getByLabelText('Small time'));
|
|
1541
|
+
const smallClass = optionsIn(small.container)[0].className;
|
|
1542
|
+
small.unmount();
|
|
1543
|
+
|
|
1544
|
+
const large = render(
|
|
1545
|
+
<DateTimeInput
|
|
1546
|
+
label="Large"
|
|
1547
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1548
|
+
timeOptionInterval={60}
|
|
1549
|
+
size="lg"
|
|
1550
|
+
onChange={() => {}}
|
|
1551
|
+
/>,
|
|
1552
|
+
);
|
|
1553
|
+
fireEvent.click(screen.getByLabelText('Large time'));
|
|
1554
|
+
const largeClass = optionsIn(large.container)[0].className;
|
|
1555
|
+
|
|
1556
|
+
// Rows that ignore size leave an sm field with md-sized options.
|
|
1557
|
+
expect(smallClass).not.toBe(largeClass);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
it('commits the typed time on Enter, not the option below it', () => {
|
|
1561
|
+
// Typing is free-form, so Enter must not quietly round 1:07 PM down to
|
|
1562
|
+
// the 1:00 PM option the highlight is resting on.
|
|
1563
|
+
const onChange = vi.fn();
|
|
1564
|
+
render(
|
|
1565
|
+
<DateTimeInput
|
|
1566
|
+
label="Meeting"
|
|
1567
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1568
|
+
timeOptionInterval={60}
|
|
1569
|
+
onChange={onChange}
|
|
1570
|
+
/>,
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1574
|
+
fireEvent.click(timeInput);
|
|
1575
|
+
fireEvent.change(timeInput, {target: {value: '1:07 PM'}});
|
|
1576
|
+
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1577
|
+
|
|
1578
|
+
const last = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
|
1579
|
+
expect(last).toBe('2026-03-15T13:07');
|
|
1580
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
it('names the list after timeLabel when one is given', () => {
|
|
1584
|
+
const {container} = render(
|
|
1585
|
+
<DateTimeInput
|
|
1586
|
+
label="Meeting"
|
|
1587
|
+
timeLabel="Start time"
|
|
1588
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1589
|
+
timeOptionInterval={60}
|
|
1590
|
+
onChange={() => {}}
|
|
1591
|
+
/>,
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
fireEvent.click(screen.getByLabelText('Start time'));
|
|
1595
|
+
// The list belongs to the field the consumer renamed, so it must not
|
|
1596
|
+
// announce itself under the old one.
|
|
1597
|
+
expect(container.querySelector('[role="listbox"]')).toHaveAttribute(
|
|
1598
|
+
'aria-label',
|
|
1599
|
+
'Start time options',
|
|
1600
|
+
);
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
it('opens from the keyboard after a pointer press that never became a click', () => {
|
|
1604
|
+
// The show is deferred past an in-flight click so light-dismiss cannot
|
|
1605
|
+
// eat it. A press that ends without a click must not wedge that flag.
|
|
1606
|
+
render(
|
|
1607
|
+
<DateTimeInput
|
|
1608
|
+
label="Meeting"
|
|
1609
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1610
|
+
timeOptionInterval={60}
|
|
1611
|
+
onChange={() => {}}
|
|
1612
|
+
/>,
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1616
|
+
fireEvent.pointerDown(timeInput);
|
|
1617
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1618
|
+
|
|
1619
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
it('activates the first option when the field has a date but no time', () => {
|
|
1623
|
+
// The state you land in after picking a date and no time yet. With no
|
|
1624
|
+
// active option, ArrowUp and Enter both did nothing at all.
|
|
1625
|
+
const onChange = vi.fn();
|
|
1626
|
+
const {container} = render(
|
|
1627
|
+
<DateTimeInput
|
|
1628
|
+
label="Meeting"
|
|
1629
|
+
value={'2026-03-15' as ISODateTimeString}
|
|
1630
|
+
timeOptionInterval={60}
|
|
1631
|
+
onChange={onChange}
|
|
1632
|
+
/>,
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1636
|
+
fireEvent.click(timeInput);
|
|
1637
|
+
|
|
1638
|
+
expect(timeInput).toHaveAttribute(
|
|
1639
|
+
'aria-activedescendant',
|
|
1640
|
+
optionsIn(container)[0].id,
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
// ArrowUp on the first option is a no-op, not a dead end.
|
|
1644
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1645
|
+
expect(timeInput).toHaveAttribute(
|
|
1646
|
+
'aria-activedescendant',
|
|
1647
|
+
optionsIn(container)[0].id,
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1651
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1652
|
+
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T00:00');
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
it('keeps the active option valid when the list shrinks while open', () => {
|
|
1656
|
+
const {container, rerender} = render(
|
|
1657
|
+
<DateTimeInput
|
|
1658
|
+
label="Meeting"
|
|
1659
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1660
|
+
timeOptionInterval={5}
|
|
1661
|
+
onChange={() => {}}
|
|
1662
|
+
/>,
|
|
1663
|
+
);
|
|
1664
|
+
|
|
1665
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1666
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1667
|
+
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1668
|
+
expect(optionsIn(container)).toHaveLength(288);
|
|
1669
|
+
|
|
1670
|
+
// The cadence widens under the open list, so the old index is far past
|
|
1671
|
+
// the end. A stale index leaves the user with no visible highlight and
|
|
1672
|
+
// arrow keys that appear dead until they walk it back into range.
|
|
1673
|
+
rerender(
|
|
1674
|
+
<DateTimeInput
|
|
1675
|
+
label="Meeting"
|
|
1676
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1677
|
+
timeOptionInterval={60}
|
|
1678
|
+
onChange={() => {}}
|
|
1679
|
+
/>,
|
|
1680
|
+
);
|
|
1681
|
+
|
|
1682
|
+
const options = optionsIn(container);
|
|
1683
|
+
expect(options).toHaveLength(24);
|
|
1684
|
+
expect(timeInput).toHaveAttribute(
|
|
1685
|
+
'aria-activedescendant',
|
|
1686
|
+
options[23].id,
|
|
1687
|
+
);
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
it('handles a min/max window that leaves exactly one option', () => {
|
|
1691
|
+
const onChange = vi.fn();
|
|
1692
|
+
const {container} = render(
|
|
1693
|
+
<DateTimeInput
|
|
1694
|
+
label="Meeting"
|
|
1695
|
+
value={'2026-03-15T09:20' as ISODateTimeString}
|
|
1696
|
+
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1697
|
+
max={'2026-03-15T09:30' as ISODateTimeString}
|
|
1698
|
+
timeOptionInterval={60}
|
|
1699
|
+
onChange={onChange}
|
|
1700
|
+
/>,
|
|
1701
|
+
);
|
|
1702
|
+
|
|
1703
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1704
|
+
fireEvent.click(timeInput);
|
|
1705
|
+
|
|
1706
|
+
const options = optionsIn(container);
|
|
1707
|
+
expect(options).toHaveLength(1);
|
|
1708
|
+
expect(options[0].textContent).toBe('9:00 AM');
|
|
1709
|
+
|
|
1710
|
+
// Every clamp expression collapses at length 1.
|
|
1711
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1712
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1713
|
+
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1714
|
+
expect(timeInput).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
1715
|
+
|
|
1716
|
+
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1717
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1718
|
+
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T09:00');
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
it('labels midnight and noon correctly in 12-hour format', () => {
|
|
1722
|
+
// 00:00 and 12:00 are where 12-hour formatting classically goes wrong.
|
|
1723
|
+
const {container} = render(
|
|
1724
|
+
<DateTimeInput
|
|
1725
|
+
label="Meeting"
|
|
1726
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1727
|
+
timeOptionInterval={60}
|
|
1728
|
+
onChange={() => {}}
|
|
1729
|
+
/>,
|
|
1730
|
+
);
|
|
1731
|
+
|
|
1732
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1733
|
+
const labels = labelsIn(container);
|
|
1734
|
+
expect(labels[0]).toBe('12:00 AM');
|
|
1735
|
+
expect(labels[12]).toBe('12:00 PM');
|
|
1736
|
+
expect(labels[11]).toBe('11:00 AM');
|
|
1737
|
+
expect(labels[13]).toBe('1:00 PM');
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
it('highlights the nearest earlier option for a value off the cadence', () => {
|
|
1741
|
+
const {container} = render(
|
|
1742
|
+
<DateTimeInput
|
|
1743
|
+
label="Meeting"
|
|
1744
|
+
value={'2026-03-15T14:07' as ISODateTimeString}
|
|
1745
|
+
timeOptionInterval={30}
|
|
1746
|
+
onChange={() => {}}
|
|
1747
|
+
/>,
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1750
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1751
|
+
fireEvent.click(timeInput);
|
|
1752
|
+
|
|
1753
|
+
// 14:07 sits between 14:00 (index 28) and 14:30.
|
|
1754
|
+
expect(timeInput).toHaveAttribute(
|
|
1755
|
+
'aria-activedescendant',
|
|
1756
|
+
optionsIn(container)[28].id,
|
|
1757
|
+
);
|
|
1758
|
+
// ...and nothing claims to be the selected value.
|
|
1759
|
+
expect(
|
|
1760
|
+
optionsIn(container).filter(
|
|
1761
|
+
o => o.getAttribute('aria-selected') === 'true',
|
|
1762
|
+
),
|
|
1763
|
+
).toHaveLength(0);
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
it('highlights the first option for a value below the whole range', () => {
|
|
1767
|
+
const {container} = render(
|
|
1768
|
+
<DateTimeInput
|
|
1769
|
+
label="Meeting"
|
|
1770
|
+
value={'2026-03-15T06:00' as ISODateTimeString}
|
|
1771
|
+
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1772
|
+
timeOptionInterval={60}
|
|
1773
|
+
onChange={() => {}}
|
|
1774
|
+
/>,
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1778
|
+
fireEvent.click(timeInput);
|
|
1779
|
+
|
|
1780
|
+
expect(timeInput).toHaveAttribute(
|
|
1781
|
+
'aria-activedescendant',
|
|
1782
|
+
optionsIn(container)[0].id,
|
|
1783
|
+
);
|
|
1784
|
+
expect(labelsIn(container)[0]).toBe('9:00 AM');
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
it('clamps the active option at both ends of the list', () => {
|
|
1788
|
+
const {container} = render(
|
|
1789
|
+
<DateTimeInput
|
|
1790
|
+
label="Meeting"
|
|
1791
|
+
value={'2026-03-15T00:00' as ISODateTimeString}
|
|
1792
|
+
timeOptionInterval={60}
|
|
1793
|
+
onChange={() => {}}
|
|
1794
|
+
/>,
|
|
1795
|
+
);
|
|
1796
|
+
|
|
1797
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1798
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1799
|
+
|
|
1800
|
+
// Already on the first option: ArrowUp must not wrap to the last.
|
|
1801
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1802
|
+
expect(timeInput).toHaveAttribute(
|
|
1803
|
+
'aria-activedescendant',
|
|
1804
|
+
optionsIn(container)[0].id,
|
|
1805
|
+
);
|
|
1806
|
+
|
|
1807
|
+
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1808
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1809
|
+
expect(timeInput).toHaveAttribute(
|
|
1810
|
+
'aria-activedescendant',
|
|
1811
|
+
optionsIn(container)[23].id,
|
|
1812
|
+
);
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
it('leaves Escape alone while the list is closed', () => {
|
|
1816
|
+
// Swallowing Escape here would stop an enclosing Dialog from closing.
|
|
1817
|
+
render(
|
|
1818
|
+
<DateTimeInput
|
|
1819
|
+
label="Meeting"
|
|
1820
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1821
|
+
timeOptionInterval={60}
|
|
1822
|
+
onChange={() => {}}
|
|
1823
|
+
/>,
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1827
|
+
// fireEvent returns false when the event was cancelled.
|
|
1828
|
+
expect(fireEvent.keyDown(timeInput, {key: 'Escape'})).toBe(true);
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
it('closes the list on Tab without blocking focus from leaving', () => {
|
|
1832
|
+
render(
|
|
1833
|
+
<DateTimeInput
|
|
1834
|
+
label="Meeting"
|
|
1835
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1836
|
+
timeOptionInterval={60}
|
|
1837
|
+
onChange={() => {}}
|
|
1838
|
+
/>,
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1842
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1843
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1844
|
+
|
|
1845
|
+
// Not cancelled, so the browser still moves focus to the next control.
|
|
1846
|
+
expect(fireEvent.keyDown(timeInput, {key: 'Tab'})).toBe(true);
|
|
1847
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
it('picking a time with no date selected changes nothing', () => {
|
|
1851
|
+
// Documents the component's existing contract: every time-commit path
|
|
1852
|
+
// requires a date, so the list cannot invent one.
|
|
1853
|
+
const onChange = vi.fn();
|
|
1854
|
+
const {container} = render(
|
|
1855
|
+
<DateTimeInput
|
|
1856
|
+
label="Meeting"
|
|
1857
|
+
timeOptionInterval={60}
|
|
1858
|
+
onChange={onChange}
|
|
1859
|
+
/>,
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1863
|
+
fireEvent.click(timeInput);
|
|
1864
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1865
|
+
|
|
1866
|
+
fireEvent.click(optionsIn(container)[9]);
|
|
1867
|
+
|
|
1868
|
+
// The click IS handled — the list closes — it just cannot produce a
|
|
1869
|
+
// datetime without a date, so no change is emitted and the field stays
|
|
1870
|
+
// empty rather than showing a time that was never committed.
|
|
1871
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1872
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1873
|
+
expect(timeInput).toHaveValue('');
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it('does not open the list while a change is in flight', () => {
|
|
1877
|
+
render(
|
|
1878
|
+
<DateTimeInput
|
|
1879
|
+
label="Meeting"
|
|
1880
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1881
|
+
timeOptionInterval={60}
|
|
1882
|
+
isLoading
|
|
1883
|
+
onChange={() => {}}
|
|
1884
|
+
/>,
|
|
1885
|
+
);
|
|
1886
|
+
|
|
1887
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
1888
|
+
fireEvent.click(timeInput);
|
|
1889
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1890
|
+
|
|
1891
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
it('commits seconds in the value when hasSeconds is set', () => {
|
|
1895
|
+
const onChange = vi.fn();
|
|
1896
|
+
const {container} = render(
|
|
1897
|
+
<DateTimeInput
|
|
1898
|
+
label="Meeting"
|
|
1899
|
+
value={'2026-03-15T14:00:00' as ISODateTimeString}
|
|
1900
|
+
timeOptionInterval={60}
|
|
1901
|
+
hasSeconds
|
|
1902
|
+
onChange={onChange}
|
|
1903
|
+
/>,
|
|
1904
|
+
);
|
|
1905
|
+
|
|
1906
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1907
|
+
fireEvent.click(optionsIn(container)[9]);
|
|
1908
|
+
|
|
1909
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1910
|
+
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T09:00:00');
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
it('keeps two fields on one page independent', () => {
|
|
1914
|
+
const {container} = render(
|
|
1915
|
+
<>
|
|
1916
|
+
<DateTimeInput
|
|
1917
|
+
label="Starts"
|
|
1918
|
+
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1919
|
+
timeOptionInterval={60}
|
|
1920
|
+
onChange={() => {}}
|
|
1921
|
+
/>
|
|
1922
|
+
<DateTimeInput
|
|
1923
|
+
label="Ends"
|
|
1924
|
+
value={'2026-03-15T17:00' as ISODateTimeString}
|
|
1925
|
+
timeOptionInterval={60}
|
|
1926
|
+
onChange={() => {}}
|
|
1927
|
+
/>
|
|
1928
|
+
</>,
|
|
1929
|
+
);
|
|
1930
|
+
|
|
1931
|
+
const starts = screen.getByLabelText('Starts time');
|
|
1932
|
+
const ends = screen.getByLabelText('Ends time');
|
|
1933
|
+
|
|
1934
|
+
fireEvent.click(starts);
|
|
1935
|
+
expect(starts).toHaveAttribute('aria-expanded', 'true');
|
|
1936
|
+
expect(ends).toHaveAttribute('aria-expanded', 'false');
|
|
1937
|
+
|
|
1938
|
+
const listboxes = container.querySelectorAll('[role="listbox"]');
|
|
1939
|
+
expect(listboxes).toHaveLength(1);
|
|
1940
|
+
// Option ids must not collide between instances.
|
|
1941
|
+
const ids = optionsIn(container).map(o => o.id);
|
|
1942
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
1943
|
+
expect(starts.getAttribute('aria-controls')).toBe(listboxes[0].id);
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
it('mounts no option nodes until the list is opened', () => {
|
|
1947
|
+
// The layer renders its children whether open or closed, so an
|
|
1948
|
+
// unconditional list would park a day's worth of divs in the DOM of
|
|
1949
|
+
// every opted-in field. At a 5-minute cadence that is 288 nodes.
|
|
1950
|
+
const {container} = render(
|
|
1951
|
+
<DateTimeInput
|
|
1952
|
+
label="Meeting"
|
|
1953
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1954
|
+
timeOptionInterval={5}
|
|
1955
|
+
onChange={() => {}}
|
|
1956
|
+
/>,
|
|
1957
|
+
);
|
|
1958
|
+
|
|
1959
|
+
expect(optionsIn(container)).toHaveLength(0);
|
|
1960
|
+
expect(container.querySelector('[role="listbox"]')).toBeNull();
|
|
1961
|
+
|
|
1962
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1963
|
+
expect(optionsIn(container)).toHaveLength(288);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
it('marks the selected option when the value carries unwanted seconds', () => {
|
|
1967
|
+
// splitDateTime slices the string after the T, so an external value of
|
|
1968
|
+
// 14:00:00 with hasSeconds off leaves valueParts.time as "14:00:00"
|
|
1969
|
+
// while every option is "14:00". A raw equality check silently marks
|
|
1970
|
+
// nothing selected.
|
|
1971
|
+
const {container} = render(
|
|
1972
|
+
<DateTimeInput
|
|
1973
|
+
label="Meeting"
|
|
1974
|
+
value={'2026-03-15T14:00:00' as ISODateTimeString}
|
|
1975
|
+
timeOptionInterval={60}
|
|
1976
|
+
onChange={() => {}}
|
|
1977
|
+
/>,
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1980
|
+
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1981
|
+
const selected = optionsIn(container).filter(
|
|
1982
|
+
o => o.getAttribute('aria-selected') === 'true',
|
|
1983
|
+
);
|
|
1984
|
+
expect(selected).toHaveLength(1);
|
|
1985
|
+
expect(selected[0].textContent).toBe('2:00 PM');
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
it('keeps focus in the time field when opening evicts the calendar', () => {
|
|
1989
|
+
// Both layers are popover="auto", so showing the time list closes the
|
|
1990
|
+
// calendar. The calendar's onHide returns focus to the date input —
|
|
1991
|
+
// which would rip focus out of the field the user is working in.
|
|
1992
|
+
render(
|
|
1993
|
+
<DateTimeInput
|
|
1994
|
+
label="Meeting"
|
|
1995
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1996
|
+
timeOptionInterval={60}
|
|
1997
|
+
onChange={() => {}}
|
|
1998
|
+
/>,
|
|
1999
|
+
);
|
|
2000
|
+
|
|
2001
|
+
const dateInput = screen.getAllByRole('combobox')[0];
|
|
2002
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
2003
|
+
|
|
2004
|
+
fireEvent.keyDown(dateInput, {key: 'ArrowDown'});
|
|
2005
|
+
expect(dateInput).toHaveAttribute('aria-expanded', 'true');
|
|
2006
|
+
|
|
2007
|
+
timeInput.focus();
|
|
2008
|
+
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
2009
|
+
|
|
2010
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
2011
|
+
expect(timeInput).toHaveFocus();
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
it('starts closed and opens only when asked', () => {
|
|
2015
|
+
render(
|
|
2016
|
+
<DateTimeInput
|
|
2017
|
+
label="Meeting"
|
|
2018
|
+
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2019
|
+
timeOptionInterval={60}
|
|
2020
|
+
onChange={() => {}}
|
|
2021
|
+
/>,
|
|
2022
|
+
);
|
|
2023
|
+
|
|
2024
|
+
const timeInput = screen.getByLabelText('Meeting time');
|
|
2025
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
2026
|
+
expect(timeInput).not.toHaveAttribute('aria-activedescendant');
|
|
2027
|
+
|
|
2028
|
+
fireEvent.click(timeInput);
|
|
2029
|
+
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
|
|
945
2033
|
describe('weekStartsOn', () => {
|
|
946
2034
|
// The calendar popover renders in the top layer; jsdom keeps the content in
|
|
947
2035
|
// the DOM but role queries skip it, so read the columnheaders directly.
|