@astryxdesign/core 0.7.6-canary.f73623d → 0.7.6-canary.f899313
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +2 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +8 -2
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +17 -18
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +8 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +27 -2
- package/dist/FieldStatus/FieldStatus.d.ts +3 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +1 -0
- package/dist/FileInput/FileInput.d.ts +3 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +59 -33
- package/dist/Layer/useTouchTrigger.d.ts +9 -0
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
- package/dist/Layer/useTouchTrigger.js +1 -1
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +10 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +19 -9
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +11 -15
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +9 -6
- package/dist/Selector/hooks.js +2 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +82 -10
- package/dist/Token/Token.d.ts +1 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +15 -0
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +16 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +10 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +36 -10
- package/dist/astryx.css +1 -1
- package/dist/i18n/useListFormatter.d.ts +9 -0
- package/dist/i18n/useListFormatter.d.ts.map +1 -0
- package/dist/i18n/useListFormatter.js +41 -0
- package/package.json +18 -5
- package/src/Badge/Badge.tsx +2 -1
- package/src/Chat/ChatMessage.spec.md +64 -65
- package/src/Citation/Citation.spec.md +60 -47
- package/src/ComplexSelector/ComplexSelector.tsx +19 -2
- package/src/DateInput/DateInput.spec.md +74 -57
- package/src/DateInput/MonthScroller.tsx +15 -17
- package/src/DateRangeInput/DateRangeInput.tsx +18 -3
- package/src/DropdownMenu/DropdownMenuGroup.tsx +2 -1
- package/src/Field/Field.doc.mjs +2 -1
- package/src/Field/FieldLabel.doc.mjs +4 -2
- package/src/Field/FieldLabel.spec.md +30 -21
- package/src/Field/FieldLabel.tsx +37 -3
- package/src/FieldStatus/FieldStatus.tsx +7 -0
- package/src/FileInput/FileInput.tsx +69 -28
- package/src/FormLayout/FormLayout.spec.md +11 -19
- package/src/Layer/useTouchTrigger.ts +1 -1
- package/src/Lightbox/Lightbox.spec.md +249 -61
- package/src/Link/Link.tsx +9 -2
- package/src/MultiSelector/MultiSelector.spec.md +121 -114
- package/src/MultiSelector/MultiSelector.tsx +19 -5
- package/src/NumberInput/NumberInput.tsx +11 -18
- package/src/ProgressBar/ProgressBar.spec.md +143 -119
- package/src/Selector/Selector.tsx +16 -6
- package/src/Selector/hooks.ts +2 -2
- package/src/Slider/Slider.spec.md +18 -16
- package/src/Slider/Slider.tsx +101 -9
- package/src/Token/Token.spec.md +24 -23
- package/src/Token/Token.tsx +15 -0
- package/src/Tokenizer/Tokenizer.tsx +10 -0
- package/src/Typeahead/BaseTypeahead.tsx +18 -2
- package/src/Typeahead/Typeahead.tsx +40 -9
- package/src/i18n/useListFormatter.ts +42 -0
- package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.d.ts +0 -2
- package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.js +0 -132
- package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.d.ts +0 -2
- package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.js +0 -78
- package/dist/Lightbox/LightboxViewport.a11y.browser.spec.d.ts +0 -2
- package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +0 -66
- package/dist/List/ListMarkers.a11y.chromium.spec.d.ts +0 -2
- package/dist/List/ListMarkers.a11y.chromium.spec.js +0 -677
- package/dist/Markdown/Markdown.fr23.bench.d.ts +0 -2
- package/dist/Markdown/Markdown.fr23.bench.d.ts.map +0 -1
- package/dist/Markdown/Markdown.fr23.bench.js +0 -152
- package/dist/Markdown/MarkdownTable.a11y.chromium.spec.d.ts +0 -2
- package/dist/Markdown/MarkdownTable.a11y.chromium.spec.js +0 -590
- package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.d.ts +0 -2
- package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.js +0 -657
- package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.d.ts +0 -2
- package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.js +0 -153
- package/dist/MultiSelector/CreateRow.a11y.chromium.spec.d.ts +0 -2
- package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +0 -117
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +0 -2
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +0 -49
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.d.ts +0 -2
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +0 -273
- package/dist/Outline/OutlineFocus.a11y.chromium.spec.d.ts +0 -2
- package/dist/Outline/OutlineFocus.a11y.chromium.spec.js +0 -421
- package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.d.ts +0 -2
- package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.js +0 -118
- package/dist/reset.a11y.chromium.spec.d.ts +0 -2
- package/dist/reset.a11y.chromium.spec.js +0 -231
- package/dist/reset.until-found.a11y.browser.spec.d.ts +0 -2
- package/dist/reset.until-found.a11y.browser.spec.js +0 -72
- package/src/AlertDialog/AlertDialog.test.tsx +0 -519
- package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +0 -128
- package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +0 -52
- package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +0 -68
- package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +0 -144
- package/src/AlertDialog/useImperativeAlertDialog.test.tsx +0 -163
- package/src/AppShell/AppShell.test.tsx +0 -1310
- package/src/AppShell/__tests__/AppShellMainKeyboard.a11y.browser.spec.ts +0 -534
- package/src/AppShell/observeMainOverflow.test.ts +0 -173
- package/src/AspectRatio/AspectRatio.test.tsx +0 -398
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +0 -158
- package/src/Avatar/Avatar.test.tsx +0 -867
- package/src/Avatar/AvatarStatusDot.test.tsx +0 -306
- package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +0 -126
- package/src/AvatarGroup/AvatarGroup.test.tsx +0 -313
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +0 -263
- package/src/Badge/Badge.test.tsx +0 -125
- package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +0 -260
- package/src/Banner/Banner.test.tsx +0 -879
- package/src/Blockquote/Blockquote.test.tsx +0 -121
- package/src/BottomSheet/BottomSheet.test.tsx +0 -3154
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +0 -248
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +0 -195
- package/src/BottomSheet/BottomSheetPanel.test.tsx +0 -549
- package/src/BottomSheet/BottomSheetReplace.test.tsx +0 -1638
- package/src/BottomSheet/BottomSheetStacking.announce.test.tsx +0 -246
- package/src/BottomSheet/BottomSheetStacking.test.tsx +0 -3184
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +0 -2010
- package/src/BottomSheet/__tests__/BottomSheetDrag.a11y.chromium.spec.ts +0 -129
- package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +0 -97
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +0 -415
- package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +0 -462
- package/src/BottomSheet/__tests__/BottomSheetPinchZoom.a11y.chromium.spec.ts +0 -247
- package/src/BottomSheet/__tests__/BottomSheetToastOutlet.a11y.chromium.spec.ts +0 -83
- package/src/BottomSheet/snapOffsets.test.ts +0 -254
- package/src/BottomSheet/useSheetGestures.test.ts +0 -1690
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +0 -921
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +0 -132
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.renders.tsx +0 -45
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.states.ts +0 -81
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.test.tsx +0 -125
- package/src/Button/Button.test.tsx +0 -864
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +0 -355
- package/src/Button/__tests__/Button.a11y.harnesses.tsx +0 -47
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -25
- package/src/Button/__tests__/Button.a11y.renders.tsx +0 -139
- package/src/Button/__tests__/Button.a11y.states.ts +0 -317
- package/src/Button/__tests__/Button.a11y.test.tsx +0 -187
- package/src/ButtonGroup/ButtonGroup.test.tsx +0 -1025
- package/src/Calendar/Calendar.test.tsx +0 -2186
- package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +0 -141
- package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +0 -12
- package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +0 -57
- package/src/Calendar/__tests__/Calendar.a11y.states.ts +0 -139
- package/src/Calendar/__tests__/Calendar.a11y.test.tsx +0 -137
- package/src/Calendar/dayCellUtils.test.ts +0 -520
- package/src/Calendar/getInitialFocusDate.test.ts +0 -94
- package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +0 -54
- package/src/Calendar/hooks/useCalendarConstraints.test.ts +0 -153
- package/src/Calendar/hooks/useCalendarDays.test.ts +0 -265
- package/src/Calendar/hooks/useCalendarNavigation.test.ts +0 -252
- package/src/Card/Card.test.tsx +0 -202
- package/src/Carousel/Carousel.test.tsx +0 -1036
- package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +0 -207
- package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +0 -12
- package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +0 -39
- package/src/Carousel/__tests__/Carousel.a11y.states.ts +0 -139
- package/src/Carousel/__tests__/Carousel.a11y.test.tsx +0 -127
- package/src/Center/Center.test.tsx +0 -406
- package/src/Chat/ChatComposer.test.tsx +0 -364
- package/src/Chat/ChatComposerDrawer.test.tsx +0 -130
- package/src/Chat/ChatComposerInput.test.tsx +0 -2136
- package/src/Chat/ChatComposerTokenElement.test.tsx +0 -85
- package/src/Chat/ChatDictationButton.test.tsx +0 -108
- package/src/Chat/ChatLayout.test.tsx +0 -311
- package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -99
- package/src/Chat/ChatMessage.test.tsx +0 -344
- package/src/Chat/ChatMessageBubble.test.tsx +0 -294
- package/src/Chat/ChatMessageList.test.tsx +0 -198
- package/src/Chat/ChatMessageMetadata.test.tsx +0 -66
- package/src/Chat/ChatPastedTextToken.test.tsx +0 -71
- package/src/Chat/ChatSendButton.test.tsx +0 -309
- package/src/Chat/ChatSystemMessage.test.tsx +0 -189
- package/src/Chat/ChatTokenizedText.test.tsx +0 -163
- package/src/Chat/ChatToolCalls.test.tsx +0 -413
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -577
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
- package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +0 -457
- package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +0 -91
- package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +0 -91
- package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +0 -465
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +0 -416
- package/src/Chat/__tests__/ChatSendButton.a11y.chromium.spec.ts +0 -1523
- package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +0 -1004
- package/src/Chat/__tests__/ChatTokenizedText.a11y.chromium.spec.ts +0 -574
- package/src/Chat/__tests__/useChatStreamScroll.a11y.chromium.spec.ts +0 -263
- package/src/Chat/__tests__/useChatStreamScroll.fixture.tsx +0 -61
- package/src/Chat/useChatDictation.test.tsx +0 -369
- package/src/Chat/useChatNewMessages.test.tsx +0 -209
- package/src/Chat/useChatPasteAsToken.test.ts +0 -191
- package/src/Chat/useChatStreamScroll.test.tsx +0 -381
- package/src/Chat/useTriggerMenu.test.tsx +0 -994
- package/src/CheckboxInput/CheckboxInput.test.tsx +0 -926
- package/src/CheckboxInput/__tests__/Checkbox.a11y.chromium.spec.ts +0 -354
- package/src/CheckboxInput/__tests__/Checkbox.a11y.known-failures.ts +0 -54
- package/src/CheckboxInput/__tests__/Checkbox.a11y.renders.tsx +0 -310
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +0 -404
- package/src/CheckboxInput/__tests__/Checkbox.a11y.test.tsx +0 -128
- package/src/CheckboxList/CheckboxList.fr10.test.tsx +0 -51
- package/src/CheckboxList/CheckboxList.test.tsx +0 -1159
- package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +0 -1998
- package/src/Citation/Citation.test.tsx +0 -361
- package/src/ClickableCard/ClickableCard.test.tsx +0 -382
- package/src/Code/Code.test.tsx +0 -75
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +0 -87
- package/src/CodeBlock/CodeBlock.test.tsx +0 -752
- package/src/CodeBlock/CodeLanguage.test.tsx +0 -173
- package/src/CodeBlock/__tests__/CodeBlockCopyClearance.a11y.chromium.spec.ts +0 -325
- package/src/CodeBlock/__tests__/CodeBlockLanguages.a11y.chromium.spec.ts +0 -64
- package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +0 -126
- package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +0 -94
- package/src/CodeBlock/highlightRanges.test.ts +0 -253
- package/src/CodeBlock/languages.test.ts +0 -402
- package/src/CodeBlock/tokenizer.test.ts +0 -280
- package/src/CodeBlock/tokenizerBudget.test.ts +0 -112
- package/src/Collapsible/Collapsible.test.tsx +0 -620
- package/src/Collapsible/CollapsibleGroup.test.tsx +0 -656
- package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +0 -194
- package/src/Collapsible/__tests__/Collapsible.a11y.states.ts +0 -117
- package/src/Collapsible/__tests__/Collapsible.a11y.test.tsx +0 -140
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.chromium.spec.ts +0 -559
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +0 -45
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +0 -139
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +0 -135
- package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +0 -141
- package/src/Collapsible/useCollapsible.test.tsx +0 -236
- package/src/CommandPalette/CommandPalette.perf.test.ts +0 -100
- package/src/CommandPalette/CommandPalette.test.tsx +0 -750
- package/src/CommandPalette/CommandPaletteEmpty.test.tsx +0 -101
- package/src/CommandPalette/CommandPaletteFooter.test.tsx +0 -122
- package/src/CommandPalette/CommandPaletteGroup.test.tsx +0 -129
- package/src/CommandPalette/CommandPaletteInput.test.tsx +0 -539
- package/src/CommandPalette/CommandPaletteItem.test.tsx +0 -417
- package/src/CommandPalette/CommandPaletteList.test.tsx +0 -184
- package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +0 -600
- package/src/CommandPalette/__tests__/CommandPaletteEmptyAudit.a11y.chromium.spec.ts +0 -715
- package/src/CommandPalette/__tests__/CommandPaletteFooter.a11y.chromium.spec.ts +0 -582
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +0 -336
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +0 -523
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +0 -76
- package/src/ComplexSelector/ComplexSelector.fr10.test.tsx +0 -262
- package/src/ComplexSelector/ComplexSelector.test.tsx +0 -1171
- package/src/ComplexSelector/__tests__/ComplexSelector.a11y.chromium.spec.ts +0 -965
- package/src/ContextMenu/ContextMenu.test.tsx +0 -1617
- package/src/ContextMenu/ContextMenu.triggerProps.test.tsx +0 -54
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +0 -439
- package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +0 -154
- package/src/ContextMenu/__tests__/ContextMenuLongPress.a11y.chromium.spec.ts +0 -117
- package/src/DateInput/DateInput.fr10.test.tsx +0 -283
- package/src/DateInput/DateInput.test.tsx +0 -1571
- package/src/DateInput/DateInputTouch.test.tsx +0 -2948
- package/src/DateInput/MonthScrollerSettle.test.tsx +0 -165
- package/src/DateInput/NativeDateField.test.tsx +0 -741
- package/src/DateInput/Presentation.test.tsx +0 -121
- package/src/DateInput/__tests__/DateInputAudit.a11y.chromium.spec.ts +0 -577
- package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +0 -312
- package/src/DateInput/__tests__/WheelPinchZoom.a11y.chromium.spec.ts +0 -171
- package/src/DateRangeInput/DateRangeInput.fr10.test.tsx +0 -165
- package/src/DateRangeInput/DateRangeInput.test.tsx +0 -1151
- package/src/DateTimeInput/DateTimeInput.fr10.test.tsx +0 -319
- package/src/DateTimeInput/DateTimeInput.test.tsx +0 -2656
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +0 -1071
- package/src/DateTimeInput/NativePickerSegments.test.tsx +0 -657
- package/src/DateTimeInput/Presentation.test.tsx +0 -89
- package/src/Dialog/Dialog.source-build.test.mjs +0 -43
- package/src/Dialog/Dialog.test.tsx +0 -1599
- package/src/Dialog/DialogHeader.test.tsx +0 -477
- package/src/Dialog/__tests__/Dialog.a11y.browser.spec.ts +0 -111
- package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +0 -218
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -28
- package/src/Dialog/__tests__/Dialog.a11y.renders.tsx +0 -107
- package/src/Dialog/__tests__/Dialog.a11y.states.ts +0 -104
- package/src/Dialog/__tests__/Dialog.a11y.test.tsx +0 -102
- package/src/Dialog/__tests__/Dialog.reduced-motion.a11y.browser.spec.ts +0 -252
- package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +0 -202
- package/src/Dialog/useImperativeDialog.test.tsx +0 -78
- package/src/Divider/Divider.test.tsx +0 -155
- package/src/Drawer/Drawer.test.tsx +0 -1335
- package/src/Drawer/DrawerDirection.test.tsx +0 -127
- package/src/Drawer/DrawerHeader.test.tsx +0 -102
- package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +0 -402
- package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +0 -298
- package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +0 -267
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +0 -93
- package/src/DropdownMenu/DropdownMenu.test.tsx +0 -4498
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +0 -22
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +0 -126
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -635
- package/src/DropdownMenu/DropdownMenuSheetFirstOpen.test.tsx +0 -72
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +0 -2442
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +0 -281
- package/src/DropdownMenu/__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts +0 -175
- package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +0 -74
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +0 -335
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +0 -367
- package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +0 -328
- package/src/DropdownMenu/__tests__/DropdownMenuRadioItemAudit.a11y.chromium.spec.ts +0 -540
- package/src/DropdownMenu/__tests__/DropdownMenuSubMenuAudit.a11y.chromium.spec.ts +0 -671
- package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +0 -158
- package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +0 -26
- package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +0 -65
- package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +0 -138
- package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +0 -136
- package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +0 -149
- package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +0 -12
- package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +0 -67
- package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +0 -141
- package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +0 -138
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +0 -231
- package/src/DropdownMenu/__tests__/MenuHoverFocus.a11y.chromium.spec.ts +0 -238
- package/src/DropdownMenu/__tests__/MenuPinchZoom.a11y.chromium.spec.ts +0 -173
- package/src/DropdownMenu/__tests__/MenuPress.a11y.chromium.spec.ts +0 -503
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +0 -268
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +0 -43
- package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +0 -60
- package/src/EmptyState/EmptyState.announce.test.tsx +0 -227
- package/src/EmptyState/EmptyState.test.tsx +0 -284
- package/src/Field/Field.test.tsx +0 -800
- package/src/Field/FieldLabel.test.tsx +0 -244
- package/src/Field/InputClearButton.test.tsx +0 -395
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +0 -297
- package/src/Field/fieldInputFamily.test.tsx +0 -160
- package/src/FieldStatus/FieldStatus.test.tsx +0 -482
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +0 -206
- package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +0 -13
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -287
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +0 -302
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +0 -154
- package/src/FileInput/FileInput.fr10.test.tsx +0 -21
- package/src/FileInput/FileInput.test.tsx +0 -1291
- package/src/FormLayout/FormLayout.test.tsx +0 -444
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -46
- package/src/Grid/Grid.test.tsx +0 -509
- package/src/HStack/HStack.test.tsx +0 -129
- package/src/Heading/Heading.test.tsx +0 -328
- package/src/HoverCard/HoverCard.test.tsx +0 -1339
- package/src/Icon/Icon.test.tsx +0 -990
- package/src/Icon/globalIconRegistry.test.tsx +0 -613
- package/src/IconButton/IconButton.test.tsx +0 -156
- package/src/Indicator/Indicator.test.tsx +0 -389
- package/src/Indicator/indicatorRegistry.test.tsx +0 -106
- package/src/InputGroup/InputGroup.test.tsx +0 -502
- package/src/InteractiveRoleContext/InteractiveRoleContext.test.tsx +0 -185
- package/src/Item/Item.source-build.test.mjs +0 -88
- package/src/Item/Item.swipeLayer.test.tsx +0 -74
- package/src/Item/Item.test.tsx +0 -1515
- package/src/Item/ItemMarkerColumns.a11y.chromium.spec.ts +0 -211
- package/src/Item/__tests__/ItemSwipeActions.a11y.chromium.spec.ts +0 -337
- package/src/Item/__tests__/ItemSwipePinchZoom.a11y.chromium.spec.ts +0 -186
- package/src/Kbd/Kbd.test.tsx +0 -299
- package/src/Layer/LayerProvider.test.tsx +0 -143
- package/src/Layer/__tests__/LayerBrowserHide.a11y.chromium.spec.ts +0 -126
- package/src/Layer/__tests__/LayerReducedMotion.a11y.chromium.spec.ts +0 -176
- package/src/Layer/__tests__/TouchTrigger.a11y.chromium.spec.ts +0 -200
- package/src/Layer/anchorName.test.ts +0 -146
- package/src/Layer/layerDismissalFamilies.test.tsx +0 -487
- package/src/Layer/layerDismissalInvariants.test.tsx +0 -501
- package/src/Layer/layerHost.test.ts +0 -117
- package/src/Layer/layerScopedContext.test.tsx +0 -499
- package/src/Layer/layerViewportInset.source-build.test.mjs +0 -90
- package/src/Layer/layerViewportInset.test.ts +0 -120
- package/src/Layer/popoverOperations.test.ts +0 -205
- package/src/Layer/useLayer.test.tsx +0 -2845
- package/src/Layer/useLayerDismissal.test.tsx +0 -580
- package/src/Layer/useTouchTrigger.test.ts +0 -70
- package/src/Layout/Layout.test.tsx +0 -375
- package/src/Layout/LayoutSlots.test.tsx +0 -376
- package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +0 -134
- package/src/Layout/__tests__/Layout.a11y.renders.tsx +0 -110
- package/src/Layout/__tests__/Layout.a11y.states.ts +0 -93
- package/src/Layout/__tests__/Layout.a11y.test.tsx +0 -142
- package/src/Layout/__tests__/childrenAsContent.test.tsx +0 -59
- package/src/Layout/__tests__/contentWidth.test.tsx +0 -303
- package/src/Layout/__tests__/edgeCompensation.test.tsx +0 -191
- package/src/Layout/__tests__/nestedPadding.test.tsx +0 -264
- package/src/Layout/overlayPaddingReset.test.tsx +0 -224
- package/src/Lightbox/Lightbox.test.tsx +0 -837
- package/src/Lightbox/LightboxKeyboard.a11y.browser.spec.ts +0 -78
- package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +0 -74
- package/src/Lightbox/useLightbox.test.tsx +0 -263
- package/src/Link/Link.test.tsx +0 -457
- package/src/Link/__tests__/Link.a11y.chromium.spec.ts +0 -153
- package/src/Link/__tests__/Link.a11y.renders.tsx +0 -97
- package/src/Link/__tests__/Link.a11y.states.ts +0 -104
- package/src/Link/__tests__/Link.a11y.test.tsx +0 -127
- package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -262
- package/src/Link/__tests__/Link.truncation.a11y.chromium.spec.ts +0 -58
- package/src/Link/useLinkComponent.test.tsx +0 -572
- package/src/Link/useLinkify.test.tsx +0 -146
- package/src/List/List.test.tsx +0 -1052
- package/src/List/ListItem.source-build.test.mjs +0 -41
- package/src/List/ListMarkers.a11y.chromium.spec.ts +0 -904
- package/src/Markdown/Markdown.codeLanguages.test.tsx +0 -137
- package/src/Markdown/Markdown.fr23.bench.ts +0 -176
- package/src/Markdown/Markdown.fr23.perf.test.ts +0 -484
- package/src/Markdown/Markdown.helpers.perf.test.ts +0 -495
- package/src/Markdown/Markdown.public.test.ts +0 -309
- package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
- package/src/Markdown/Markdown.rerender.test.tsx +0 -143
- package/src/Markdown/Markdown.test.tsx +0 -1383
- package/src/Markdown/MarkdownTable.a11y.chromium.spec.ts +0 -727
- package/src/Markdown/__tests__/MarkdownNesting.a11y.chromium.spec.ts +0 -102
- package/src/Markdown/angleDestinations.test.tsx +0 -104
- package/src/Markdown/ast.test.ts +0 -374
- package/src/Markdown/atxHeadingIndentation.test.tsx +0 -53
- package/src/Markdown/blockNesting.test.tsx +0 -124
- package/src/Markdown/blockquoteMarkers.test.tsx +0 -69
- package/src/Markdown/characterReferences.test.ts +0 -47
- package/src/Markdown/codeSpans.test.tsx +0 -65
- package/src/Markdown/emphasis.test.tsx +0 -214
- package/src/Markdown/fenceInfo.test.tsx +0 -47
- package/src/Markdown/imageAlt.test.tsx +0 -84
- package/src/Markdown/incremental.test.ts +0 -1198
- package/src/Markdown/incrementalIndentation.test.tsx +0 -36
- package/src/Markdown/indentedFences.test.tsx +0 -374
- package/src/Markdown/indentedTables.test.tsx +0 -53
- package/src/Markdown/linkBrackets.test.tsx +0 -116
- package/src/Markdown/linkDestinationLinear.test.tsx +0 -54
- package/src/Markdown/linkDestinations.test.tsx +0 -127
- package/src/Markdown/linkTextCode.test.tsx +0 -65
- package/src/Markdown/listBullets.test.tsx +0 -55
- package/src/Markdown/listContinuation.test.tsx +0 -94
- package/src/Markdown/listThematicBreak.test.tsx +0 -49
- package/src/Markdown/longLinkLabels.test.tsx +0 -49
- package/src/Markdown/openFenceFinalNewline.test.tsx +0 -85
- package/src/Markdown/parser.perf.test.ts +0 -398
- package/src/Markdown/parser.public.test.ts +0 -144
- package/src/Markdown/parser.test.ts +0 -1925
- package/src/Markdown/plugin-renderer/MarkdownPluginNodeRenderer.test.tsx +0 -300
- package/src/Markdown/plugins/frontmatter.test.tsx +0 -150
- package/src/Markdown/plugins/headingLinks.a11y.chromium.spec.ts +0 -856
- package/src/Markdown/plugins/headingLinks.ssr.test.tsx +0 -143
- package/src/Markdown/plugins/headingLinks.test.tsx +0 -637
- package/src/Markdown/plugins/protocol.test.tsx +0 -1083
- package/src/Markdown/plugins/semanticFence.test.tsx +0 -655
- package/src/Markdown/plugins/softBreaks.test.tsx +0 -172
- package/src/Markdown/plugins/sourceDecoration.test.tsx +0 -799
- package/src/Markdown/plugins/sourceLines.test.tsx +0 -715
- package/src/Markdown/plugins/textTransform.test.ts +0 -467
- package/src/Markdown/remark.test.tsx +0 -1877
- package/src/Markdown/streaming.test.ts +0 -148
- package/src/Markdown/streamingListShapes.test.tsx +0 -93
- package/src/Markdown/trailingSpaces.test.tsx +0 -45
- package/src/MetadataList/MetadataList.test.tsx +0 -383
- package/src/MobileNav/MobileNav.test.tsx +0 -296
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +0 -584
- package/src/MobileNav/MobileNavCloseTiming.test.ts +0 -58
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +0 -172
- package/src/MobileNav/MobileNavEntryAnimation.test.tsx +0 -212
- package/src/MobileNav/MobileNavFocus.test.tsx +0 -553
- package/src/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.ts +0 -147
- package/src/MobileNav/MobileNavReopen.test.tsx +0 -118
- package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +0 -109
- package/src/MobileNav/MobileNavToggle.test.tsx +0 -179
- package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +0 -89
- package/src/MobileNav/__tests__/MobileNavWidth.a11y.chromium.spec.ts +0 -103
- package/src/MoreMenu/MoreMenu.test.tsx +0 -367
- package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +0 -148
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +0 -72
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +0 -121
- package/src/MultiSelector/MultiSelector.fr10.test.tsx +0 -221
- package/src/MultiSelector/MultiSelector.test.tsx +0 -4971
- package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +0 -310
- package/src/MultiSelector/__tests__/MultiSelector.a11y.chromium.spec.ts +0 -46
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +0 -135
- package/src/NavIcon/NavIcon.test.tsx +0 -41
- package/src/NavMenu/NavHeadingMenu.test.tsx +0 -714
- package/src/NavMenu/__tests__/NavMenusSlideSelection.a11y.chromium.spec.ts +0 -137
- package/src/NumberInput/NumberInput.fr10.test.tsx +0 -21
- package/src/NumberInput/NumberInput.test.tsx +0 -2697
- package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +0 -124
- package/src/NumberInput/__tests__/NumberInput.a11y.known-failures.ts +0 -26
- package/src/NumberInput/__tests__/NumberInput.a11y.renders.tsx +0 -77
- package/src/NumberInput/__tests__/NumberInput.a11y.states.ts +0 -116
- package/src/NumberInput/__tests__/NumberInput.a11y.test.tsx +0 -154
- package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +0 -102
- package/src/NumberInput/numberInputCommit.test.ts +0 -72
- package/src/NumberInput/numberParser.docblock.test.ts +0 -234
- package/src/NumberInput/numberParser.test.ts +0 -222
- package/src/Outline/Outline.test.tsx +0 -1134
- package/src/Outline/OutlineFocus.a11y.chromium.spec.ts +0 -551
- package/src/Outline/parseOutlineFromMarkdown.test.ts +0 -104
- package/src/OverflowList/OverflowList.test.tsx +0 -1073
- package/src/Overlay/Overlay.test.tsx +0 -424
- package/src/Pagination/Pagination.test.tsx +0 -1754
- package/src/Popover/Popover.test.tsx +0 -1508
- package/src/Popover/PopoverPresentation.test.tsx +0 -952
- package/src/Popover/__tests__/PopoverSecondDocument.a11y.chromium.spec.ts +0 -267
- package/src/PowerSearch/PowerSearch.test.tsx +0 -1027
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +0 -1190
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +0 -377
- package/src/PowerSearch/PowerSearchToken.test.tsx +0 -230
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -754
- package/src/PowerSearch/formatFilterValue.test.ts +0 -339
- package/src/PowerSearch/resolveDateTimeRangePart.test.ts +0 -40
- package/src/PowerSearch/usePowerSearchConfig.test.ts +0 -832
- package/src/PowerSearch/usePowerSearchSource.test.ts +0 -621
- package/src/ProgressBar/ProgressBar.test.tsx +0 -928
- package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +0 -127
- package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +0 -12
- package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +0 -38
- package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +0 -123
- package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +0 -107
- package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +0 -831
- package/src/RadioList/RadioList.fr10.test.tsx +0 -67
- package/src/RadioList/RadioList.test.tsx +0 -1051
- package/src/RadioList/RadioListItem.press.source-build.test.mjs +0 -80
- package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +0 -274
- package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +0 -12
- package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +0 -232
- package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +0 -503
- package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +0 -217
- package/src/Resizable/ResizeHandle.test.tsx +0 -1067
- package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +0 -127
- package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +0 -39
- package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +0 -80
- package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +0 -197
- package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +0 -123
- package/src/Resizable/shippedDirections.test.ts +0 -462
- package/src/Resizable/useResizable.test.ts +0 -1725
- package/src/Resizable/utils.test.ts +0 -74
- package/src/ScrollableArea/ScrollableArea.test.tsx +0 -304
- package/src/Section/Section.test.tsx +0 -376
- package/src/SegmentedControl/SegmentedControl.test.tsx +0 -981
- package/src/SelectableCard/SelectableCard.test.tsx +0 -216
- package/src/Selector/Selector.focusableDisabled.test.tsx +0 -127
- package/src/Selector/Selector.fr10.test.tsx +0 -149
- package/src/Selector/Selector.sheetActiveDescendant.test.tsx +0 -95
- package/src/Selector/Selector.source-build.test.mjs +0 -79
- package/src/Selector/Selector.test.tsx +0 -5117
- package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +0 -27
- package/src/Selector/__tests__/Combobox.a11y.states.ts +0 -145
- package/src/Selector/__tests__/ComboboxField.a11y.chromium.spec.ts +0 -458
- package/src/Selector/__tests__/EmptyStateAnnouncement.a11y.chromium.spec.ts +0 -91
- package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +0 -131
- package/src/Selector/__tests__/Listbox.a11y.dom.ts +0 -38
- package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +0 -63
- package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +0 -12
- package/src/Selector/__tests__/Listbox.a11y.states.ts +0 -232
- package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +0 -46
- package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +0 -109
- package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +0 -133
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +0 -128
- package/src/Selector/useCombobox.public.test.tsx +0 -89
- package/src/Selector/useCombobox.test.tsx +0 -444
- package/src/SideNav/SideNav.test.tsx +0 -4275
- package/src/SideNav/SideNavRailMotion.test.tsx +0 -620
- package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +0 -542
- package/src/SizeContext/SizeContext.test.tsx +0 -226
- package/src/Skeleton/Skeleton.test.tsx +0 -148
- package/src/Slider/Slider.fr10.test.tsx +0 -93
- package/src/Slider/Slider.test.tsx +0 -1661
- package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +0 -125
- package/src/Slider/__tests__/Slider.a11y.known-failures.ts +0 -12
- package/src/Slider/__tests__/Slider.a11y.renders.tsx +0 -42
- package/src/Slider/__tests__/Slider.a11y.states.ts +0 -106
- package/src/Slider/__tests__/Slider.a11y.test.tsx +0 -105
- package/src/Spinner/Spinner.test.tsx +0 -484
- package/src/Stack/Stack.test.tsx +0 -473
- package/src/Stack/StackItem.test.tsx +0 -105
- package/src/StatusDot/StatusDot.test.tsx +0 -213
- package/src/Stepper/Stepper.public.test.ts +0 -91
- package/src/Stepper/Stepper.test.tsx +0 -2130
- package/src/Switch/Switch.focusRing.source-build.test.mjs +0 -81
- package/src/Switch/Switch.test.tsx +0 -1136
- package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +0 -211
- package/src/Switch/__tests__/Switch.a11y.known-failures.ts +0 -28
- package/src/Switch/__tests__/Switch.a11y.states.ts +0 -275
- package/src/Switch/__tests__/Switch.a11y.test.tsx +0 -144
- package/src/TabList/TabList.test.tsx +0 -1755
- package/src/TabList/__tests__/TabListOverflow.interaction.browser.spec.ts +0 -99
- package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +0 -163
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +0 -199
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +0 -12
- package/src/TabList/__tests__/Tabs.a11y.renders.tsx +0 -92
- package/src/TabList/__tests__/Tabs.a11y.states.ts +0 -247
- package/src/TabList/__tests__/Tabs.a11y.test.tsx +0 -153
- package/src/Table/Table.perf.test.tsx +0 -569
- package/src/Table/Table.test.tsx +0 -1883
- package/src/Table/TableHeaderCell.test.tsx +0 -291
- package/src/Table/TableSelectionToolbar.test.tsx +0 -109
- package/src/Table/__tests__/Table.a11y.chromium.spec.ts +0 -141
- package/src/Table/__tests__/Table.a11y.contracts.ts +0 -35
- package/src/Table/__tests__/Table.a11y.renders.tsx +0 -112
- package/src/Table/__tests__/Table.a11y.states.ts +0 -136
- package/src/Table/__tests__/Table.a11y.test.tsx +0 -198
- package/src/Table/__tests__/TableMinWidth.a11y.chromium.spec.ts +0 -75
- package/src/Table/__tests__/TableRowGlide.a11y.browser.spec.ts +0 -382
- package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +0 -138
- package/src/Table/__tests__/TableStickyDividers.a11y.browser.spec.ts +0 -560
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +0 -941
- package/src/Table/plugins/columnSettings/useTableColumnSettings.test.tsx +0 -175
- package/src/Table/plugins/columnSettings/useTableColumnSettingsState.test.tsx +0 -296
- package/src/Table/plugins/filtering/useTableFilterState.test.tsx +0 -143
- package/src/Table/plugins/filtering/useTableFiltering.test.tsx +0 -357
- package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +0 -112
- package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +0 -409
- package/src/Table/plugins/pagination/useTablePagination-perf.test.tsx +0 -232
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +0 -666
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.glide.test.tsx +0 -280
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -681
- package/src/Table/plugins/rowIndex/useTableRowIndex.test.tsx +0 -132
- package/src/Table/plugins/rowStatus/useTableRowStatus.public.test.ts +0 -92
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +0 -519
- package/src/Table/plugins/selection/useTableSelection-perf.test.tsx +0 -258
- package/src/Table/plugins/selection/useTableSelection.test.tsx +0 -424
- package/src/Table/plugins/selection/useTableSelectionState.test.tsx +0 -451
- package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.test.tsx +0 -174
- package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.ts +0 -86
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +0 -786
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +0 -580
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +0 -334
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +0 -163
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +0 -992
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +0 -740
- package/src/Table/tableContextMenu.test.tsx +0 -208
- package/src/Text/Text.test.tsx +0 -405
- package/src/Text/useTruncation.test.tsx +0 -196
- package/src/TextArea/TextArea.fr10.test.tsx +0 -21
- package/src/TextArea/TextArea.test.tsx +0 -1317
- package/src/TextInput/TextInput.fr10.test.tsx +0 -21
- package/src/TextInput/TextInput.test.tsx +0 -1280
- package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +0 -214
- package/src/TextInput/__tests__/TextInput.a11y.known-failures.ts +0 -40
- package/src/TextInput/__tests__/TextInput.a11y.renders.tsx +0 -202
- package/src/TextInput/__tests__/TextInput.a11y.states.ts +0 -384
- package/src/TextInput/__tests__/TextInput.a11y.test.tsx +0 -130
- package/src/Thumbnail/Thumbnail.test.tsx +0 -384
- package/src/TimeInput/NativeTimeInput.test.tsx +0 -343
- package/src/TimeInput/Presentation.test.tsx +0 -192
- package/src/TimeInput/TimeInput.fr10.test.tsx +0 -312
- package/src/TimeInput/TimeInput.test.tsx +0 -907
- package/src/Timer/Timer.test.tsx +0 -302
- package/src/Timestamp/Timestamp.test.tsx +0 -1661
- package/src/Timestamp/tooltipEntries.test.ts +0 -566
- package/src/Toast/ToastViewport.modalHost.test.tsx +0 -891
- package/src/Toast/ToastViewport.test.tsx +0 -3130
- package/src/Toast/useToast.test.tsx +0 -258
- package/src/ToggleButton/ToggleButton.test.tsx +0 -1000
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +0 -177
- package/src/ToggleButton/__tests__/ToggleButton.a11y.renders.tsx +0 -133
- package/src/ToggleButton/__tests__/ToggleButton.a11y.states.ts +0 -167
- package/src/ToggleButton/__tests__/ToggleButton.a11y.test.tsx +0 -103
- package/src/Token/Token.test.tsx +0 -707
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +0 -87
- package/src/Tokenizer/Tokenizer.fr10.test.tsx +0 -28
- package/src/Tokenizer/Tokenizer.props-type.test.tsx +0 -126
- package/src/Tokenizer/Tokenizer.ssr.test.tsx +0 -196
- package/src/Tokenizer/Tokenizer.test.tsx +0 -2216
- package/src/Tokenizer/TokenizerBottomSheet.test.tsx +0 -2738
- package/src/Tokenizer/__tests__/TokenizerEndLane.a11y.chromium.spec.ts +0 -505
- package/src/Toolbar/Toolbar.test.tsx +0 -600
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +0 -176
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +0 -63
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +0 -142
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +0 -126
- package/src/Tooltip/Tooltip.test.tsx +0 -819
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +0 -141
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +0 -86
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +0 -132
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +0 -126
- package/src/Tooltip/__tests__/TooltipFocusReturn.a11y.chromium.spec.ts +0 -130
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +0 -75
- package/src/Tooltip/__tests__/TooltipTallContent.a11y.browser.spec.ts +0 -673
- package/src/Tooltip/__tests__/TooltipTallContentTracking.a11y.browser.spec.ts +0 -1162
- package/src/Tooltip/useTooltip.test.tsx +0 -483
- package/src/TopNav/TopNav.test.tsx +0 -863
- package/src/TopNav/TopNavMegaMenu.reduced-motion.source-build.test.mjs +0 -83
- package/src/TopNav/TopNavMegaMenu.test.tsx +0 -1153
- package/src/TopNav/TopNavMenu.test.tsx +0 -502
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +0 -75
- package/src/TreeList/TreeList.test.tsx +0 -1737
- package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +0 -138
- package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +0 -44
- package/src/TreeList/__tests__/TreeList.a11y.states.ts +0 -164
- package/src/TreeList/__tests__/TreeList.a11y.test.tsx +0 -138
- package/src/TreeList/__tests__/TreeListGuideAlignment.interaction.browser.spec.ts +0 -127
- package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +0 -83
- package/src/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.ts +0 -138
- package/src/Typeahead/BaseTypeahead.test.tsx +0 -553
- package/src/Typeahead/Typeahead.fr10.test.tsx +0 -28
- package/src/Typeahead/Typeahead.test.tsx +0 -2984
- package/src/Typeahead/TypeaheadItem.test.tsx +0 -134
- package/src/VStack/VStack.test.tsx +0 -128
- package/src/VisuallyHidden/VisuallyHidden.test.tsx +0 -115
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +0 -853
- package/src/__tests__/CardPress.a11y.chromium.spec.ts +0 -353
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +0 -87
- package/src/__tests__/PressModality.a11y.chromium.spec.ts +0 -392
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +0 -814
- package/src/__tests__/TestIcon.tsx +0 -26
- package/src/__tests__/babelPluginAddExtensions.test.ts +0 -190
- package/src/__tests__/fastRoleQueries.ts +0 -66
- package/src/__tests__/forcedColors.ts +0 -60
- package/src/__tests__/internalModulesNotExperimental.test.ts +0 -88
- package/src/__tests__/pressState.ts +0 -216
- package/src/__tests__/stubMatchMedia.test.ts +0 -79
- package/src/__tests__/stubMatchMedia.ts +0 -95
- package/src/__tests__/stylexDeclarations.ts +0 -51
- package/src/__tests__/unthemedLength.ts +0 -34
- package/src/docPropLiterals.test.ts +0 -964
- package/src/docPropReferences.test.ts +0 -178
- package/src/docs-script.test.mjs +0 -55
- package/src/gestures/__tests__/GestureHost.test.ts +0 -2136
- package/src/gestures/__tests__/LongPressGestureRecognizer.test.ts +0 -141
- package/src/gestures/__tests__/PanGestureRecognizer.test.ts +0 -327
- package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +0 -74
- package/src/gestures/__tests__/PressTracker.test.ts +0 -275
- package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +0 -498
- package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +0 -108
- package/src/gestures/__tests__/TapGestureRecognizer.test.ts +0 -154
- package/src/gestures/__tests__/VelocityTracker.streams.test.ts +0 -243
- package/src/gestures/__tests__/VelocityTracker.test.ts +0 -261
- package/src/gestures/__tests__/arena.test.ts +0 -864
- package/src/gestures/__tests__/arenaContinuous.test.ts +0 -207
- package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +0 -193
- package/src/gestures/__tests__/arenaTapLongPress.test.ts +0 -211
- package/src/gestures/__tests__/browser/build-fixture.mjs +0 -46
- package/src/gestures/__tests__/browser/fixture.ts +0 -307
- package/src/gestures/__tests__/browser/gestures.interaction.browser.spec.ts +0 -1137
- package/src/gestures/__tests__/browserNegotiation.test.ts +0 -293
- package/src/gestures/__tests__/fixtures/pointer-streams/README.md +0 -82
- package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-fling.json +0 -130
- package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-slow.json +0 -860
- package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-stop-release.json +0 -357
- package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-fling.json +0 -66
- package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-slow.json +0 -268
- package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-stop-release.json +0 -128
- package/src/gestures/__tests__/fixtures/pointer-streams/capture.html +0 -123
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-fling.json +0 -64
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-slow.json +0 -268
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-stop-release.json +0 -124
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-fling.json +0 -64
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-slow.json +0 -268
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-stop-release.json +0 -124
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-fling.json +0 -84
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-slow.json +0 -381
- package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-stop-release.json +0 -171
- package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-fling.json +0 -45
- package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-slow.json +0 -101
- package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-stop-release.json +0 -130
- package/src/gestures/__tests__/haptics.test.ts +0 -187
- package/src/gestures/__tests__/harness.ts +0 -252
- package/src/gestures/__tests__/pressTrackerFactory.test.ts +0 -59
- package/src/gestures/__tests__/recognizerKind.test.ts +0 -78
- package/src/gestures/__tests__/touchActionRule.test.ts +0 -54
- package/src/gestures/__tests__/useGestureRecognizer.test.tsx +0 -570
- package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +0 -144
- package/src/hooks/__tests__/menuLinkClicks.ts +0 -71
- package/src/hooks/actionGroupTracker.exports.test.ts +0 -28
- package/src/hooks/actionGroupTracker.test.ts +0 -510
- package/src/hooks/computeOverflow.test.ts +0 -422
- package/src/hooks/deprecatedApis.public.test.ts +0 -33
- package/src/hooks/menuPressGesture.test.ts +0 -253
- package/src/hooks/scrollKeyboardDelegation.test.ts +0 -188
- package/src/hooks/scrollbarGutter.test.ts +0 -167
- package/src/hooks/useAnnounce.modal.test.tsx +0 -1291
- package/src/hooks/useAnnounce.replace.test.tsx +0 -516
- package/src/hooks/useAnnounce.test.tsx +0 -293
- package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +0 -235
- package/src/hooks/useAutoMediaMode.test.ts +0 -109
- package/src/hooks/useBusyAnnouncement.adopters.test.tsx +0 -69
- package/src/hooks/useBusyAnnouncement.test.tsx +0 -112
- package/src/hooks/useClickableContainer.test.tsx +0 -236
- package/src/hooks/useClipboard.test.tsx +0 -151
- package/src/hooks/useContainerReveal.test.tsx +0 -163
- package/src/hooks/useDevWarning.test.tsx +0 -45
- package/src/hooks/useEntryAnimation.ssr.test.tsx +0 -143
- package/src/hooks/useFocusTrap.test.tsx +0 -635
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +0 -166
- package/src/hooks/useGridFocus.test.tsx +0 -165
- package/src/hooks/useHotkeys.test.ts +0 -265
- package/src/hooks/useImageMode.test.ts +0 -133
- package/src/hooks/useInputContainer.test.tsx +0 -180
- package/src/hooks/useInputStatusIcon.test.tsx +0 -240
- package/src/hooks/useIsHydrating.test.tsx +0 -70
- package/src/hooks/useKeyboardHint.test.tsx +0 -92
- package/src/hooks/useListFocus.test.tsx +0 -664
- package/src/hooks/useLongPress.test.tsx +0 -152
- package/src/hooks/useMediaQuery.test.ts +0 -165
- package/src/hooks/useMenuHover.test.tsx +0 -536
- package/src/hooks/useMenuPress.test.tsx +0 -565
- package/src/hooks/useMergedRefs.test.tsx +0 -62
- package/src/hooks/useOverflow.test.ts +0 -504
- package/src/hooks/useRenamedProp.test.tsx +0 -95
- package/src/hooks/useScrollLock.test.ts +0 -248
- package/src/hooks/useScrollOverflow.test.tsx +0 -323
- package/src/hooks/useScrollableArea.test.tsx +0 -836
- package/src/hooks/useStreamingText.test.ts +0 -237
- package/src/hooks/useTreeFocus.test.tsx +0 -351
- package/src/hooks/useTypeahead.test.tsx +0 -351
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +0 -128
- package/src/i18n/__tests__/e2e-pagination.test.tsx +0 -172
- package/src/i18n/__tests__/e2e-powersearch.test.tsx +0 -166
- package/src/i18n/__tests__/getLocaleDirection.test.ts +0 -40
- package/src/i18n/__tests__/resolve.test.ts +0 -325
- package/src/i18n/__tests__/useCollator.test.tsx +0 -115
- package/src/i18n/__tests__/useDirection.test.tsx +0 -66
- package/src/i18n/__tests__/useLocale.test.tsx +0 -52
- package/src/i18n/useTranslator.test.tsx +0 -267
- package/src/inputFontFloor.test.ts +0 -106
- package/src/internal/tokenizer-presentation.test.ts +0 -29
- package/src/internal/touch.test.ts +0 -44
- package/src/motion/__tests__/animatedLayout.interaction.browser.spec.ts +0 -1613
- package/src/motion/__tests__/animatedLayoutAdopters.interaction.browser.spec.ts +0 -217
- package/src/motion/__tests__/animatedLayoutBundle.ts +0 -17
- package/src/motion/__tests__/animator.interaction.browser.spec.ts +0 -843
- package/src/motion/__tests__/motionBundle.ts +0 -14
- package/src/motion/animatedLayout.boundary.test.ts +0 -155
- package/src/motion/animatedLayout.swap.test.ts +0 -827
- package/src/motion/animatedLayout.test.ts +0 -1154
- package/src/motion/animatedLayoutAdopters.test.tsx +0 -500
- package/src/motion/animator.test.ts +0 -3746
- package/src/motion/bezier.test.ts +0 -52
- package/src/motion/coast.test.ts +0 -99
- package/src/motion/decay.test.ts +0 -120
- package/src/motion/projection.test.ts +0 -107
- package/src/motion/reducedMotion.test.ts +0 -129
- package/src/motion/reducedMotionPlan.test.ts +0 -208
- package/src/motion/resolveMotion.test.ts +0 -123
- package/src/motion/resolveSnapPoint.test.ts +0 -189
- package/src/motion/roles.test.ts +0 -273
- package/src/motion/rubberBand.test.ts +0 -88
- package/src/motion/selectPage.test.ts +0 -31
- package/src/motion/spring.test.ts +0 -650
- package/src/motion/uikit/gestures.test.ts +0 -89
- package/src/motion/uikit/springRows.test.ts +0 -71
- package/src/motion/uikit/tableImports.test.ts +0 -293
- package/src/motion/uikit/uikit.test.ts +0 -364
- package/src/motion/uikit/values.test.ts +0 -121
- package/src/naming.test.ts +0 -44
- package/src/postinstall.test.mjs +0 -216
- package/src/reset.a11y.chromium.spec.ts +0 -238
- package/src/reset.test.ts +0 -205
- package/src/reset.until-found.a11y.browser.spec.ts +0 -76
- package/src/reset.until-found.test.ts +0 -32
- package/src/serverSafeComponents.test.ts +0 -675
- package/src/stylexValidation.source-build.test.mjs +0 -42
- package/src/tailwind-theme.test.ts +0 -153
- package/src/theme/MediaTheme.dom.test.tsx +0 -97
- package/src/theme/Theme.test.tsx +0 -337
- package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +0 -199
- package/src/theme/contrast.test.ts +0 -94
- package/src/theme/declarationBoundary.test.ts +0 -126
- package/src/theme/defineTheme.test.ts +0 -2028
- package/src/theme/derivedVarRegistry.test.ts +0 -592
- package/src/theme/expandColorScale.test.ts +0 -651
- package/src/theme/expandMotionScale.test.ts +0 -145
- package/src/theme/expandRadiusScale.test.ts +0 -59
- package/src/theme/expandTypeScale.test.ts +0 -351
- package/src/theme/extensibleAxes.test.ts +0 -432
- package/src/theme/generateThemeRules.test.ts +0 -1870
- package/src/theme/hct.test.ts +0 -93
- package/src/theme/onMediaTokens.test.ts +0 -431
- package/src/theme/publicThemeHelperContract.test.ts +0 -67
- package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +0 -91
- package/src/theme/syntax/serverSafeSyntax.test.ts +0 -68
- package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +0 -40
- package/src/theme/themeAdaptations.test.ts +0 -1795
- package/src/theme/themeMotion.test.ts +0 -620
- package/src/theme/themeRegistry.test.ts +0 -59
- package/src/theme/themingTargets.test.ts +0 -589
- package/src/theme/tokenValueCompat.test.ts +0 -204
- package/src/theme/tokens.test.ts +0 -441
- package/src/theme/useTheme.test.tsx +0 -322
- package/src/unicodePropertyEscapes.source-build.test.mjs +0 -69
- package/src/utils/chainEventHandlers.test.ts +0 -39
- package/src/utils/characters.test.ts +0 -141
- package/src/utils/color.test.ts +0 -233
- package/src/utils/composeEventHandlers.test.ts +0 -71
- package/src/utils/customPropertyArmLayering.test.ts +0 -131
- package/src/utils/dateParser.test.ts +0 -402
- package/src/utils/dateTypes.test.ts +0 -40
- package/src/utils/devWarning.test.ts +0 -56
- package/src/utils/edgeAutoscroll.test.ts +0 -192
- package/src/utils/focusReturn.test.ts +0 -119
- package/src/utils/getKey.test.ts +0 -25
- package/src/utils/getNodeText.test.ts +0 -24
- package/src/utils/hitArea.test.ts +0 -628
- package/src/utils/ime.test.ts +0 -32
- package/src/utils/inputAria.test.ts +0 -39
- package/src/utils/interactionModality.test.ts +0 -144
- package/src/utils/isApplePlatform.test.ts +0 -61
- package/src/utils/mergeProps.test.ts +0 -166
- package/src/utils/mergeRefs.test.ts +0 -48
- package/src/utils/parseStyleKey.test.ts +0 -56
- package/src/utils/plainDate.test.ts +0 -938
- package/src/utils/presentation.test.ts +0 -380
- package/src/utils/pressFeedback.test.ts +0 -1012
- package/src/utils/pressGesture.test.ts +0 -332
- package/src/utils/pressPaintPriority.test.ts +0 -80
- package/src/utils/pressPaintSource.test.ts +0 -65
- package/src/utils/pressableCoverage.test.ts +0 -192
- package/src/utils/primaryClick.test.ts +0 -38
- package/src/utils/safeUrl.test.ts +0 -101
- package/src/utils/sharedMutationObserver.test.ts +0 -106
- package/src/utils/sharedResizeObserver.test.ts +0 -283
- package/src/utils/themeProps.test.ts +0 -62
- package/src/utils/timeFormat.test.ts +0 -204
- package/src/utils/timeParser.test.ts +0 -295
- package/src/utils/touchPresentation.test.ts +0 -462
- package/src/utils/unicodeDigits.test.ts +0 -44
|
@@ -1,2656 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file DateTimeInput.test.tsx
|
|
5
|
-
* @input Uses vitest, @testing-library/react, DateTimeInput component
|
|
6
|
-
* @output Unit tests for DateTimeInput component behavior
|
|
7
|
-
* @position Testing; validates DateTimeInput.tsx implementation
|
|
8
|
-
*
|
|
9
|
-
* SYNC: When DateTimeInput.tsx changes, update tests to match new behavior
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {useState} from 'react';
|
|
13
|
-
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
14
|
-
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
15
|
-
import userEvent from '@testing-library/user-event';
|
|
16
|
-
import * as stylex from '@stylexjs/stylex';
|
|
17
|
-
import {colorVars} from '../theme/tokens.stylex';
|
|
18
|
-
import {getButton, queryButton} from '../__tests__/fastRoleQueries';
|
|
19
|
-
import {DateTimeInput} from './DateTimeInput';
|
|
20
|
-
import type {ISODateTimeString} from './DateTimeInput';
|
|
21
|
-
import {Icon} from '../Icon';
|
|
22
|
-
import {defineTheme} from '../theme/defineTheme';
|
|
23
|
-
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
24
|
-
import {InternationalizationProvider} from '../i18n';
|
|
25
|
-
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
26
|
-
|
|
27
|
-
function politeRegion(): HTMLElement | null {
|
|
28
|
-
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
__resetLiveRegionsForTest();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
36
|
-
const {prose, component} = generateThemeCSS(theme);
|
|
37
|
-
return [prose, component].filter(Boolean).join('\n\n');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// jsdom does not perform flex layout, so these probe classes verify the
|
|
41
|
-
// declarations that switch between horizontal and stacked intrinsic layouts.
|
|
42
|
-
const responsiveLayoutProbe = stylex.create({
|
|
43
|
-
responsiveRow: {
|
|
44
|
-
flexWrap: 'wrap',
|
|
45
|
-
},
|
|
46
|
-
responsiveSegment: {
|
|
47
|
-
flexBasis: 196,
|
|
48
|
-
minWidth: 0,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
function expectResponsiveProbeClasses(
|
|
53
|
-
element: HTMLElement,
|
|
54
|
-
style: (typeof responsiveLayoutProbe)[keyof typeof responsiveLayoutProbe],
|
|
55
|
-
): void {
|
|
56
|
-
const classes = (stylex.props(style).className ?? '')
|
|
57
|
-
.split(' ')
|
|
58
|
-
.filter(className => className !== '' && !className.includes('__'));
|
|
59
|
-
expect(classes.length).toBeGreaterThan(0);
|
|
60
|
-
for (const className of classes) {
|
|
61
|
-
expect(element).toHaveClass(className);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
describe('DateTimeInput', () => {
|
|
66
|
-
it('renders with label', () => {
|
|
67
|
-
render(<DateTimeInput label="Meeting time" onChange={() => {}} />);
|
|
68
|
-
expect(screen.getByLabelText('Meeting time')).toBeInTheDocument();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('updates the committed date display when provider locale changes', () => {
|
|
72
|
-
const renderDateTimeInput = (locale: 'en-US' | 'es-ES') => (
|
|
73
|
-
<InternationalizationProvider locale={locale}>
|
|
74
|
-
<DateTimeInput
|
|
75
|
-
label="Meeting"
|
|
76
|
-
value={'2026-01-25T14:30' as ISODateTimeString}
|
|
77
|
-
onChange={() => {}}
|
|
78
|
-
/>
|
|
79
|
-
</InternationalizationProvider>
|
|
80
|
-
);
|
|
81
|
-
const {rerender} = render(renderDateTimeInput('en-US'));
|
|
82
|
-
|
|
83
|
-
expect(screen.getByRole('combobox')).toHaveValue('January 25, 2026');
|
|
84
|
-
|
|
85
|
-
rerender(renderDateTimeInput('es-ES'));
|
|
86
|
-
expect(screen.getByRole('combobox')).toHaveValue('25 de enero de 2026');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('derives the time input label from the field label (forms-15)', () => {
|
|
90
|
-
render(<DateTimeInput label="Meeting time" onChange={() => {}} />);
|
|
91
|
-
// Not a hardcoded "Time" — tied to the field label so it is localizable
|
|
92
|
-
// and unambiguous when multiple date-time fields share a page.
|
|
93
|
-
expect(screen.getByLabelText('Meeting time time')).toBeInTheDocument();
|
|
94
|
-
expect(screen.queryByLabelText('Time')).not.toBeInTheDocument();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('uses an explicit timeLabel when provided', () => {
|
|
98
|
-
render(
|
|
99
|
-
<DateTimeInput
|
|
100
|
-
label="Meeting time"
|
|
101
|
-
timeLabel="Start time"
|
|
102
|
-
onChange={() => {}}
|
|
103
|
-
/>,
|
|
104
|
-
);
|
|
105
|
-
expect(screen.getByLabelText('Start time')).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('renders with placeholder', () => {
|
|
109
|
-
render(
|
|
110
|
-
<DateTimeInput
|
|
111
|
-
label="Time"
|
|
112
|
-
onChange={() => {}}
|
|
113
|
-
placeholder="Pick a date"
|
|
114
|
-
/>,
|
|
115
|
-
);
|
|
116
|
-
expect(screen.getByPlaceholderText('Pick a date')).toBeInTheDocument();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('defaults the time portion placeholder to "Select a time"', () => {
|
|
120
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
121
|
-
expect(screen.getByPlaceholderText('Select a time')).toBeInTheDocument();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('applies a custom timePlaceholder to the time portion', () => {
|
|
125
|
-
render(
|
|
126
|
-
<DateTimeInput
|
|
127
|
-
label="Meeting"
|
|
128
|
-
onChange={() => {}}
|
|
129
|
-
placeholder="Pick a date"
|
|
130
|
-
timePlaceholder="Pick a time"
|
|
131
|
-
/>,
|
|
132
|
-
);
|
|
133
|
-
// Time portion uses the override; date portion keeps its own placeholder.
|
|
134
|
-
expect(screen.getByPlaceholderText('Pick a time')).toBeInTheDocument();
|
|
135
|
-
expect(screen.getByPlaceholderText('Pick a date')).toBeInTheDocument();
|
|
136
|
-
expect(
|
|
137
|
-
screen.queryByPlaceholderText('Select a time'),
|
|
138
|
-
).not.toBeInTheDocument();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('renders both date and time inputs', () => {
|
|
142
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
143
|
-
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
144
|
-
expect(screen.getByLabelText('Meeting time')).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('allows the date and time segments to wrap when they no longer fit', () => {
|
|
148
|
-
const {container} = render(
|
|
149
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
150
|
-
);
|
|
151
|
-
const row = container.querySelector(
|
|
152
|
-
'.astryx-date-time-input',
|
|
153
|
-
) as HTMLElement;
|
|
154
|
-
expect(row).not.toBeNull();
|
|
155
|
-
expectResponsiveProbeClasses(row, responsiveLayoutProbe.responsiveRow);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('gives both segments the intrinsic wrap threshold and allows them to shrink', () => {
|
|
159
|
-
const {container} = render(
|
|
160
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
161
|
-
);
|
|
162
|
-
const dateSegment = container.querySelector(
|
|
163
|
-
'.astryx-date-time-input-date-segment',
|
|
164
|
-
) as HTMLElement;
|
|
165
|
-
const timeSegment = container.querySelector(
|
|
166
|
-
'.astryx-date-time-input-time-segment',
|
|
167
|
-
) as HTMLElement;
|
|
168
|
-
expectResponsiveProbeClasses(
|
|
169
|
-
dateSegment,
|
|
170
|
-
responsiveLayoutProbe.responsiveSegment,
|
|
171
|
-
);
|
|
172
|
-
expectResponsiveProbeClasses(
|
|
173
|
-
timeSegment,
|
|
174
|
-
responsiveLayoutProbe.responsiveSegment,
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('does not commit the date on a composing Enter (IME)', () => {
|
|
179
|
-
const onChange = vi.fn();
|
|
180
|
-
render(<DateTimeInput label="Meeting" onChange={onChange} />);
|
|
181
|
-
const dateInput = screen.getByRole('combobox');
|
|
182
|
-
fireEvent.change(dateInput, {target: {value: '03/15/2026'}});
|
|
183
|
-
onChange.mockClear();
|
|
184
|
-
|
|
185
|
-
// The composing keydown (isComposing / legacy keyCode 229) that commits an
|
|
186
|
-
// IME candidate must not be read as "commit the typed date".
|
|
187
|
-
fireEvent.keyDown(dateInput, {key: 'Enter', isComposing: true});
|
|
188
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
189
|
-
fireEvent.keyDown(dateInput, {key: 'Enter', keyCode: 229});
|
|
190
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
191
|
-
|
|
192
|
-
// A real, non-composing Enter still commits.
|
|
193
|
-
fireEvent.keyDown(dateInput, {key: 'Enter'});
|
|
194
|
-
expect(onChange).toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('does not step the time on a composing ArrowUp (IME)', () => {
|
|
198
|
-
const onChange = vi.fn();
|
|
199
|
-
render(
|
|
200
|
-
<DateTimeInput
|
|
201
|
-
label="Meeting"
|
|
202
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
203
|
-
onChange={onChange}
|
|
204
|
-
/>,
|
|
205
|
-
);
|
|
206
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
207
|
-
|
|
208
|
-
// An IME candidate window navigates with the arrows; a composing ArrowUp
|
|
209
|
-
// must not step the time value.
|
|
210
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp', isComposing: true});
|
|
211
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
212
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp', keyCode: 229});
|
|
213
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
214
|
-
|
|
215
|
-
// A real, non-composing ArrowUp still steps the time by one minute.
|
|
216
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
217
|
-
expect(onChange).toHaveBeenCalledWith('2026-03-15T14:31');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('displays formatted date in date input when value is provided', () => {
|
|
221
|
-
render(
|
|
222
|
-
<DateTimeInput
|
|
223
|
-
label="Meeting"
|
|
224
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
225
|
-
onChange={() => {}}
|
|
226
|
-
/>,
|
|
227
|
-
);
|
|
228
|
-
expect(screen.getByDisplayValue('March 15, 2026')).toBeInTheDocument();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('displays formatted time in time input when value is provided (12h)', () => {
|
|
232
|
-
render(
|
|
233
|
-
<DateTimeInput
|
|
234
|
-
label="Meeting"
|
|
235
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
236
|
-
onChange={() => {}}
|
|
237
|
-
/>,
|
|
238
|
-
);
|
|
239
|
-
expect(screen.getByDisplayValue('2:30 PM')).toBeInTheDocument();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("follows the provider locale's hour cycle when hourFormat is unset", () => {
|
|
243
|
-
const renderIn = (locale: string) => (
|
|
244
|
-
<InternationalizationProvider locale={locale}>
|
|
245
|
-
<DateTimeInput
|
|
246
|
-
label="Meeting"
|
|
247
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
248
|
-
onChange={() => {}}
|
|
249
|
-
/>
|
|
250
|
-
</InternationalizationProvider>
|
|
251
|
-
);
|
|
252
|
-
const {rerender} = render(renderIn('en-US'));
|
|
253
|
-
expect(screen.getByDisplayValue('2:30 PM')).toBeInTheDocument();
|
|
254
|
-
|
|
255
|
-
rerender(renderIn('de-DE'));
|
|
256
|
-
expect(screen.getByDisplayValue('14:30')).toBeInTheDocument();
|
|
257
|
-
|
|
258
|
-
rerender(renderIn('ko-KR'));
|
|
259
|
-
expect(screen.getByDisplayValue('오후 2:30')).toBeInTheDocument();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('displays formatted time in 24h format', () => {
|
|
263
|
-
render(
|
|
264
|
-
<DateTimeInput
|
|
265
|
-
label="Meeting"
|
|
266
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
267
|
-
onChange={() => {}}
|
|
268
|
-
hourFormat="24h"
|
|
269
|
-
/>,
|
|
270
|
-
);
|
|
271
|
-
expect(screen.getByDisplayValue('14:30')).toBeInTheDocument();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('displays time with seconds', () => {
|
|
275
|
-
render(
|
|
276
|
-
<DateTimeInput
|
|
277
|
-
label="Timestamp"
|
|
278
|
-
value={'2026-03-15T14:30:45' as ISODateTimeString}
|
|
279
|
-
onChange={() => {}}
|
|
280
|
-
hasSeconds
|
|
281
|
-
/>,
|
|
282
|
-
);
|
|
283
|
-
expect(screen.getByDisplayValue('2:30:45 PM')).toBeInTheDocument();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('forwards ref to date input', () => {
|
|
287
|
-
const ref = vi.fn();
|
|
288
|
-
render(<DateTimeInput ref={ref} label="Meeting" onChange={() => {}} />);
|
|
289
|
-
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('visually hides label when isLabelHidden is true', () => {
|
|
293
|
-
render(<DateTimeInput label="Meeting" isLabelHidden onChange={() => {}} />);
|
|
294
|
-
const label = screen.getByText('Meeting');
|
|
295
|
-
expect(label).toBeInTheDocument();
|
|
296
|
-
expect(screen.getByLabelText('Meeting')).toBeInTheDocument();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('sets aria-required when isRequired is true', () => {
|
|
300
|
-
render(<DateTimeInput label="Meeting" isRequired onChange={() => {}} />);
|
|
301
|
-
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
302
|
-
'aria-required',
|
|
303
|
-
'true',
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('does not set aria-required when isRequired is false', () => {
|
|
308
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
309
|
-
expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-required');
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('keeps both disabled inputs focusable without native disabled (AST-085 FR1)', () => {
|
|
313
|
-
render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
|
|
314
|
-
for (const input of [
|
|
315
|
-
screen.getByRole('combobox'),
|
|
316
|
-
screen.getByLabelText('Meeting time'),
|
|
317
|
-
]) {
|
|
318
|
-
expect(input).not.toHaveAttribute('disabled');
|
|
319
|
-
expect(input).toHaveAttribute('aria-disabled', 'true');
|
|
320
|
-
expect(input).toHaveAttribute('readonly');
|
|
321
|
-
input.focus();
|
|
322
|
-
expect(input).toHaveFocus();
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('restores the focus ring on a focused disabled field (AST-085 FR6)', () => {
|
|
327
|
-
// The same declarations as the field's ring style compile to the same
|
|
328
|
-
// atomic classes; the shared disabled style alone drops them.
|
|
329
|
-
const ringClasses = stylex
|
|
330
|
-
.props(
|
|
331
|
-
stylex.create({
|
|
332
|
-
ring: {
|
|
333
|
-
borderColor: {
|
|
334
|
-
default: colorVars['--color-border-emphasized'],
|
|
335
|
-
':focus-within': colorVars['--color-accent'],
|
|
336
|
-
},
|
|
337
|
-
boxShadow: {
|
|
338
|
-
default: 'none',
|
|
339
|
-
':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
}).ring,
|
|
343
|
-
)
|
|
344
|
-
.className!.split(' ')
|
|
345
|
-
.filter(name => !name.includes('__'));
|
|
346
|
-
render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
|
|
347
|
-
for (const input of [
|
|
348
|
-
screen.getByRole('combobox'),
|
|
349
|
-
screen.getByLabelText('Meeting time'),
|
|
350
|
-
]) {
|
|
351
|
-
const wrapper = input.closest(
|
|
352
|
-
'.astryx-date-time-input-date-segment, .astryx-date-time-input-time-segment',
|
|
353
|
-
)!;
|
|
354
|
-
for (const name of ringClasses) {
|
|
355
|
-
expect(wrapper).toHaveClass(name);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('is not disabled by default', () => {
|
|
361
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
362
|
-
expect(screen.getByRole('combobox')).not.toBeDisabled();
|
|
363
|
-
expect(screen.getByLabelText('Meeting time')).not.toBeDisabled();
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('date input has role="combobox"', () => {
|
|
367
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
368
|
-
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('date input has aria-haspopup="dialog"', () => {
|
|
372
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
373
|
-
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
374
|
-
'aria-haspopup',
|
|
375
|
-
'dialog',
|
|
376
|
-
);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('date input has aria-expanded=false by default', () => {
|
|
380
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
381
|
-
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
382
|
-
'aria-expanded',
|
|
383
|
-
'false',
|
|
384
|
-
);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('opens the calendar popover on ArrowDown (keyboard, forms-13)', () => {
|
|
388
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
389
|
-
const input = screen.getByRole('combobox');
|
|
390
|
-
input.focus();
|
|
391
|
-
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
392
|
-
fireEvent.keyDown(input, {key: 'ArrowDown'});
|
|
393
|
-
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
394
|
-
// skipAutoFocus keeps focus in the input, per the APG date-picker pattern
|
|
395
|
-
expect(input).toHaveFocus();
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it('opens the calendar popover on Alt+ArrowDown (keyboard, forms-13)', () => {
|
|
399
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
400
|
-
const input = screen.getByRole('combobox');
|
|
401
|
-
fireEvent.keyDown(input, {key: 'ArrowDown', altKey: true});
|
|
402
|
-
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('does not open on ArrowDown when disabled', () => {
|
|
406
|
-
render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
|
|
407
|
-
const input = screen.getByRole('combobox');
|
|
408
|
-
fireEvent.keyDown(input, {key: 'ArrowDown'});
|
|
409
|
-
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it('does not re-trigger on ArrowDown when the calendar is already open', () => {
|
|
413
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
414
|
-
const input = screen.getByRole('combobox');
|
|
415
|
-
fireEvent.keyDown(input, {key: 'ArrowDown'});
|
|
416
|
-
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
417
|
-
// A second ArrowDown is a no-op: the calendar stays open
|
|
418
|
-
fireEvent.keyDown(input, {key: 'ArrowDown'});
|
|
419
|
-
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it('calendar button is focusable and clickable', () => {
|
|
423
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
424
|
-
const button = getButton('Open calendar');
|
|
425
|
-
expect(button).toBeInTheDocument();
|
|
426
|
-
expect(button).not.toBeDisabled();
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('calendar button is disabled when isDisabled is true', () => {
|
|
430
|
-
render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
|
|
431
|
-
const button = getButton('Open calendar');
|
|
432
|
-
expect(button).not.toHaveAttribute('disabled');
|
|
433
|
-
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
434
|
-
fireEvent.click(button);
|
|
435
|
-
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
436
|
-
'aria-expanded',
|
|
437
|
-
'false',
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('disables inputs and button when isLoading is true', () => {
|
|
442
|
-
render(<DateTimeInput label="Meeting" isLoading onChange={() => {}} />);
|
|
443
|
-
for (const el of [
|
|
444
|
-
screen.getByRole('combobox'),
|
|
445
|
-
screen.getByLabelText('Meeting time'),
|
|
446
|
-
getButton('Open calendar'),
|
|
447
|
-
]) {
|
|
448
|
-
expect(el).not.toHaveAttribute('disabled');
|
|
449
|
-
expect(el).toHaveAttribute('aria-disabled', 'true');
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it('sets aria-busy when isLoading is true', () => {
|
|
454
|
-
render(<DateTimeInput label="Meeting" isLoading onChange={() => {}} />);
|
|
455
|
-
expect(screen.getByRole('combobox')).toHaveAttribute('aria-busy', 'true');
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('does not set aria-busy when not loading', () => {
|
|
459
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
460
|
-
expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-busy');
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it('sets aria-busy on the time input when isLoading is true', () => {
|
|
464
|
-
render(<DateTimeInput label="Meeting" isLoading onChange={() => {}} />);
|
|
465
|
-
expect(screen.getByLabelText('Meeting time')).toHaveAttribute(
|
|
466
|
-
'aria-busy',
|
|
467
|
-
'true',
|
|
468
|
-
);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it('does not set aria-busy on the time input when not loading', () => {
|
|
472
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
473
|
-
expect(screen.getByLabelText('Meeting time')).not.toHaveAttribute(
|
|
474
|
-
'aria-busy',
|
|
475
|
-
);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it('renders status icon for error status', () => {
|
|
479
|
-
render(
|
|
480
|
-
<DateTimeInput
|
|
481
|
-
label="Meeting"
|
|
482
|
-
onChange={() => {}}
|
|
483
|
-
status={{type: 'error', message: 'Invalid datetime'}}
|
|
484
|
-
/>,
|
|
485
|
-
);
|
|
486
|
-
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
487
|
-
'aria-invalid',
|
|
488
|
-
'true',
|
|
489
|
-
);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('does not set aria-invalid for warning status', () => {
|
|
493
|
-
render(
|
|
494
|
-
<DateTimeInput
|
|
495
|
-
label="Meeting"
|
|
496
|
-
onChange={() => {}}
|
|
497
|
-
status={{type: 'warning', message: 'Watch out'}}
|
|
498
|
-
/>,
|
|
499
|
-
);
|
|
500
|
-
expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-invalid');
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it('renders description and links via aria-describedby', () => {
|
|
504
|
-
render(
|
|
505
|
-
<DateTimeInput
|
|
506
|
-
label="Meeting"
|
|
507
|
-
description="Pick the meeting datetime"
|
|
508
|
-
onChange={() => {}}
|
|
509
|
-
/>,
|
|
510
|
-
);
|
|
511
|
-
const input = screen.getByRole('combobox');
|
|
512
|
-
expect(screen.getByText('Pick the meeting datetime')).toBeInTheDocument();
|
|
513
|
-
expect(input).toHaveAttribute('aria-describedby');
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
it('links status message via aria-describedby', () => {
|
|
517
|
-
render(
|
|
518
|
-
<DateTimeInput
|
|
519
|
-
label="Meeting"
|
|
520
|
-
onChange={() => {}}
|
|
521
|
-
status={{type: 'error', message: 'Invalid datetime'}}
|
|
522
|
-
/>,
|
|
523
|
-
);
|
|
524
|
-
const input = screen.getByRole('combobox');
|
|
525
|
-
const describedBy = input.getAttribute('aria-describedby')!;
|
|
526
|
-
const ids = describedBy.split(' ');
|
|
527
|
-
const found = ids.some(id => {
|
|
528
|
-
const el = document.getElementById(id);
|
|
529
|
-
return el?.textContent?.includes('Invalid datetime');
|
|
530
|
-
});
|
|
531
|
-
expect(found).toBe(true);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it('links the description to the time input via aria-describedby', () => {
|
|
535
|
-
render(
|
|
536
|
-
<DateTimeInput
|
|
537
|
-
label="Meeting"
|
|
538
|
-
description="Pick the meeting datetime"
|
|
539
|
-
onChange={() => {}}
|
|
540
|
-
/>,
|
|
541
|
-
);
|
|
542
|
-
// The description covers both halves of the field, so the time input must
|
|
543
|
-
// carry it too — a screen-reader user tabbing into the time half should
|
|
544
|
-
// not lose the field's description.
|
|
545
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
546
|
-
const describedBy = timeInput.getAttribute('aria-describedby')!;
|
|
547
|
-
const ids = describedBy.split(' ');
|
|
548
|
-
const found = ids.some(id =>
|
|
549
|
-
document
|
|
550
|
-
.getElementById(id)
|
|
551
|
-
?.textContent?.includes('Pick the meeting datetime'),
|
|
552
|
-
);
|
|
553
|
-
expect(found).toBe(true);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('links the status message to the time input via aria-describedby', () => {
|
|
557
|
-
render(
|
|
558
|
-
<DateTimeInput
|
|
559
|
-
label="Meeting"
|
|
560
|
-
onChange={() => {}}
|
|
561
|
-
status={{type: 'error', message: 'Invalid datetime'}}
|
|
562
|
-
/>,
|
|
563
|
-
);
|
|
564
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
565
|
-
const describedBy = timeInput.getAttribute('aria-describedby')!;
|
|
566
|
-
const ids = describedBy.split(' ');
|
|
567
|
-
const found = ids.some(id =>
|
|
568
|
-
document.getElementById(id)?.textContent?.includes('Invalid datetime'),
|
|
569
|
-
);
|
|
570
|
-
expect(found).toBe(true);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
it('links the disabled reason to the time input via aria-describedby', () => {
|
|
574
|
-
HTMLElement.prototype.showPopover = vi.fn();
|
|
575
|
-
HTMLElement.prototype.hidePopover = vi.fn();
|
|
576
|
-
render(
|
|
577
|
-
<DateTimeInput
|
|
578
|
-
label="When"
|
|
579
|
-
onChange={() => {}}
|
|
580
|
-
isDisabled
|
|
581
|
-
disabledMessage="You need the Editor role"
|
|
582
|
-
/>,
|
|
583
|
-
);
|
|
584
|
-
const timeInput = screen.getByLabelText('When time');
|
|
585
|
-
const tooltip = screen.getByRole('tooltip', {hidden: true});
|
|
586
|
-
expect(timeInput.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
it('handles Escape keydown on date input without error', () => {
|
|
590
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
591
|
-
const input = screen.getByRole('combobox');
|
|
592
|
-
fireEvent.keyDown(input, {key: 'Escape'});
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// --- Date text input behavior ---
|
|
596
|
-
|
|
597
|
-
it('calls onChange when valid date is typed', () => {
|
|
598
|
-
const onChange = vi.fn();
|
|
599
|
-
render(<DateTimeInput label="Meeting" onChange={onChange} />);
|
|
600
|
-
|
|
601
|
-
const input = screen.getByRole('combobox');
|
|
602
|
-
fireEvent.change(input, {target: {value: '03/15/2026'}});
|
|
603
|
-
|
|
604
|
-
expect(onChange).toHaveBeenCalled();
|
|
605
|
-
const calledValue = onChange.mock.calls[0][0] as string;
|
|
606
|
-
expect(calledValue).toMatch(/^2026-03-15T/);
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it('does not call onChange while typing invalid date', () => {
|
|
610
|
-
const onChange = vi.fn();
|
|
611
|
-
render(<DateTimeInput label="Meeting" onChange={onChange} />);
|
|
612
|
-
|
|
613
|
-
const input = screen.getByRole('combobox');
|
|
614
|
-
fireEvent.change(input, {target: {value: 'invalid'}});
|
|
615
|
-
|
|
616
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('reverts date input on blur when input is invalid', () => {
|
|
620
|
-
const onChange = vi.fn();
|
|
621
|
-
render(
|
|
622
|
-
<DateTimeInput
|
|
623
|
-
label="Meeting"
|
|
624
|
-
value={'2026-01-25T10:00' as ISODateTimeString}
|
|
625
|
-
onChange={onChange}
|
|
626
|
-
/>,
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
const input = screen.getByRole('combobox');
|
|
630
|
-
fireEvent.change(input, {target: {value: 'not a date'}});
|
|
631
|
-
fireEvent.blur(input);
|
|
632
|
-
|
|
633
|
-
expect(screen.getByDisplayValue('January 25, 2026')).toBeInTheDocument();
|
|
634
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
// --- Time input behavior ---
|
|
638
|
-
|
|
639
|
-
it('does not call onChange for time when no date is set', () => {
|
|
640
|
-
const onChange = vi.fn();
|
|
641
|
-
render(<DateTimeInput label="Meeting" onChange={onChange} />);
|
|
642
|
-
|
|
643
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
644
|
-
fireEvent.change(timeInput, {target: {value: '3:45 pm'}});
|
|
645
|
-
|
|
646
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
it('calls onChange for time when date is already set', () => {
|
|
650
|
-
const onChange = vi.fn();
|
|
651
|
-
render(
|
|
652
|
-
<DateTimeInput
|
|
653
|
-
label="Meeting"
|
|
654
|
-
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
655
|
-
onChange={onChange}
|
|
656
|
-
/>,
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
660
|
-
fireEvent.change(timeInput, {target: {value: '3:45 pm'}});
|
|
661
|
-
|
|
662
|
-
expect(onChange).toHaveBeenCalledWith('2026-03-15T15:45');
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it('renders with size="lg"', () => {
|
|
666
|
-
render(
|
|
667
|
-
<DateTimeInput label="Meeting time" onChange={() => {}} size="lg" />,
|
|
668
|
-
);
|
|
669
|
-
expect(screen.getByLabelText('Meeting time')).toBeInTheDocument();
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
describe('hasClear', () => {
|
|
673
|
-
it('shows no selected calendar days after clearing', async () => {
|
|
674
|
-
const user = userEvent.setup();
|
|
675
|
-
|
|
676
|
-
function ControlledDateTimeInput() {
|
|
677
|
-
const [value, setValue] = useState<ISODateTimeString | undefined>(
|
|
678
|
-
'2026-01-01T09:00' as ISODateTimeString,
|
|
679
|
-
);
|
|
680
|
-
return (
|
|
681
|
-
<DateTimeInput
|
|
682
|
-
label="Meeting"
|
|
683
|
-
value={value}
|
|
684
|
-
onChange={setValue}
|
|
685
|
-
min={'2026-01-01T00:00' as ISODateTimeString}
|
|
686
|
-
max={'2026-01-31T23:59' as ISODateTimeString}
|
|
687
|
-
hasClear
|
|
688
|
-
/>
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
render(<ControlledDateTimeInput />);
|
|
693
|
-
await user.click(getButton('Open calendar'));
|
|
694
|
-
await user.click(getButton(/January 15, 2026/));
|
|
695
|
-
await user.click(getButton('Clear Meeting'));
|
|
696
|
-
await user.click(getButton('Open calendar'));
|
|
697
|
-
|
|
698
|
-
expect(
|
|
699
|
-
screen.queryAllByRole('gridcell', {selected: true, hidden: true}),
|
|
700
|
-
).toHaveLength(0);
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
it('shows clear button when hasClear is true and value exists', () => {
|
|
704
|
-
render(
|
|
705
|
-
<DateTimeInput
|
|
706
|
-
label="Meeting"
|
|
707
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
708
|
-
onChange={() => {}}
|
|
709
|
-
hasClear
|
|
710
|
-
/>,
|
|
711
|
-
);
|
|
712
|
-
expect(getButton('Clear Meeting')).toBeInTheDocument();
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
it('does not show clear button when value is undefined', () => {
|
|
716
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} hasClear />);
|
|
717
|
-
expect(queryButton('Clear Meeting')).not.toBeInTheDocument();
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
it('does not show clear button when hasClear is false', () => {
|
|
721
|
-
render(
|
|
722
|
-
<DateTimeInput
|
|
723
|
-
label="Meeting"
|
|
724
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
725
|
-
onChange={() => {}}
|
|
726
|
-
/>,
|
|
727
|
-
);
|
|
728
|
-
expect(queryButton('Clear Meeting')).not.toBeInTheDocument();
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it('does not show clear button when disabled', () => {
|
|
732
|
-
render(
|
|
733
|
-
<DateTimeInput
|
|
734
|
-
label="Meeting"
|
|
735
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
736
|
-
onChange={() => {}}
|
|
737
|
-
hasClear
|
|
738
|
-
isDisabled
|
|
739
|
-
/>,
|
|
740
|
-
);
|
|
741
|
-
expect(queryButton('Clear Meeting')).not.toBeInTheDocument();
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
it('calls onChange with undefined when clear is clicked', () => {
|
|
745
|
-
const onChange = vi.fn();
|
|
746
|
-
render(
|
|
747
|
-
<DateTimeInput
|
|
748
|
-
label="Meeting"
|
|
749
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
750
|
-
onChange={onChange}
|
|
751
|
-
hasClear
|
|
752
|
-
/>,
|
|
753
|
-
);
|
|
754
|
-
fireEvent.click(getButton('Clear Meeting'));
|
|
755
|
-
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
756
|
-
});
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
describe('external value changes', () => {
|
|
760
|
-
it('clears pending date input when value changes externally', () => {
|
|
761
|
-
const onChange = vi.fn();
|
|
762
|
-
const {rerender} = render(
|
|
763
|
-
<DateTimeInput
|
|
764
|
-
label="Meeting"
|
|
765
|
-
value={'2026-01-15T10:00' as ISODateTimeString}
|
|
766
|
-
onChange={onChange}
|
|
767
|
-
/>,
|
|
768
|
-
);
|
|
769
|
-
|
|
770
|
-
const dateInput = screen.getByRole('combobox');
|
|
771
|
-
expect(dateInput).toHaveValue('January 15, 2026');
|
|
772
|
-
|
|
773
|
-
// User starts typing a new date
|
|
774
|
-
fireEvent.change(dateInput, {target: {value: 'Feb'}});
|
|
775
|
-
expect(dateInput).toHaveValue('Feb');
|
|
776
|
-
|
|
777
|
-
// Value changes externally
|
|
778
|
-
rerender(
|
|
779
|
-
<DateTimeInput
|
|
780
|
-
label="Meeting"
|
|
781
|
-
value={'2026-03-20T10:00' as ISODateTimeString}
|
|
782
|
-
onChange={onChange}
|
|
783
|
-
/>,
|
|
784
|
-
);
|
|
785
|
-
|
|
786
|
-
// Pending input should be cleared, showing the new formatted date
|
|
787
|
-
expect(dateInput).toHaveValue('March 20, 2026');
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
describe('invalid typed input feedback (WCAG 3.3.1)', () => {
|
|
792
|
-
it('sets aria-invalid="true" on the date input when typed date is unparseable', () => {
|
|
793
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
794
|
-
|
|
795
|
-
const dateInput = screen.getByRole('combobox');
|
|
796
|
-
fireEvent.change(dateInput, {target: {value: '13/45/2024'}});
|
|
797
|
-
|
|
798
|
-
expect(dateInput).toHaveAttribute('aria-invalid', 'true');
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
it('does not set aria-invalid on the date input when typed date is valid', () => {
|
|
802
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
803
|
-
|
|
804
|
-
const dateInput = screen.getByRole('combobox');
|
|
805
|
-
fireEvent.change(dateInput, {target: {value: '03/15/2026'}});
|
|
806
|
-
|
|
807
|
-
expect(dateInput).not.toHaveAttribute('aria-invalid');
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
it('announces an alert message when the typed date is invalid', () => {
|
|
811
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
812
|
-
|
|
813
|
-
const dateInput = screen.getByRole('combobox');
|
|
814
|
-
fireEvent.change(dateInput, {target: {value: '13/45/2024'}});
|
|
815
|
-
|
|
816
|
-
expect(screen.getByText('Invalid date')).toBeInTheDocument();
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it('sets aria-invalid="true" on the time input when typed time is unparseable', () => {
|
|
820
|
-
render(
|
|
821
|
-
<DateTimeInput
|
|
822
|
-
label="Meeting"
|
|
823
|
-
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
824
|
-
onChange={() => {}}
|
|
825
|
-
/>,
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
829
|
-
fireEvent.change(timeInput, {target: {value: '99:99 zz'}});
|
|
830
|
-
|
|
831
|
-
expect(timeInput).toHaveAttribute('aria-invalid', 'true');
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
it('does not set aria-invalid on the time input when typed time is valid', () => {
|
|
835
|
-
render(
|
|
836
|
-
<DateTimeInput
|
|
837
|
-
label="Meeting"
|
|
838
|
-
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
839
|
-
onChange={() => {}}
|
|
840
|
-
/>,
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
844
|
-
fireEvent.change(timeInput, {target: {value: '3:45 pm'}});
|
|
845
|
-
|
|
846
|
-
expect(timeInput).not.toHaveAttribute('aria-invalid');
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
it('announces an alert message when the typed time is invalid', () => {
|
|
850
|
-
render(
|
|
851
|
-
<DateTimeInput
|
|
852
|
-
label="Meeting"
|
|
853
|
-
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
854
|
-
onChange={() => {}}
|
|
855
|
-
/>,
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
859
|
-
fireEvent.change(timeInput, {target: {value: '99:99 zz'}});
|
|
860
|
-
|
|
861
|
-
expect(screen.getByText('Invalid time')).toBeInTheDocument();
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
it('resolves the invalid date and time announcements from the i18n catalog', () => {
|
|
865
|
-
render(
|
|
866
|
-
<InternationalizationProvider
|
|
867
|
-
locale="en"
|
|
868
|
-
overrides={{
|
|
869
|
-
en: {
|
|
870
|
-
'@astryx.dateInput.invalidDate': 'Ungültiges Datum',
|
|
871
|
-
'@astryx.timeInput.invalidTime': 'Ungültige Zeit',
|
|
872
|
-
},
|
|
873
|
-
}}>
|
|
874
|
-
<DateTimeInput
|
|
875
|
-
label="Meeting"
|
|
876
|
-
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
877
|
-
onChange={() => {}}
|
|
878
|
-
/>
|
|
879
|
-
</InternationalizationProvider>,
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
fireEvent.change(screen.getByRole('combobox'), {
|
|
883
|
-
target: {value: '13/45/2024'},
|
|
884
|
-
});
|
|
885
|
-
expect(screen.getByText('Ungültiges Datum')).toBeInTheDocument();
|
|
886
|
-
|
|
887
|
-
fireEvent.change(screen.getByLabelText('Meeting time'), {
|
|
888
|
-
target: {value: '99:99 zz'},
|
|
889
|
-
});
|
|
890
|
-
expect(screen.getByText('Ungültige Zeit')).toBeInTheDocument();
|
|
891
|
-
});
|
|
892
|
-
});
|
|
893
|
-
describe('disabledMessage', () => {
|
|
894
|
-
// jsdom does not implement the Popover API used by the tooltip, so mock
|
|
895
|
-
// showPopover/hidePopover to toggle a `popover-open` attribute the tests
|
|
896
|
-
// can assert on.
|
|
897
|
-
beforeEach(() => {
|
|
898
|
-
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
899
|
-
this.setAttribute('popover-open', '');
|
|
900
|
-
});
|
|
901
|
-
HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
|
|
902
|
-
this.removeAttribute('popover-open');
|
|
903
|
-
});
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
// jsdom popover content is in the DOM but not "visible" in the
|
|
907
|
-
// accessibility tree; use hidden: true to find it.
|
|
908
|
-
const h = {hidden: true} as const;
|
|
909
|
-
|
|
910
|
-
it('shows the reason tooltip on hover when disabled with a reason', async () => {
|
|
911
|
-
render(
|
|
912
|
-
<DateTimeInput
|
|
913
|
-
label="When"
|
|
914
|
-
onChange={() => {}}
|
|
915
|
-
isDisabled
|
|
916
|
-
disabledMessage="You need the Editor role"
|
|
917
|
-
/>,
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
// The tooltip anchors on the outer row that wraps both inputs.
|
|
921
|
-
const dateInput = screen.getByRole('combobox');
|
|
922
|
-
const row = dateInput.parentElement?.parentElement as HTMLElement;
|
|
923
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
924
|
-
expect(tooltip).toHaveTextContent('You need the Editor role');
|
|
925
|
-
|
|
926
|
-
fireEvent.mouseEnter(row);
|
|
927
|
-
await waitFor(() => {
|
|
928
|
-
expect(tooltip).toHaveAttribute('popover-open');
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
fireEvent.mouseLeave(row);
|
|
932
|
-
await waitFor(() => {
|
|
933
|
-
expect(tooltip).not.toHaveAttribute('popover-open');
|
|
934
|
-
});
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
it('shows the reason tooltip on keyboard focus', async () => {
|
|
938
|
-
const user = userEvent.setup();
|
|
939
|
-
render(
|
|
940
|
-
<DateTimeInput
|
|
941
|
-
label="When"
|
|
942
|
-
onChange={() => {}}
|
|
943
|
-
isDisabled
|
|
944
|
-
disabledMessage="You need the Editor role"
|
|
945
|
-
/>,
|
|
946
|
-
);
|
|
947
|
-
|
|
948
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
949
|
-
// The calendar button keeps its enabled tab stop (spec:AST-085 FR1).
|
|
950
|
-
await user.tab();
|
|
951
|
-
expect(getButton('Open calendar')).toHaveFocus();
|
|
952
|
-
expect(
|
|
953
|
-
getButton('Open calendar').getAttribute('aria-describedby'),
|
|
954
|
-
).toContain(tooltip.id);
|
|
955
|
-
await user.tab();
|
|
956
|
-
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
957
|
-
await waitFor(() => {
|
|
958
|
-
expect(tooltip).toHaveAttribute('popover-open');
|
|
959
|
-
});
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
it('does not render a tooltip when not disabled', () => {
|
|
963
|
-
render(
|
|
964
|
-
<DateTimeInput
|
|
965
|
-
label="When"
|
|
966
|
-
onChange={() => {}}
|
|
967
|
-
disabledMessage="You need the Editor role"
|
|
968
|
-
/>,
|
|
969
|
-
);
|
|
970
|
-
expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
it('does not render a tooltip when disabled without a reason', () => {
|
|
974
|
-
render(<DateTimeInput label="When" onChange={() => {}} isDisabled />);
|
|
975
|
-
expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
it('keeps both inputs focusable via aria-disabled when a reason is provided', () => {
|
|
979
|
-
render(
|
|
980
|
-
<DateTimeInput
|
|
981
|
-
label="When"
|
|
982
|
-
onChange={() => {}}
|
|
983
|
-
isDisabled
|
|
984
|
-
disabledMessage="You need the Editor role"
|
|
985
|
-
/>,
|
|
986
|
-
);
|
|
987
|
-
const dateInput = screen.getByRole('combobox');
|
|
988
|
-
const timeInput = screen.getByLabelText('When time');
|
|
989
|
-
expect(dateInput).not.toBeDisabled();
|
|
990
|
-
expect(dateInput).toHaveAttribute('aria-disabled', 'true');
|
|
991
|
-
expect(dateInput).toHaveAttribute('readonly');
|
|
992
|
-
expect(timeInput).not.toBeDisabled();
|
|
993
|
-
expect(timeInput).toHaveAttribute('aria-disabled', 'true');
|
|
994
|
-
expect(timeInput).toHaveAttribute('readonly');
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
it('links the reason tooltip from the date input via aria-describedby', () => {
|
|
998
|
-
render(
|
|
999
|
-
<DateTimeInput
|
|
1000
|
-
label="When"
|
|
1001
|
-
onChange={() => {}}
|
|
1002
|
-
isDisabled
|
|
1003
|
-
disabledMessage="You need the Editor role"
|
|
1004
|
-
/>,
|
|
1005
|
-
);
|
|
1006
|
-
const dateInput = screen.getByRole('combobox');
|
|
1007
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
1008
|
-
expect(dateInput.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
it('blocks value changes and opening while focusable-disabled', async () => {
|
|
1012
|
-
const user = userEvent.setup();
|
|
1013
|
-
const onChange = vi.fn();
|
|
1014
|
-
render(
|
|
1015
|
-
<DateTimeInput
|
|
1016
|
-
label="When"
|
|
1017
|
-
onChange={onChange}
|
|
1018
|
-
isDisabled
|
|
1019
|
-
disabledMessage="You need the Editor role"
|
|
1020
|
-
/>,
|
|
1021
|
-
);
|
|
1022
|
-
|
|
1023
|
-
const dateInput = screen.getByRole('combobox');
|
|
1024
|
-
await user.click(dateInput);
|
|
1025
|
-
await user.type(dateInput, '2026-03-15');
|
|
1026
|
-
expect(dateInput).toHaveValue('');
|
|
1027
|
-
expect(dateInput).toHaveAttribute('aria-expanded', 'false');
|
|
1028
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
it('blocks typing, opening and a caller onClick when disabled without a reason (AST-085 FR2)', async () => {
|
|
1032
|
-
const user = userEvent.setup();
|
|
1033
|
-
const onChange = vi.fn();
|
|
1034
|
-
const onClick = vi.fn();
|
|
1035
|
-
render(
|
|
1036
|
-
<DateTimeInput
|
|
1037
|
-
label="When"
|
|
1038
|
-
onChange={onChange}
|
|
1039
|
-
onClick={onClick}
|
|
1040
|
-
isDisabled
|
|
1041
|
-
/>,
|
|
1042
|
-
);
|
|
1043
|
-
const dateInput = screen.getByRole('combobox');
|
|
1044
|
-
await user.click(dateInput);
|
|
1045
|
-
await user.type(dateInput, '2026-03-15');
|
|
1046
|
-
await user.keyboard('{ArrowDown}');
|
|
1047
|
-
expect(dateInput).toHaveValue('');
|
|
1048
|
-
expect(dateInput).toHaveAttribute('aria-expanded', 'false');
|
|
1049
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1050
|
-
expect(onClick).not.toHaveBeenCalled();
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
it('drops a date draft instead of committing it when the field becomes disabled (AST-085 FR2)', async () => {
|
|
1054
|
-
const user = userEvent.setup();
|
|
1055
|
-
const onChange = vi.fn();
|
|
1056
|
-
const value = '2026-03-10T09:00' as ISODateTimeString;
|
|
1057
|
-
const {rerender} = render(
|
|
1058
|
-
<DateTimeInput label="When" value={value} onChange={onChange} />,
|
|
1059
|
-
);
|
|
1060
|
-
const dateInput = screen.getByRole('combobox');
|
|
1061
|
-
await user.click(dateInput);
|
|
1062
|
-
await user.clear(dateInput);
|
|
1063
|
-
rerender(
|
|
1064
|
-
<DateTimeInput
|
|
1065
|
-
label="When"
|
|
1066
|
-
value={value}
|
|
1067
|
-
onChange={onChange}
|
|
1068
|
-
isDisabled
|
|
1069
|
-
/>,
|
|
1070
|
-
);
|
|
1071
|
-
await user.tab();
|
|
1072
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
it('does not swap in the time format-hint placeholder on focus while disabled', () => {
|
|
1076
|
-
render(
|
|
1077
|
-
<DateTimeInput
|
|
1078
|
-
label="When"
|
|
1079
|
-
onChange={() => {}}
|
|
1080
|
-
isDisabled
|
|
1081
|
-
disabledMessage="You need the Editor role"
|
|
1082
|
-
/>,
|
|
1083
|
-
);
|
|
1084
|
-
const timeInput = screen.getByLabelText('When time');
|
|
1085
|
-
timeInput.focus();
|
|
1086
|
-
fireEvent.focus(timeInput);
|
|
1087
|
-
expect(timeInput).toHaveAttribute('placeholder', 'Select a time');
|
|
1088
|
-
});
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
describe('timeIncrement', () => {
|
|
1092
|
-
it('steps the time by timeIncrement minutes on ArrowUp', () => {
|
|
1093
|
-
const onChange = vi.fn();
|
|
1094
|
-
render(
|
|
1095
|
-
<DateTimeInput
|
|
1096
|
-
label="Meeting"
|
|
1097
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1098
|
-
timeIncrement={15}
|
|
1099
|
-
onChange={onChange}
|
|
1100
|
-
/>,
|
|
1101
|
-
);
|
|
1102
|
-
|
|
1103
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1104
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1105
|
-
|
|
1106
|
-
// 14:30 + 15min increment = 14:45
|
|
1107
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1108
|
-
expect(onChange.mock.calls[0][0]).toContain('14:45');
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
it('defaults to a 1-minute increment', () => {
|
|
1112
|
-
const onChange = vi.fn();
|
|
1113
|
-
render(
|
|
1114
|
-
<DateTimeInput
|
|
1115
|
-
label="Meeting"
|
|
1116
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1117
|
-
onChange={onChange}
|
|
1118
|
-
/>,
|
|
1119
|
-
);
|
|
1120
|
-
|
|
1121
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1122
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1123
|
-
|
|
1124
|
-
// 14:30 + default 1min = 14:31
|
|
1125
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1126
|
-
expect(onChange.mock.calls[0][0]).toContain('14:31');
|
|
1127
|
-
});
|
|
1128
|
-
|
|
1129
|
-
// Arrow-key stepping mutates a plain textbox programmatically, and screen
|
|
1130
|
-
// readers do not announce programmatic textbox changes — the new value
|
|
1131
|
-
// must be announced through the polite live region (WCAG 4.1.2).
|
|
1132
|
-
it('politely announces the new time after ArrowUp stepping', async () => {
|
|
1133
|
-
render(
|
|
1134
|
-
<DateTimeInput
|
|
1135
|
-
label="Meeting"
|
|
1136
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1137
|
-
onChange={() => {}}
|
|
1138
|
-
/>,
|
|
1139
|
-
);
|
|
1140
|
-
|
|
1141
|
-
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
1142
|
-
key: 'ArrowUp',
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
await waitFor(() => {
|
|
1146
|
-
expect(politeRegion()).toHaveTextContent('2:31 PM');
|
|
1147
|
-
});
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
it('announces no step from a disabled time field (AST-085 FR2)', async () => {
|
|
1151
|
-
render(
|
|
1152
|
-
<DateTimeInput
|
|
1153
|
-
label="Meeting"
|
|
1154
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1155
|
-
onChange={() => {}}
|
|
1156
|
-
isDisabled
|
|
1157
|
-
/>,
|
|
1158
|
-
);
|
|
1159
|
-
|
|
1160
|
-
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
1161
|
-
key: 'ArrowUp',
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1165
|
-
expect(politeRegion()?.textContent ?? '').not.toContain('2:31 PM');
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
it('politely announces the new time after ArrowDown stepping', async () => {
|
|
1169
|
-
render(
|
|
1170
|
-
<DateTimeInput
|
|
1171
|
-
label="Meeting"
|
|
1172
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1173
|
-
onChange={() => {}}
|
|
1174
|
-
/>,
|
|
1175
|
-
);
|
|
1176
|
-
|
|
1177
|
-
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
1178
|
-
key: 'ArrowDown',
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
await waitFor(() => {
|
|
1182
|
-
expect(politeRegion()).toHaveTextContent('2:29 PM');
|
|
1183
|
-
});
|
|
1184
|
-
});
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
// ===========================================================================
|
|
1188
|
-
// Time-option dropdown (#2727)
|
|
1189
|
-
// ===========================================================================
|
|
1190
|
-
|
|
1191
|
-
describe('timeOptionInterval', () => {
|
|
1192
|
-
// The dropdown renders through the same popover as the calendar, so its
|
|
1193
|
-
// content lands in the top layer: jsdom keeps the nodes in the DOM but
|
|
1194
|
-
// role queries skip them. Read the options off the container directly,
|
|
1195
|
-
// exactly as the weekStartsOn tests read columnheaders.
|
|
1196
|
-
const optionsIn = (container: HTMLElement): HTMLElement[] =>
|
|
1197
|
-
Array.from(container.querySelectorAll('[role="option"]'));
|
|
1198
|
-
|
|
1199
|
-
const labelsIn = (container: HTMLElement): (string | null)[] =>
|
|
1200
|
-
optionsIn(container).map(o => o.textContent);
|
|
1201
|
-
|
|
1202
|
-
// ---- opt-in gate --------------------------------------------------------
|
|
1203
|
-
|
|
1204
|
-
it('adds no listbox and no second combobox without timeOptionInterval', () => {
|
|
1205
|
-
const {container} = render(
|
|
1206
|
-
<DateTimeInput
|
|
1207
|
-
label="Meeting"
|
|
1208
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1209
|
-
onChange={() => {}}
|
|
1210
|
-
/>,
|
|
1211
|
-
);
|
|
1212
|
-
|
|
1213
|
-
// The date input stays the only combobox on the field. 30+ existing
|
|
1214
|
-
// assertions use the singular getByRole('combobox'); a second one would
|
|
1215
|
-
// break every one of them, and every downstream consumer's too.
|
|
1216
|
-
expect(screen.getAllByRole('combobox')).toHaveLength(1);
|
|
1217
|
-
|
|
1218
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1219
|
-
expect(timeInput).not.toHaveAttribute('role', 'combobox');
|
|
1220
|
-
fireEvent.click(timeInput);
|
|
1221
|
-
expect(optionsIn(container)).toHaveLength(0);
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
// ---- option generation --------------------------------------------------
|
|
1225
|
-
|
|
1226
|
-
it('renders one option per interval step across the day', () => {
|
|
1227
|
-
const {container} = render(
|
|
1228
|
-
<DateTimeInput
|
|
1229
|
-
label="Meeting"
|
|
1230
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1231
|
-
timeOptionInterval={30}
|
|
1232
|
-
onChange={() => {}}
|
|
1233
|
-
/>,
|
|
1234
|
-
);
|
|
1235
|
-
|
|
1236
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1237
|
-
// A full day at 30-minute cadence.
|
|
1238
|
-
expect(optionsIn(container)).toHaveLength(48);
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
it('renders 24 options at an hourly interval', () => {
|
|
1242
|
-
const {container} = render(
|
|
1243
|
-
<DateTimeInput
|
|
1244
|
-
label="Meeting"
|
|
1245
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1246
|
-
timeOptionInterval={60}
|
|
1247
|
-
onChange={() => {}}
|
|
1248
|
-
/>,
|
|
1249
|
-
);
|
|
1250
|
-
|
|
1251
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1252
|
-
const labels = labelsIn(container);
|
|
1253
|
-
expect(labels).toHaveLength(24);
|
|
1254
|
-
// The 12 AM - 11 PM list the issue asks for.
|
|
1255
|
-
expect(labels[0]).toBe('12:00 AM');
|
|
1256
|
-
expect(labels[23]).toBe('11:00 PM');
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
it('labels options in 24-hour format when hourFormat is 24h', () => {
|
|
1260
|
-
const {container} = render(
|
|
1261
|
-
<DateTimeInput
|
|
1262
|
-
label="Meeting"
|
|
1263
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1264
|
-
timeOptionInterval={60}
|
|
1265
|
-
hourFormat="24h"
|
|
1266
|
-
onChange={() => {}}
|
|
1267
|
-
/>,
|
|
1268
|
-
);
|
|
1269
|
-
|
|
1270
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1271
|
-
const labels = labelsIn(container);
|
|
1272
|
-
expect(labels[0]).toBe('00:00');
|
|
1273
|
-
expect(labels[13]).toBe('13:00');
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
it('includes seconds in option labels when hasSeconds is set', () => {
|
|
1277
|
-
const {container} = render(
|
|
1278
|
-
<DateTimeInput
|
|
1279
|
-
label="Meeting"
|
|
1280
|
-
value={'2026-03-15T14:30:00' as ISODateTimeString}
|
|
1281
|
-
timeOptionInterval={60}
|
|
1282
|
-
hasSeconds
|
|
1283
|
-
onChange={() => {}}
|
|
1284
|
-
/>,
|
|
1285
|
-
);
|
|
1286
|
-
|
|
1287
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1288
|
-
expect(labelsIn(container)[0]).toBe('12:00:00 AM');
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
// ---- committing ---------------------------------------------------------
|
|
1292
|
-
|
|
1293
|
-
it('commits the clicked option and keeps the selected date', () => {
|
|
1294
|
-
const onChange = vi.fn();
|
|
1295
|
-
const {container} = render(
|
|
1296
|
-
<DateTimeInput
|
|
1297
|
-
label="Meeting"
|
|
1298
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1299
|
-
timeOptionInterval={60}
|
|
1300
|
-
onChange={onChange}
|
|
1301
|
-
/>,
|
|
1302
|
-
);
|
|
1303
|
-
|
|
1304
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1305
|
-
// 09:00 is the tenth hourly option.
|
|
1306
|
-
fireEvent.click(optionsIn(container)[9]);
|
|
1307
|
-
|
|
1308
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1309
|
-
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T09:00');
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
it('marks the committed option as selected, not merely highlighted', () => {
|
|
1313
|
-
const {container} = render(
|
|
1314
|
-
<DateTimeInput
|
|
1315
|
-
label="Meeting"
|
|
1316
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1317
|
-
timeOptionInterval={60}
|
|
1318
|
-
onChange={() => {}}
|
|
1319
|
-
/>,
|
|
1320
|
-
);
|
|
1321
|
-
|
|
1322
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1323
|
-
const selected = optionsIn(container).filter(
|
|
1324
|
-
o => o.getAttribute('aria-selected') === 'true',
|
|
1325
|
-
);
|
|
1326
|
-
expect(selected).toHaveLength(1);
|
|
1327
|
-
expect(selected[0].textContent).toBe('2:00 PM');
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
// ---- keyboard: the arrow-key collision ----------------------------------
|
|
1331
|
-
|
|
1332
|
-
it('keeps ArrowUp stepping by timeIncrement while the list is closed', () => {
|
|
1333
|
-
const onChange = vi.fn();
|
|
1334
|
-
render(
|
|
1335
|
-
<DateTimeInput
|
|
1336
|
-
label="Meeting"
|
|
1337
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1338
|
-
timeIncrement={15}
|
|
1339
|
-
timeOptionInterval={30}
|
|
1340
|
-
onChange={onChange}
|
|
1341
|
-
/>,
|
|
1342
|
-
);
|
|
1343
|
-
|
|
1344
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1345
|
-
// Closed list: arrows still step the value, as they always have.
|
|
1346
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1347
|
-
|
|
1348
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1349
|
-
expect(onChange.mock.calls[0][0]).toContain('14:45');
|
|
1350
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1351
|
-
});
|
|
1352
|
-
|
|
1353
|
-
it('opens the list on Alt+ArrowDown without changing the value', () => {
|
|
1354
|
-
const onChange = vi.fn();
|
|
1355
|
-
const {container} = render(
|
|
1356
|
-
<DateTimeInput
|
|
1357
|
-
label="Meeting"
|
|
1358
|
-
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
1359
|
-
timeOptionInterval={30}
|
|
1360
|
-
onChange={onChange}
|
|
1361
|
-
/>,
|
|
1362
|
-
);
|
|
1363
|
-
|
|
1364
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1365
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1366
|
-
|
|
1367
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1368
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1369
|
-
expect(optionsIn(container)).toHaveLength(48);
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
it('moves the active option with ArrowDown while open, without committing', () => {
|
|
1373
|
-
const onChange = vi.fn();
|
|
1374
|
-
const {container} = render(
|
|
1375
|
-
<DateTimeInput
|
|
1376
|
-
label="Meeting"
|
|
1377
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1378
|
-
timeOptionInterval={60}
|
|
1379
|
-
onChange={onChange}
|
|
1380
|
-
/>,
|
|
1381
|
-
);
|
|
1382
|
-
|
|
1383
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1384
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1385
|
-
// Opening starts the highlight on the committed value (14:00).
|
|
1386
|
-
expect(timeInput).toHaveAttribute(
|
|
1387
|
-
'aria-activedescendant',
|
|
1388
|
-
optionsIn(container)[14].id,
|
|
1389
|
-
);
|
|
1390
|
-
|
|
1391
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1392
|
-
expect(timeInput).toHaveAttribute(
|
|
1393
|
-
'aria-activedescendant',
|
|
1394
|
-
optionsIn(container)[15].id,
|
|
1395
|
-
);
|
|
1396
|
-
// Navigating is not selecting.
|
|
1397
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1398
|
-
});
|
|
1399
|
-
|
|
1400
|
-
it('commits the active option on Enter and closes the list', () => {
|
|
1401
|
-
const onChange = vi.fn();
|
|
1402
|
-
render(
|
|
1403
|
-
<DateTimeInput
|
|
1404
|
-
label="Meeting"
|
|
1405
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1406
|
-
timeOptionInterval={60}
|
|
1407
|
-
onChange={onChange}
|
|
1408
|
-
/>,
|
|
1409
|
-
);
|
|
1410
|
-
|
|
1411
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1412
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1413
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1414
|
-
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1415
|
-
|
|
1416
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1417
|
-
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T15:00');
|
|
1418
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
it('jumps to the first and last option with Home and End', () => {
|
|
1422
|
-
const {container} = render(
|
|
1423
|
-
<DateTimeInput
|
|
1424
|
-
label="Meeting"
|
|
1425
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1426
|
-
timeOptionInterval={60}
|
|
1427
|
-
onChange={() => {}}
|
|
1428
|
-
/>,
|
|
1429
|
-
);
|
|
1430
|
-
|
|
1431
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1432
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1433
|
-
|
|
1434
|
-
fireEvent.keyDown(timeInput, {key: 'Home'});
|
|
1435
|
-
expect(timeInput).toHaveAttribute(
|
|
1436
|
-
'aria-activedescendant',
|
|
1437
|
-
optionsIn(container)[0].id,
|
|
1438
|
-
);
|
|
1439
|
-
|
|
1440
|
-
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1441
|
-
expect(timeInput).toHaveAttribute(
|
|
1442
|
-
'aria-activedescendant',
|
|
1443
|
-
optionsIn(container)[23].id,
|
|
1444
|
-
);
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1447
|
-
it('closes on Escape without committing', () => {
|
|
1448
|
-
const onChange = vi.fn();
|
|
1449
|
-
render(
|
|
1450
|
-
<DateTimeInput
|
|
1451
|
-
label="Meeting"
|
|
1452
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1453
|
-
timeOptionInterval={60}
|
|
1454
|
-
onChange={onChange}
|
|
1455
|
-
/>,
|
|
1456
|
-
);
|
|
1457
|
-
|
|
1458
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1459
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1460
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1461
|
-
fireEvent.keyDown(timeInput, {key: 'Escape'});
|
|
1462
|
-
|
|
1463
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1464
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
// ---- typed entry keeps working -----------------------------------------
|
|
1468
|
-
|
|
1469
|
-
it('moves the highlight to the closest option as the user types, without dropping options', () => {
|
|
1470
|
-
const {container} = render(
|
|
1471
|
-
<DateTimeInput
|
|
1472
|
-
label="Meeting"
|
|
1473
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1474
|
-
timeOptionInterval={60}
|
|
1475
|
-
onChange={() => {}}
|
|
1476
|
-
/>,
|
|
1477
|
-
);
|
|
1478
|
-
|
|
1479
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1480
|
-
fireEvent.click(timeInput);
|
|
1481
|
-
fireEvent.change(timeInput, {target: {value: '9:00 PM'}});
|
|
1482
|
-
|
|
1483
|
-
// Free-form entry is the contract here: the list narrows nothing, it
|
|
1484
|
-
// follows the typed value so Enter still lands on something sensible.
|
|
1485
|
-
expect(optionsIn(container)).toHaveLength(24);
|
|
1486
|
-
expect(timeInput).toHaveAttribute(
|
|
1487
|
-
'aria-activedescendant',
|
|
1488
|
-
optionsIn(container)[21].id,
|
|
1489
|
-
);
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// ---- constraints --------------------------------------------------------
|
|
1493
|
-
|
|
1494
|
-
it('drops options outside min/max on the boundary date', () => {
|
|
1495
|
-
const {container} = render(
|
|
1496
|
-
<DateTimeInput
|
|
1497
|
-
label="Meeting"
|
|
1498
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1499
|
-
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1500
|
-
max={'2026-03-15T17:00' as ISODateTimeString}
|
|
1501
|
-
timeOptionInterval={60}
|
|
1502
|
-
onChange={() => {}}
|
|
1503
|
-
/>,
|
|
1504
|
-
);
|
|
1505
|
-
|
|
1506
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1507
|
-
const labels = labelsIn(container);
|
|
1508
|
-
// 09:00 through 17:00 inclusive.
|
|
1509
|
-
expect(labels).toHaveLength(9);
|
|
1510
|
-
expect(labels[0]).toBe('9:00 AM');
|
|
1511
|
-
expect(labels[8]).toBe('5:00 PM');
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
it('offers the whole day when the selected date is inside the min/max range', () => {
|
|
1515
|
-
const {container} = render(
|
|
1516
|
-
<DateTimeInput
|
|
1517
|
-
label="Meeting"
|
|
1518
|
-
value={'2026-03-16T14:00' as ISODateTimeString}
|
|
1519
|
-
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1520
|
-
max={'2026-03-17T17:00' as ISODateTimeString}
|
|
1521
|
-
timeOptionInterval={60}
|
|
1522
|
-
onChange={() => {}}
|
|
1523
|
-
/>,
|
|
1524
|
-
);
|
|
1525
|
-
|
|
1526
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
1527
|
-
// The time bound only bites on the boundary date itself.
|
|
1528
|
-
expect(optionsIn(container)).toHaveLength(24);
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
// ---- a11y wiring + disabled --------------------------------------------
|
|
1532
|
-
|
|
1533
|
-
it('wires the time input as a combobox onto the listbox', () => {
|
|
1534
|
-
const {container} = render(
|
|
1535
|
-
<DateTimeInput
|
|
1536
|
-
label="Meeting"
|
|
1537
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1538
|
-
timeOptionInterval={60}
|
|
1539
|
-
onChange={() => {}}
|
|
1540
|
-
/>,
|
|
1541
|
-
);
|
|
1542
|
-
|
|
1543
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1544
|
-
expect(timeInput).toHaveAttribute('role', 'combobox');
|
|
1545
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1546
|
-
expect(timeInput).toHaveAttribute('aria-autocomplete', 'list');
|
|
1547
|
-
|
|
1548
|
-
fireEvent.click(timeInput);
|
|
1549
|
-
const listbox = container.querySelector('[role="listbox"]');
|
|
1550
|
-
expect(listbox).not.toBeNull();
|
|
1551
|
-
expect(timeInput).toHaveAttribute('aria-controls', listbox?.id);
|
|
1552
|
-
// Named, so the list is not an anonymous group to a screen reader.
|
|
1553
|
-
expect(listbox).toHaveAttribute('aria-label');
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
it('highlights on hover without scrolling, keyboard still scrolls (#6077)', () => {
|
|
1557
|
-
// Hover must highlight only: scrollIntoView under a stationary pointer
|
|
1558
|
-
// moves the next option under it, re-highlighting and scrolling again —
|
|
1559
|
-
// a runaway auto-scroll loop with no user input.
|
|
1560
|
-
const scrollIntoView = vi.fn();
|
|
1561
|
-
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
1562
|
-
configurable: true,
|
|
1563
|
-
value: scrollIntoView,
|
|
1564
|
-
});
|
|
1565
|
-
try {
|
|
1566
|
-
const {container} = render(
|
|
1567
|
-
<DateTimeInput
|
|
1568
|
-
label="Meeting"
|
|
1569
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1570
|
-
timeOptionInterval={60}
|
|
1571
|
-
onChange={() => {}}
|
|
1572
|
-
/>,
|
|
1573
|
-
);
|
|
1574
|
-
|
|
1575
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1576
|
-
fireEvent.click(timeInput);
|
|
1577
|
-
scrollIntoView.mockClear();
|
|
1578
|
-
|
|
1579
|
-
const options = container
|
|
1580
|
-
.querySelector('[role="listbox"]')!
|
|
1581
|
-
.querySelectorAll('[role="option"]');
|
|
1582
|
-
fireEvent.mouseEnter(options[5]);
|
|
1583
|
-
|
|
1584
|
-
expect(timeInput.getAttribute('aria-activedescendant')).toBe(
|
|
1585
|
-
options[5].id,
|
|
1586
|
-
);
|
|
1587
|
-
expect(scrollIntoView).not.toHaveBeenCalled();
|
|
1588
|
-
|
|
1589
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1590
|
-
expect(scrollIntoView).toHaveBeenCalledWith({block: 'nearest'});
|
|
1591
|
-
} finally {
|
|
1592
|
-
delete (HTMLElement.prototype as unknown as {scrollIntoView?: unknown})
|
|
1593
|
-
.scrollIntoView;
|
|
1594
|
-
}
|
|
1595
|
-
});
|
|
1596
|
-
|
|
1597
|
-
it('does not open the list when disabled', () => {
|
|
1598
|
-
render(
|
|
1599
|
-
<DateTimeInput
|
|
1600
|
-
label="Meeting"
|
|
1601
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1602
|
-
timeOptionInterval={60}
|
|
1603
|
-
isDisabled
|
|
1604
|
-
onChange={() => {}}
|
|
1605
|
-
/>,
|
|
1606
|
-
);
|
|
1607
|
-
|
|
1608
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1609
|
-
fireEvent.click(timeInput);
|
|
1610
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1611
|
-
|
|
1612
|
-
// The layer mounts its content whether or not it is open — visibility is
|
|
1613
|
-
// the popover attribute's job, which jsdom does not apply. So assert the
|
|
1614
|
-
// open state itself, not the presence of option nodes.
|
|
1615
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1616
|
-
expect(timeInput).not.toHaveAttribute('aria-controls');
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
it('does not open the list for a focusable-disabled field', () => {
|
|
1620
|
-
// With disabledMessage the input drops the native `disabled` attribute so
|
|
1621
|
-
// the reason stays discoverable, which means clicks and keys really do
|
|
1622
|
-
// reach it. This is the case the guards have to catch — a natively
|
|
1623
|
-
// disabled input never delivers the events in the first place.
|
|
1624
|
-
render(
|
|
1625
|
-
<DateTimeInput
|
|
1626
|
-
label="Meeting"
|
|
1627
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1628
|
-
timeOptionInterval={60}
|
|
1629
|
-
isDisabled
|
|
1630
|
-
disabledMessage="Pick a project first"
|
|
1631
|
-
onChange={() => {}}
|
|
1632
|
-
/>,
|
|
1633
|
-
);
|
|
1634
|
-
|
|
1635
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1636
|
-
expect(timeInput).not.toBeDisabled();
|
|
1637
|
-
|
|
1638
|
-
fireEvent.click(timeInput);
|
|
1639
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1640
|
-
|
|
1641
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1642
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1643
|
-
});
|
|
1644
|
-
|
|
1645
|
-
// -- edge cases ----------------------------------------------------------
|
|
1646
|
-
|
|
1647
|
-
it('does not fire a change when the already-selected option is clicked', () => {
|
|
1648
|
-
// The marker compares a seconds-normalised time while the commit path
|
|
1649
|
-
// compared the raw one, so with a stray-seconds value the option that
|
|
1650
|
-
// renders as selected still emitted a change when clicked.
|
|
1651
|
-
const onChange = vi.fn();
|
|
1652
|
-
const {container} = render(
|
|
1653
|
-
<DateTimeInput
|
|
1654
|
-
label="Meeting"
|
|
1655
|
-
value={'2026-03-15T14:00:00' as ISODateTimeString}
|
|
1656
|
-
timeOptionInterval={60}
|
|
1657
|
-
onChange={onChange}
|
|
1658
|
-
/>,
|
|
1659
|
-
);
|
|
1660
|
-
|
|
1661
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1662
|
-
fireEvent.click(timeInput);
|
|
1663
|
-
const selected = optionsIn(container).find(
|
|
1664
|
-
o => o.getAttribute('aria-selected') === 'true',
|
|
1665
|
-
);
|
|
1666
|
-
expect(selected).toBeDefined();
|
|
1667
|
-
|
|
1668
|
-
fireEvent.click(selected as HTMLElement);
|
|
1669
|
-
|
|
1670
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
1671
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
|
-
it('does not open an empty list when min/max leave no valid option', () => {
|
|
1675
|
-
// A window narrower than the cadence survives no option at all. Opening
|
|
1676
|
-
// then shows a blank popover with nothing to pick.
|
|
1677
|
-
render(
|
|
1678
|
-
<DateTimeInput
|
|
1679
|
-
label="Meeting"
|
|
1680
|
-
value={'2026-03-15T09:15' as ISODateTimeString}
|
|
1681
|
-
min={'2026-03-15T09:10' as ISODateTimeString}
|
|
1682
|
-
max={'2026-03-15T09:20' as ISODateTimeString}
|
|
1683
|
-
timeOptionInterval={30}
|
|
1684
|
-
onChange={() => {}}
|
|
1685
|
-
/>,
|
|
1686
|
-
);
|
|
1687
|
-
|
|
1688
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1689
|
-
fireEvent.click(timeInput);
|
|
1690
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1691
|
-
|
|
1692
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1693
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1694
|
-
});
|
|
1695
|
-
|
|
1696
|
-
it('commits the active option on Enter once the user arrows away from what they typed', () => {
|
|
1697
|
-
// Typed text beats the highlight only while the highlight is still the
|
|
1698
|
-
// one typing derived. After an arrow key the highlight is the user's own
|
|
1699
|
-
// choice, and Enter must honour what the field says is active.
|
|
1700
|
-
const onChange = vi.fn();
|
|
1701
|
-
const {container} = render(
|
|
1702
|
-
<DateTimeInput
|
|
1703
|
-
label="Meeting"
|
|
1704
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1705
|
-
timeOptionInterval={60}
|
|
1706
|
-
onChange={onChange}
|
|
1707
|
-
/>,
|
|
1708
|
-
);
|
|
1709
|
-
|
|
1710
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1711
|
-
fireEvent.click(timeInput);
|
|
1712
|
-
fireEvent.change(timeInput, {target: {value: '9:15 AM'}});
|
|
1713
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1714
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1715
|
-
|
|
1716
|
-
expect(timeInput).toHaveAttribute(
|
|
1717
|
-
'aria-activedescendant',
|
|
1718
|
-
optionsIn(container)[11].id,
|
|
1719
|
-
);
|
|
1720
|
-
|
|
1721
|
-
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1722
|
-
expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toBe(
|
|
1723
|
-
'2026-03-15T11:00',
|
|
1724
|
-
);
|
|
1725
|
-
});
|
|
1726
|
-
|
|
1727
|
-
it('closes a list that empties underneath it', () => {
|
|
1728
|
-
// The open-time guard cannot help here: the window narrows after the
|
|
1729
|
-
// list is already up. Left open, the keyboard switch is gated off and
|
|
1730
|
-
// the arrows fall back to stepping a value the user cannot see.
|
|
1731
|
-
const onChange = vi.fn();
|
|
1732
|
-
const {container, rerender} = render(
|
|
1733
|
-
<DateTimeInput
|
|
1734
|
-
label="Meeting"
|
|
1735
|
-
value={'2026-03-16T14:00' as ISODateTimeString}
|
|
1736
|
-
min={'2026-03-15T09:10' as ISODateTimeString}
|
|
1737
|
-
max={'2026-03-15T09:20' as ISODateTimeString}
|
|
1738
|
-
timeOptionInterval={60}
|
|
1739
|
-
onChange={onChange}
|
|
1740
|
-
/>,
|
|
1741
|
-
);
|
|
1742
|
-
|
|
1743
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1744
|
-
fireEvent.click(timeInput);
|
|
1745
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1746
|
-
|
|
1747
|
-
rerender(
|
|
1748
|
-
<DateTimeInput
|
|
1749
|
-
label="Meeting"
|
|
1750
|
-
value={'2026-03-15T09:15' as ISODateTimeString}
|
|
1751
|
-
min={'2026-03-15T09:10' as ISODateTimeString}
|
|
1752
|
-
max={'2026-03-15T09:20' as ISODateTimeString}
|
|
1753
|
-
timeOptionInterval={60}
|
|
1754
|
-
onChange={onChange}
|
|
1755
|
-
/>,
|
|
1756
|
-
);
|
|
1757
|
-
|
|
1758
|
-
expect(optionsIn(container)).toHaveLength(0);
|
|
1759
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
it('closes the list when focus leaves the field entirely', () => {
|
|
1763
|
-
// Light dismiss covers outside clicks and Escape, never a programmatic
|
|
1764
|
-
// focus move. Without this the listbox is orphaned in the top layer.
|
|
1765
|
-
render(
|
|
1766
|
-
<>
|
|
1767
|
-
<DateTimeInput
|
|
1768
|
-
label="Meeting"
|
|
1769
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1770
|
-
timeOptionInterval={60}
|
|
1771
|
-
onChange={() => {}}
|
|
1772
|
-
/>
|
|
1773
|
-
<button type="button">after</button>
|
|
1774
|
-
</>,
|
|
1775
|
-
);
|
|
1776
|
-
|
|
1777
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1778
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1779
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1780
|
-
|
|
1781
|
-
fireEvent.blur(timeInput, {relatedTarget: getButton('after')});
|
|
1782
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1783
|
-
});
|
|
1784
|
-
|
|
1785
|
-
it('closes an open list when the field goes busy', () => {
|
|
1786
|
-
const {rerender} = render(
|
|
1787
|
-
<DateTimeInput
|
|
1788
|
-
label="Meeting"
|
|
1789
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1790
|
-
timeOptionInterval={60}
|
|
1791
|
-
onChange={() => {}}
|
|
1792
|
-
/>,
|
|
1793
|
-
);
|
|
1794
|
-
|
|
1795
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1796
|
-
fireEvent.click(timeInput);
|
|
1797
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1798
|
-
|
|
1799
|
-
rerender(
|
|
1800
|
-
<DateTimeInput
|
|
1801
|
-
label="Meeting"
|
|
1802
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1803
|
-
timeOptionInterval={60}
|
|
1804
|
-
isLoading
|
|
1805
|
-
onChange={() => {}}
|
|
1806
|
-
/>,
|
|
1807
|
-
);
|
|
1808
|
-
|
|
1809
|
-
// An open dropdown over a control that no longer takes input.
|
|
1810
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1811
|
-
});
|
|
1812
|
-
|
|
1813
|
-
it('scales option padding with the field size', () => {
|
|
1814
|
-
const small = render(
|
|
1815
|
-
<DateTimeInput
|
|
1816
|
-
label="Small"
|
|
1817
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1818
|
-
timeOptionInterval={60}
|
|
1819
|
-
size="sm"
|
|
1820
|
-
onChange={() => {}}
|
|
1821
|
-
/>,
|
|
1822
|
-
);
|
|
1823
|
-
fireEvent.click(screen.getByLabelText('Small time'));
|
|
1824
|
-
const smallClass = optionsIn(small.container)[0].className;
|
|
1825
|
-
small.unmount();
|
|
1826
|
-
|
|
1827
|
-
const large = render(
|
|
1828
|
-
<DateTimeInput
|
|
1829
|
-
label="Large"
|
|
1830
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1831
|
-
timeOptionInterval={60}
|
|
1832
|
-
size="lg"
|
|
1833
|
-
onChange={() => {}}
|
|
1834
|
-
/>,
|
|
1835
|
-
);
|
|
1836
|
-
fireEvent.click(screen.getByLabelText('Large time'));
|
|
1837
|
-
const largeClass = optionsIn(large.container)[0].className;
|
|
1838
|
-
|
|
1839
|
-
// Rows that ignore size leave an sm field with md-sized options.
|
|
1840
|
-
expect(smallClass).not.toBe(largeClass);
|
|
1841
|
-
});
|
|
1842
|
-
|
|
1843
|
-
it('commits the typed time on Enter, not the option below it', () => {
|
|
1844
|
-
// Typing is free-form, so Enter must not quietly round 1:07 PM down to
|
|
1845
|
-
// the 1:00 PM option the highlight is resting on.
|
|
1846
|
-
const onChange = vi.fn();
|
|
1847
|
-
render(
|
|
1848
|
-
<DateTimeInput
|
|
1849
|
-
label="Meeting"
|
|
1850
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1851
|
-
timeOptionInterval={60}
|
|
1852
|
-
onChange={onChange}
|
|
1853
|
-
/>,
|
|
1854
|
-
);
|
|
1855
|
-
|
|
1856
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1857
|
-
fireEvent.click(timeInput);
|
|
1858
|
-
fireEvent.change(timeInput, {target: {value: '1:07 PM'}});
|
|
1859
|
-
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1860
|
-
|
|
1861
|
-
const last = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
|
1862
|
-
expect(last).toBe('2026-03-15T13:07');
|
|
1863
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
1864
|
-
});
|
|
1865
|
-
|
|
1866
|
-
it('names the list after timeLabel when one is given', () => {
|
|
1867
|
-
const {container} = render(
|
|
1868
|
-
<DateTimeInput
|
|
1869
|
-
label="Meeting"
|
|
1870
|
-
timeLabel="Start time"
|
|
1871
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1872
|
-
timeOptionInterval={60}
|
|
1873
|
-
onChange={() => {}}
|
|
1874
|
-
/>,
|
|
1875
|
-
);
|
|
1876
|
-
|
|
1877
|
-
fireEvent.click(screen.getByLabelText('Start time'));
|
|
1878
|
-
// The list belongs to the field the consumer renamed, so it must not
|
|
1879
|
-
// announce itself under the old one.
|
|
1880
|
-
expect(container.querySelector('[role="listbox"]')).toHaveAttribute(
|
|
1881
|
-
'aria-label',
|
|
1882
|
-
'Start time options',
|
|
1883
|
-
);
|
|
1884
|
-
});
|
|
1885
|
-
|
|
1886
|
-
it('opens from the keyboard after a pointer press that never became a click', () => {
|
|
1887
|
-
// The show is deferred past an in-flight click so light-dismiss cannot
|
|
1888
|
-
// eat it. A press that ends without a click must not wedge that flag.
|
|
1889
|
-
render(
|
|
1890
|
-
<DateTimeInput
|
|
1891
|
-
label="Meeting"
|
|
1892
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
1893
|
-
timeOptionInterval={60}
|
|
1894
|
-
onChange={() => {}}
|
|
1895
|
-
/>,
|
|
1896
|
-
);
|
|
1897
|
-
|
|
1898
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1899
|
-
fireEvent.pointerDown(timeInput);
|
|
1900
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1901
|
-
|
|
1902
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
it('activates the first option when the field has a date but no time', () => {
|
|
1906
|
-
// The state you land in after picking a date and no time yet. With no
|
|
1907
|
-
// active option, ArrowUp and Enter both did nothing at all.
|
|
1908
|
-
const onChange = vi.fn();
|
|
1909
|
-
const {container} = render(
|
|
1910
|
-
<DateTimeInput
|
|
1911
|
-
label="Meeting"
|
|
1912
|
-
value={'2026-03-15' as ISODateTimeString}
|
|
1913
|
-
timeOptionInterval={60}
|
|
1914
|
-
onChange={onChange}
|
|
1915
|
-
/>,
|
|
1916
|
-
);
|
|
1917
|
-
|
|
1918
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1919
|
-
fireEvent.click(timeInput);
|
|
1920
|
-
|
|
1921
|
-
expect(timeInput).toHaveAttribute(
|
|
1922
|
-
'aria-activedescendant',
|
|
1923
|
-
optionsIn(container)[0].id,
|
|
1924
|
-
);
|
|
1925
|
-
|
|
1926
|
-
// ArrowUp on the first option is a no-op, not a dead end.
|
|
1927
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1928
|
-
expect(timeInput).toHaveAttribute(
|
|
1929
|
-
'aria-activedescendant',
|
|
1930
|
-
optionsIn(container)[0].id,
|
|
1931
|
-
);
|
|
1932
|
-
|
|
1933
|
-
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
1934
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
1935
|
-
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T00:00');
|
|
1936
|
-
});
|
|
1937
|
-
|
|
1938
|
-
it('keeps the active option valid when the list shrinks while open', () => {
|
|
1939
|
-
const {container, rerender} = render(
|
|
1940
|
-
<DateTimeInput
|
|
1941
|
-
label="Meeting"
|
|
1942
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1943
|
-
timeOptionInterval={5}
|
|
1944
|
-
onChange={() => {}}
|
|
1945
|
-
/>,
|
|
1946
|
-
);
|
|
1947
|
-
|
|
1948
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1949
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
1950
|
-
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1951
|
-
expect(optionsIn(container)).toHaveLength(288);
|
|
1952
|
-
|
|
1953
|
-
// The cadence widens under the open list, so the old index is far past
|
|
1954
|
-
// the end. A stale index leaves the user with no visible highlight and
|
|
1955
|
-
// arrow keys that appear dead until they walk it back into range.
|
|
1956
|
-
rerender(
|
|
1957
|
-
<DateTimeInput
|
|
1958
|
-
label="Meeting"
|
|
1959
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
1960
|
-
timeOptionInterval={60}
|
|
1961
|
-
onChange={() => {}}
|
|
1962
|
-
/>,
|
|
1963
|
-
);
|
|
1964
|
-
|
|
1965
|
-
const options = optionsIn(container);
|
|
1966
|
-
expect(options).toHaveLength(24);
|
|
1967
|
-
expect(timeInput).toHaveAttribute(
|
|
1968
|
-
'aria-activedescendant',
|
|
1969
|
-
options[23].id,
|
|
1970
|
-
);
|
|
1971
|
-
});
|
|
1972
|
-
|
|
1973
|
-
it('handles a min/max window that leaves exactly one option', () => {
|
|
1974
|
-
const onChange = vi.fn();
|
|
1975
|
-
const {container} = render(
|
|
1976
|
-
<DateTimeInput
|
|
1977
|
-
label="Meeting"
|
|
1978
|
-
value={'2026-03-15T09:20' as ISODateTimeString}
|
|
1979
|
-
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
1980
|
-
max={'2026-03-15T09:30' as ISODateTimeString}
|
|
1981
|
-
timeOptionInterval={60}
|
|
1982
|
-
onChange={onChange}
|
|
1983
|
-
/>,
|
|
1984
|
-
);
|
|
1985
|
-
|
|
1986
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
1987
|
-
fireEvent.click(timeInput);
|
|
1988
|
-
|
|
1989
|
-
const options = optionsIn(container);
|
|
1990
|
-
expect(options).toHaveLength(1);
|
|
1991
|
-
expect(options[0].textContent).toBe('9:00 AM');
|
|
1992
|
-
|
|
1993
|
-
// Every clamp expression collapses at length 1.
|
|
1994
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
1995
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
1996
|
-
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
1997
|
-
expect(timeInput).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
1998
|
-
|
|
1999
|
-
fireEvent.keyDown(timeInput, {key: 'Enter'});
|
|
2000
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
2001
|
-
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T09:00');
|
|
2002
|
-
});
|
|
2003
|
-
|
|
2004
|
-
it('labels midnight and noon correctly in 12-hour format', () => {
|
|
2005
|
-
// 00:00 and 12:00 are where 12-hour formatting classically goes wrong.
|
|
2006
|
-
const {container} = render(
|
|
2007
|
-
<DateTimeInput
|
|
2008
|
-
label="Meeting"
|
|
2009
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2010
|
-
timeOptionInterval={60}
|
|
2011
|
-
onChange={() => {}}
|
|
2012
|
-
/>,
|
|
2013
|
-
);
|
|
2014
|
-
|
|
2015
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
2016
|
-
const labels = labelsIn(container);
|
|
2017
|
-
expect(labels[0]).toBe('12:00 AM');
|
|
2018
|
-
expect(labels[12]).toBe('12:00 PM');
|
|
2019
|
-
expect(labels[11]).toBe('11:00 AM');
|
|
2020
|
-
expect(labels[13]).toBe('1:00 PM');
|
|
2021
|
-
});
|
|
2022
|
-
|
|
2023
|
-
it('highlights the nearest earlier option for a value off the cadence', () => {
|
|
2024
|
-
const {container} = render(
|
|
2025
|
-
<DateTimeInput
|
|
2026
|
-
label="Meeting"
|
|
2027
|
-
value={'2026-03-15T14:07' as ISODateTimeString}
|
|
2028
|
-
timeOptionInterval={30}
|
|
2029
|
-
onChange={() => {}}
|
|
2030
|
-
/>,
|
|
2031
|
-
);
|
|
2032
|
-
|
|
2033
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2034
|
-
fireEvent.click(timeInput);
|
|
2035
|
-
|
|
2036
|
-
// 14:07 sits between 14:00 (index 28) and 14:30.
|
|
2037
|
-
expect(timeInput).toHaveAttribute(
|
|
2038
|
-
'aria-activedescendant',
|
|
2039
|
-
optionsIn(container)[28].id,
|
|
2040
|
-
);
|
|
2041
|
-
// ...and nothing claims to be the selected value.
|
|
2042
|
-
expect(
|
|
2043
|
-
optionsIn(container).filter(
|
|
2044
|
-
o => o.getAttribute('aria-selected') === 'true',
|
|
2045
|
-
),
|
|
2046
|
-
).toHaveLength(0);
|
|
2047
|
-
});
|
|
2048
|
-
|
|
2049
|
-
it('highlights the first option for a value below the whole range', () => {
|
|
2050
|
-
const {container} = render(
|
|
2051
|
-
<DateTimeInput
|
|
2052
|
-
label="Meeting"
|
|
2053
|
-
value={'2026-03-15T06:00' as ISODateTimeString}
|
|
2054
|
-
min={'2026-03-15T09:00' as ISODateTimeString}
|
|
2055
|
-
timeOptionInterval={60}
|
|
2056
|
-
onChange={() => {}}
|
|
2057
|
-
/>,
|
|
2058
|
-
);
|
|
2059
|
-
|
|
2060
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2061
|
-
fireEvent.click(timeInput);
|
|
2062
|
-
|
|
2063
|
-
expect(timeInput).toHaveAttribute(
|
|
2064
|
-
'aria-activedescendant',
|
|
2065
|
-
optionsIn(container)[0].id,
|
|
2066
|
-
);
|
|
2067
|
-
expect(labelsIn(container)[0]).toBe('9:00 AM');
|
|
2068
|
-
});
|
|
2069
|
-
|
|
2070
|
-
it('clamps the active option at both ends of the list', () => {
|
|
2071
|
-
const {container} = render(
|
|
2072
|
-
<DateTimeInput
|
|
2073
|
-
label="Meeting"
|
|
2074
|
-
value={'2026-03-15T00:00' as ISODateTimeString}
|
|
2075
|
-
timeOptionInterval={60}
|
|
2076
|
-
onChange={() => {}}
|
|
2077
|
-
/>,
|
|
2078
|
-
);
|
|
2079
|
-
|
|
2080
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2081
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
2082
|
-
|
|
2083
|
-
// Already on the first option: ArrowUp must not wrap to the last.
|
|
2084
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowUp'});
|
|
2085
|
-
expect(timeInput).toHaveAttribute(
|
|
2086
|
-
'aria-activedescendant',
|
|
2087
|
-
optionsIn(container)[0].id,
|
|
2088
|
-
);
|
|
2089
|
-
|
|
2090
|
-
fireEvent.keyDown(timeInput, {key: 'End'});
|
|
2091
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown'});
|
|
2092
|
-
expect(timeInput).toHaveAttribute(
|
|
2093
|
-
'aria-activedescendant',
|
|
2094
|
-
optionsIn(container)[23].id,
|
|
2095
|
-
);
|
|
2096
|
-
});
|
|
2097
|
-
|
|
2098
|
-
it('leaves Escape alone while the list is closed', () => {
|
|
2099
|
-
// Swallowing Escape here would stop an enclosing Dialog from closing.
|
|
2100
|
-
render(
|
|
2101
|
-
<DateTimeInput
|
|
2102
|
-
label="Meeting"
|
|
2103
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2104
|
-
timeOptionInterval={60}
|
|
2105
|
-
onChange={() => {}}
|
|
2106
|
-
/>,
|
|
2107
|
-
);
|
|
2108
|
-
|
|
2109
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2110
|
-
// fireEvent returns false when the event was cancelled.
|
|
2111
|
-
expect(fireEvent.keyDown(timeInput, {key: 'Escape'})).toBe(true);
|
|
2112
|
-
});
|
|
2113
|
-
|
|
2114
|
-
it('closes the list on Tab without blocking focus from leaving', () => {
|
|
2115
|
-
render(
|
|
2116
|
-
<DateTimeInput
|
|
2117
|
-
label="Meeting"
|
|
2118
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2119
|
-
timeOptionInterval={60}
|
|
2120
|
-
onChange={() => {}}
|
|
2121
|
-
/>,
|
|
2122
|
-
);
|
|
2123
|
-
|
|
2124
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2125
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
2126
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
2127
|
-
|
|
2128
|
-
// Not cancelled, so the browser still moves focus to the next control.
|
|
2129
|
-
expect(fireEvent.keyDown(timeInput, {key: 'Tab'})).toBe(true);
|
|
2130
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
2131
|
-
});
|
|
2132
|
-
|
|
2133
|
-
it('picking a time with no date selected changes nothing', () => {
|
|
2134
|
-
// Documents the component's existing contract: every time-commit path
|
|
2135
|
-
// requires a date, so the list cannot invent one.
|
|
2136
|
-
const onChange = vi.fn();
|
|
2137
|
-
const {container} = render(
|
|
2138
|
-
<DateTimeInput
|
|
2139
|
-
label="Meeting"
|
|
2140
|
-
timeOptionInterval={60}
|
|
2141
|
-
onChange={onChange}
|
|
2142
|
-
/>,
|
|
2143
|
-
);
|
|
2144
|
-
|
|
2145
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2146
|
-
fireEvent.click(timeInput);
|
|
2147
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
2148
|
-
|
|
2149
|
-
fireEvent.click(optionsIn(container)[9]);
|
|
2150
|
-
|
|
2151
|
-
// The click IS handled — the list closes — it just cannot produce a
|
|
2152
|
-
// datetime without a date, so no change is emitted and the field stays
|
|
2153
|
-
// empty rather than showing a time that was never committed.
|
|
2154
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
2155
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
2156
|
-
expect(timeInput).toHaveValue('');
|
|
2157
|
-
});
|
|
2158
|
-
|
|
2159
|
-
it('does not open the list while a change is in flight', () => {
|
|
2160
|
-
render(
|
|
2161
|
-
<DateTimeInput
|
|
2162
|
-
label="Meeting"
|
|
2163
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2164
|
-
timeOptionInterval={60}
|
|
2165
|
-
isLoading
|
|
2166
|
-
onChange={() => {}}
|
|
2167
|
-
/>,
|
|
2168
|
-
);
|
|
2169
|
-
|
|
2170
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2171
|
-
fireEvent.click(timeInput);
|
|
2172
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
2173
|
-
|
|
2174
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
2175
|
-
});
|
|
2176
|
-
|
|
2177
|
-
it('commits seconds in the value when hasSeconds is set', () => {
|
|
2178
|
-
const onChange = vi.fn();
|
|
2179
|
-
const {container} = render(
|
|
2180
|
-
<DateTimeInput
|
|
2181
|
-
label="Meeting"
|
|
2182
|
-
value={'2026-03-15T14:00:00' as ISODateTimeString}
|
|
2183
|
-
timeOptionInterval={60}
|
|
2184
|
-
hasSeconds
|
|
2185
|
-
onChange={onChange}
|
|
2186
|
-
/>,
|
|
2187
|
-
);
|
|
2188
|
-
|
|
2189
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
2190
|
-
fireEvent.click(optionsIn(container)[9]);
|
|
2191
|
-
|
|
2192
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
2193
|
-
expect(onChange.mock.calls[0][0]).toBe('2026-03-15T09:00:00');
|
|
2194
|
-
});
|
|
2195
|
-
|
|
2196
|
-
it('keeps two fields on one page independent', () => {
|
|
2197
|
-
const {container} = render(
|
|
2198
|
-
<>
|
|
2199
|
-
<DateTimeInput
|
|
2200
|
-
label="Starts"
|
|
2201
|
-
value={'2026-03-15T09:00' as ISODateTimeString}
|
|
2202
|
-
timeOptionInterval={60}
|
|
2203
|
-
onChange={() => {}}
|
|
2204
|
-
/>
|
|
2205
|
-
<DateTimeInput
|
|
2206
|
-
label="Ends"
|
|
2207
|
-
value={'2026-03-15T17:00' as ISODateTimeString}
|
|
2208
|
-
timeOptionInterval={60}
|
|
2209
|
-
onChange={() => {}}
|
|
2210
|
-
/>
|
|
2211
|
-
</>,
|
|
2212
|
-
);
|
|
2213
|
-
|
|
2214
|
-
const starts = screen.getByLabelText('Starts time');
|
|
2215
|
-
const ends = screen.getByLabelText('Ends time');
|
|
2216
|
-
|
|
2217
|
-
fireEvent.click(starts);
|
|
2218
|
-
expect(starts).toHaveAttribute('aria-expanded', 'true');
|
|
2219
|
-
expect(ends).toHaveAttribute('aria-expanded', 'false');
|
|
2220
|
-
|
|
2221
|
-
const listboxes = container.querySelectorAll('[role="listbox"]');
|
|
2222
|
-
expect(listboxes).toHaveLength(1);
|
|
2223
|
-
// Option ids must not collide between instances.
|
|
2224
|
-
const ids = optionsIn(container).map(o => o.id);
|
|
2225
|
-
expect(new Set(ids).size).toBe(ids.length);
|
|
2226
|
-
expect(starts.getAttribute('aria-controls')).toBe(listboxes[0].id);
|
|
2227
|
-
});
|
|
2228
|
-
|
|
2229
|
-
it('mounts no option nodes until the list is opened', () => {
|
|
2230
|
-
// The layer renders its children whether open or closed, so an
|
|
2231
|
-
// unconditional list would park a day's worth of divs in the DOM of
|
|
2232
|
-
// every opted-in field. At a 5-minute cadence that is 288 nodes.
|
|
2233
|
-
const {container} = render(
|
|
2234
|
-
<DateTimeInput
|
|
2235
|
-
label="Meeting"
|
|
2236
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2237
|
-
timeOptionInterval={5}
|
|
2238
|
-
onChange={() => {}}
|
|
2239
|
-
/>,
|
|
2240
|
-
);
|
|
2241
|
-
|
|
2242
|
-
expect(optionsIn(container)).toHaveLength(0);
|
|
2243
|
-
expect(container.querySelector('[role="listbox"]')).toBeNull();
|
|
2244
|
-
|
|
2245
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
2246
|
-
expect(optionsIn(container)).toHaveLength(288);
|
|
2247
|
-
});
|
|
2248
|
-
|
|
2249
|
-
it('marks the selected option when the value carries unwanted seconds', () => {
|
|
2250
|
-
// splitDateTime slices the string after the T, so an external value of
|
|
2251
|
-
// 14:00:00 with hasSeconds off leaves valueParts.time as "14:00:00"
|
|
2252
|
-
// while every option is "14:00". A raw equality check silently marks
|
|
2253
|
-
// nothing selected.
|
|
2254
|
-
const {container} = render(
|
|
2255
|
-
<DateTimeInput
|
|
2256
|
-
label="Meeting"
|
|
2257
|
-
value={'2026-03-15T14:00:00' as ISODateTimeString}
|
|
2258
|
-
timeOptionInterval={60}
|
|
2259
|
-
onChange={() => {}}
|
|
2260
|
-
/>,
|
|
2261
|
-
);
|
|
2262
|
-
|
|
2263
|
-
fireEvent.click(screen.getByLabelText('Meeting time'));
|
|
2264
|
-
const selected = optionsIn(container).filter(
|
|
2265
|
-
o => o.getAttribute('aria-selected') === 'true',
|
|
2266
|
-
);
|
|
2267
|
-
expect(selected).toHaveLength(1);
|
|
2268
|
-
expect(selected[0].textContent).toBe('2:00 PM');
|
|
2269
|
-
});
|
|
2270
|
-
|
|
2271
|
-
it('keeps focus in the time field when opening evicts the calendar', () => {
|
|
2272
|
-
// Both layers are popover="auto", so showing the time list closes the
|
|
2273
|
-
// calendar. The calendar's onHide returns focus to the date input —
|
|
2274
|
-
// which would rip focus out of the field the user is working in.
|
|
2275
|
-
render(
|
|
2276
|
-
<DateTimeInput
|
|
2277
|
-
label="Meeting"
|
|
2278
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2279
|
-
timeOptionInterval={60}
|
|
2280
|
-
onChange={() => {}}
|
|
2281
|
-
/>,
|
|
2282
|
-
);
|
|
2283
|
-
|
|
2284
|
-
const dateInput = screen.getAllByRole('combobox')[0];
|
|
2285
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2286
|
-
|
|
2287
|
-
fireEvent.keyDown(dateInput, {key: 'ArrowDown'});
|
|
2288
|
-
expect(dateInput).toHaveAttribute('aria-expanded', 'true');
|
|
2289
|
-
|
|
2290
|
-
timeInput.focus();
|
|
2291
|
-
fireEvent.keyDown(timeInput, {key: 'ArrowDown', altKey: true});
|
|
2292
|
-
|
|
2293
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
2294
|
-
expect(timeInput).toHaveFocus();
|
|
2295
|
-
});
|
|
2296
|
-
|
|
2297
|
-
it('starts closed and opens only when asked', () => {
|
|
2298
|
-
render(
|
|
2299
|
-
<DateTimeInput
|
|
2300
|
-
label="Meeting"
|
|
2301
|
-
value={'2026-03-15T14:00' as ISODateTimeString}
|
|
2302
|
-
timeOptionInterval={60}
|
|
2303
|
-
onChange={() => {}}
|
|
2304
|
-
/>,
|
|
2305
|
-
);
|
|
2306
|
-
|
|
2307
|
-
const timeInput = screen.getByLabelText('Meeting time');
|
|
2308
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'false');
|
|
2309
|
-
expect(timeInput).not.toHaveAttribute('aria-activedescendant');
|
|
2310
|
-
|
|
2311
|
-
fireEvent.click(timeInput);
|
|
2312
|
-
expect(timeInput).toHaveAttribute('aria-expanded', 'true');
|
|
2313
|
-
});
|
|
2314
|
-
});
|
|
2315
|
-
|
|
2316
|
-
describe('calendar while the popover is closed', () => {
|
|
2317
|
-
const calendarGrid = () => document.querySelector('[role="grid"]');
|
|
2318
|
-
|
|
2319
|
-
it('renders no calendar until the popover opens, and none after it closes', () => {
|
|
2320
|
-
render(
|
|
2321
|
-
<DateTimeInput
|
|
2322
|
-
label="Meeting"
|
|
2323
|
-
value={'2026-01-15T10:00' as ISODateTimeString}
|
|
2324
|
-
onChange={() => {}}
|
|
2325
|
-
/>,
|
|
2326
|
-
);
|
|
2327
|
-
const toggle = getButton('Open calendar');
|
|
2328
|
-
expect(calendarGrid()).toBeNull();
|
|
2329
|
-
|
|
2330
|
-
fireEvent.click(toggle);
|
|
2331
|
-
expect(calendarGrid()).not.toBeNull();
|
|
2332
|
-
|
|
2333
|
-
fireEvent.click(toggle);
|
|
2334
|
-
expect(calendarGrid()).toBeNull();
|
|
2335
|
-
});
|
|
2336
|
-
|
|
2337
|
-
it('opens on the month of a date typed while closed', () => {
|
|
2338
|
-
function Controlled() {
|
|
2339
|
-
const [value, setValue] = useState<ISODateTimeString | undefined>(
|
|
2340
|
-
'2026-01-15T10:00' as ISODateTimeString,
|
|
2341
|
-
);
|
|
2342
|
-
return (
|
|
2343
|
-
<DateTimeInput label="Meeting" value={value} onChange={setValue} />
|
|
2344
|
-
);
|
|
2345
|
-
}
|
|
2346
|
-
render(<Controlled />);
|
|
2347
|
-
const dateInput = screen.getAllByRole('combobox')[0];
|
|
2348
|
-
|
|
2349
|
-
fireEvent.change(dateInput, {target: {value: 'March 5, 2027'}});
|
|
2350
|
-
fireEvent.keyDown(dateInput, {key: 'ArrowDown'});
|
|
2351
|
-
|
|
2352
|
-
expect(calendarGrid()).toHaveAccessibleName('March 2027');
|
|
2353
|
-
});
|
|
2354
|
-
|
|
2355
|
-
it('moves focus into the calendar on each open from the toggle', async () => {
|
|
2356
|
-
render(
|
|
2357
|
-
<DateTimeInput
|
|
2358
|
-
label="Meeting"
|
|
2359
|
-
value={'2026-01-15T10:00' as ISODateTimeString}
|
|
2360
|
-
onChange={() => {}}
|
|
2361
|
-
/>,
|
|
2362
|
-
);
|
|
2363
|
-
const toggle = getButton('Open calendar');
|
|
2364
|
-
|
|
2365
|
-
for (let i = 0; i < 2; i++) {
|
|
2366
|
-
fireEvent.click(toggle);
|
|
2367
|
-
// The first calendar control takes focus, not the dialog surface.
|
|
2368
|
-
await waitFor(() => {
|
|
2369
|
-
expect(getButton('Previous month')).toHaveFocus();
|
|
2370
|
-
});
|
|
2371
|
-
fireEvent.click(toggle);
|
|
2372
|
-
expect(calendarGrid()).toBeNull();
|
|
2373
|
-
}
|
|
2374
|
-
});
|
|
2375
|
-
});
|
|
2376
|
-
|
|
2377
|
-
describe('weekStartsOn', () => {
|
|
2378
|
-
// The calendar popover renders in the top layer; jsdom keeps the content in
|
|
2379
|
-
// the DOM but role queries skip it, so read the columnheaders directly.
|
|
2380
|
-
const openAndReadWeekdays = (container: HTMLElement): (string | null)[] => {
|
|
2381
|
-
fireEvent.keyDown(screen.getAllByRole('combobox')[0], {key: 'ArrowDown'});
|
|
2382
|
-
return Array.from(container.querySelectorAll('[role="columnheader"]'))
|
|
2383
|
-
.slice(0, 7)
|
|
2384
|
-
.map(h => h.textContent);
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
it('defaults to a Sunday-first week', () => {
|
|
2388
|
-
const {container} = render(
|
|
2389
|
-
<DateTimeInput label="When" onChange={() => {}} />,
|
|
2390
|
-
);
|
|
2391
|
-
expect(openAndReadWeekdays(container)).toEqual([
|
|
2392
|
-
'Su',
|
|
2393
|
-
'Mo',
|
|
2394
|
-
'Tu',
|
|
2395
|
-
'We',
|
|
2396
|
-
'Th',
|
|
2397
|
-
'Fr',
|
|
2398
|
-
'Sa',
|
|
2399
|
-
]);
|
|
2400
|
-
});
|
|
2401
|
-
|
|
2402
|
-
it('forwards a numeric weekStartsOn to the calendar', () => {
|
|
2403
|
-
const {container} = render(
|
|
2404
|
-
<DateTimeInput label="When" onChange={() => {}} weekStartsOn={1} />,
|
|
2405
|
-
);
|
|
2406
|
-
expect(openAndReadWeekdays(container)).toEqual([
|
|
2407
|
-
'Mo',
|
|
2408
|
-
'Tu',
|
|
2409
|
-
'We',
|
|
2410
|
-
'Th',
|
|
2411
|
-
'Fr',
|
|
2412
|
-
'Sa',
|
|
2413
|
-
'Su',
|
|
2414
|
-
]);
|
|
2415
|
-
});
|
|
2416
|
-
|
|
2417
|
-
it('accepts a three-letter day name', () => {
|
|
2418
|
-
const {container} = render(
|
|
2419
|
-
<DateTimeInput label="When" onChange={() => {}} weekStartsOn="mon" />,
|
|
2420
|
-
);
|
|
2421
|
-
expect(openAndReadWeekdays(container)).toEqual([
|
|
2422
|
-
'Mo',
|
|
2423
|
-
'Tu',
|
|
2424
|
-
'We',
|
|
2425
|
-
'Th',
|
|
2426
|
-
'Fr',
|
|
2427
|
-
'Sa',
|
|
2428
|
-
'Su',
|
|
2429
|
-
]);
|
|
2430
|
-
});
|
|
2431
|
-
});
|
|
2432
|
-
// ===========================================================================
|
|
2433
|
-
// Segment theme targets (#4075)
|
|
2434
|
-
// ===========================================================================
|
|
2435
|
-
|
|
2436
|
-
describe('segment theme targets', () => {
|
|
2437
|
-
// The root only publishes `astryx-date-time-input`; the date and time
|
|
2438
|
-
// wrappers were anonymous nodes carrying hashed atomic classes only, so a
|
|
2439
|
-
// theme that restyles input geometry through the text-input/date-input/
|
|
2440
|
-
// time-input targets could not reach them and DateTimeInput rendered
|
|
2441
|
-
// shorter than every other input under that theme.
|
|
2442
|
-
|
|
2443
|
-
it('renders the date segment target on the date wrapper', () => {
|
|
2444
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
2445
|
-
const wrapper = screen
|
|
2446
|
-
.getByLabelText('Meeting')
|
|
2447
|
-
.closest('.astryx-date-time-input-date-segment');
|
|
2448
|
-
|
|
2449
|
-
expect(wrapper).not.toBeNull();
|
|
2450
|
-
// The wrapper is the input's own container, not an ancestor further up.
|
|
2451
|
-
expect(wrapper).toBe(screen.getByLabelText('Meeting').parentElement);
|
|
2452
|
-
});
|
|
2453
|
-
|
|
2454
|
-
it('renders the time segment target on the time wrapper', () => {
|
|
2455
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
2456
|
-
const wrapper = screen
|
|
2457
|
-
.getByLabelText('Meeting time')
|
|
2458
|
-
.closest('.astryx-date-time-input-time-segment');
|
|
2459
|
-
|
|
2460
|
-
expect(wrapper).not.toBeNull();
|
|
2461
|
-
expect(wrapper).toBe(screen.getByLabelText('Meeting time').parentElement);
|
|
2462
|
-
});
|
|
2463
|
-
|
|
2464
|
-
it('reflects size on both segments so themes can restyle geometry', () => {
|
|
2465
|
-
render(<DateTimeInput label="Meeting" size="lg" onChange={() => {}} />);
|
|
2466
|
-
|
|
2467
|
-
const date = screen
|
|
2468
|
-
.getByLabelText('Meeting')
|
|
2469
|
-
.closest('.astryx-date-time-input-date-segment');
|
|
2470
|
-
const time = screen
|
|
2471
|
-
.getByLabelText('Meeting time')
|
|
2472
|
-
.closest('.astryx-date-time-input-time-segment');
|
|
2473
|
-
|
|
2474
|
-
expect(date).toHaveAttribute('data-size', 'lg');
|
|
2475
|
-
expect(time).toHaveAttribute('data-size', 'lg');
|
|
2476
|
-
});
|
|
2477
|
-
|
|
2478
|
-
it('reflects status on both segments, mirroring the root', () => {
|
|
2479
|
-
render(
|
|
2480
|
-
<DateTimeInput
|
|
2481
|
-
label="Meeting"
|
|
2482
|
-
status={{type: 'error', message: 'Required'}}
|
|
2483
|
-
onChange={() => {}}
|
|
2484
|
-
/>,
|
|
2485
|
-
);
|
|
2486
|
-
|
|
2487
|
-
const date = screen
|
|
2488
|
-
.getByLabelText('Meeting')
|
|
2489
|
-
.closest('.astryx-date-time-input-date-segment');
|
|
2490
|
-
const time = screen
|
|
2491
|
-
.getByLabelText('Meeting time')
|
|
2492
|
-
.closest('.astryx-date-time-input-time-segment');
|
|
2493
|
-
|
|
2494
|
-
expect(date).toHaveAttribute('data-status', 'error');
|
|
2495
|
-
expect(time).toHaveAttribute('data-status', 'error');
|
|
2496
|
-
});
|
|
2497
|
-
|
|
2498
|
-
it('omits data-status when there is no status, like the root does', () => {
|
|
2499
|
-
render(<DateTimeInput label="Meeting" onChange={() => {}} />);
|
|
2500
|
-
|
|
2501
|
-
const date = screen
|
|
2502
|
-
.getByLabelText('Meeting')
|
|
2503
|
-
.closest('.astryx-date-time-input-date-segment');
|
|
2504
|
-
|
|
2505
|
-
expect(date).not.toHaveAttribute('data-status');
|
|
2506
|
-
});
|
|
2507
|
-
|
|
2508
|
-
it('keeps the root target intact', () => {
|
|
2509
|
-
const {container} = render(
|
|
2510
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
2511
|
-
);
|
|
2512
|
-
// Additive change — the existing root target still renders.
|
|
2513
|
-
expect(container.querySelector('.astryx-date-time-input')).not.toBeNull();
|
|
2514
|
-
});
|
|
2515
|
-
|
|
2516
|
-
it('exposes both segments as themeable defineTheme targets', () => {
|
|
2517
|
-
// jsdom cannot resolve the @layer cascade, so the generated CSS is what
|
|
2518
|
-
// proves a theme can actually reach these nodes.
|
|
2519
|
-
const theme = defineTheme({
|
|
2520
|
-
name: 'date-time-input-segments-test',
|
|
2521
|
-
components: {
|
|
2522
|
-
'date-time-input-date-segment': {
|
|
2523
|
-
base: {blockSize: 'var(--size-element-lg)'},
|
|
2524
|
-
'size:lg': {paddingInline: 'var(--spacing-4)'},
|
|
2525
|
-
},
|
|
2526
|
-
'date-time-input-time-segment': {
|
|
2527
|
-
base: {blockSize: 'var(--size-element-lg)'},
|
|
2528
|
-
},
|
|
2529
|
-
},
|
|
2530
|
-
});
|
|
2531
|
-
const css = generateThemeTestCSS(theme);
|
|
2532
|
-
|
|
2533
|
-
expect(css).toContain('.astryx-date-time-input-date-segment {');
|
|
2534
|
-
expect(css).toContain(
|
|
2535
|
-
'.astryx-date-time-input-date-segment[data-size="lg"]',
|
|
2536
|
-
);
|
|
2537
|
-
expect(css).toContain('.astryx-date-time-input-time-segment {');
|
|
2538
|
-
expect(css).toContain('block-size: var(--size-element-lg)');
|
|
2539
|
-
expect(css).toContain('padding-inline: var(--spacing-4)');
|
|
2540
|
-
});
|
|
2541
|
-
|
|
2542
|
-
it('renders the toggle-icon target on the calendar glyph with open/closed state', async () => {
|
|
2543
|
-
const {container} = render(
|
|
2544
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
2545
|
-
);
|
|
2546
|
-
// The stable target lands on the icon element itself (not the button),
|
|
2547
|
-
// so a theme can restyle just this glyph — mirroring
|
|
2548
|
-
// `date-input-toggle-icon`.
|
|
2549
|
-
const icon = container.querySelector(
|
|
2550
|
-
'.astryx-date-time-input-toggle-icon',
|
|
2551
|
-
);
|
|
2552
|
-
expect(icon).not.toBeNull();
|
|
2553
|
-
expect(icon).toHaveClass('astryx-icon');
|
|
2554
|
-
expect(icon).toHaveAttribute('data-state', 'collapsed');
|
|
2555
|
-
|
|
2556
|
-
fireEvent.click(getButton('Open calendar'));
|
|
2557
|
-
await waitFor(() => {
|
|
2558
|
-
expect(
|
|
2559
|
-
container.querySelector('.astryx-date-time-input-toggle-icon'),
|
|
2560
|
-
).toHaveAttribute('data-state', 'expanded');
|
|
2561
|
-
});
|
|
2562
|
-
});
|
|
2563
|
-
|
|
2564
|
-
it('renders the clock-icon target on the leading time glyph', () => {
|
|
2565
|
-
const {container} = render(
|
|
2566
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
2567
|
-
);
|
|
2568
|
-
const icon = container.querySelector(
|
|
2569
|
-
'.astryx-date-time-input-clock-icon',
|
|
2570
|
-
);
|
|
2571
|
-
expect(icon).not.toBeNull();
|
|
2572
|
-
expect(icon).toHaveClass('astryx-icon');
|
|
2573
|
-
});
|
|
2574
|
-
|
|
2575
|
-
it('leaves both leading glyphs byte-identical to a plain secondary/sm icon by default', () => {
|
|
2576
|
-
// The targets are purely additive: the stable target class and reflected
|
|
2577
|
-
// state add nothing to the render until a theme or consumer stylesheet
|
|
2578
|
-
// targets them. Guard that by diffing each glyph's StyleX classes against
|
|
2579
|
-
// a standalone secondary/sm icon, excluding only that theme metadata.
|
|
2580
|
-
const {container} = render(
|
|
2581
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
2582
|
-
);
|
|
2583
|
-
const calendarIcon = container.querySelector(
|
|
2584
|
-
'.astryx-date-time-input-toggle-icon',
|
|
2585
|
-
) as HTMLElement;
|
|
2586
|
-
const clockIcon = container.querySelector(
|
|
2587
|
-
'.astryx-date-time-input-clock-icon',
|
|
2588
|
-
) as HTMLElement;
|
|
2589
|
-
|
|
2590
|
-
const {container: refContainer} = render(
|
|
2591
|
-
<Icon icon="calendar" size="sm" color="secondary" />,
|
|
2592
|
-
);
|
|
2593
|
-
const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
|
|
2594
|
-
|
|
2595
|
-
const themeTargetClasses = new Set([
|
|
2596
|
-
'astryx-date-time-input-toggle-icon',
|
|
2597
|
-
'astryx-date-time-input-clock-icon',
|
|
2598
|
-
'collapsed',
|
|
2599
|
-
'expanded',
|
|
2600
|
-
]);
|
|
2601
|
-
const styleClasses = (el: HTMLElement) =>
|
|
2602
|
-
el.className
|
|
2603
|
-
.split(' ')
|
|
2604
|
-
.filter(c => !themeTargetClasses.has(c))
|
|
2605
|
-
.sort();
|
|
2606
|
-
|
|
2607
|
-
expect(styleClasses(calendarIcon)).toEqual(styleClasses(refIcon));
|
|
2608
|
-
expect(styleClasses(clockIcon)).toEqual(styleClasses(refIcon));
|
|
2609
|
-
});
|
|
2610
|
-
|
|
2611
|
-
it('exposes the icon targets so a theme reaches their size and per-state color', () => {
|
|
2612
|
-
// jsdom cannot resolve the @layer cascade, so the generated CSS is what
|
|
2613
|
-
// proves a same-element theme rule can reach these glyphs and win over
|
|
2614
|
-
// the icon's own base size/color.
|
|
2615
|
-
const theme = defineTheme({
|
|
2616
|
-
name: 'date-time-input-icon-targets-test',
|
|
2617
|
-
components: {
|
|
2618
|
-
'date-time-input-toggle-icon': {
|
|
2619
|
-
base: {width: '14px', height: '14px', fontSize: '14px'},
|
|
2620
|
-
'state:expanded': {color: 'var(--color-icon-primary)'},
|
|
2621
|
-
},
|
|
2622
|
-
'date-time-input-clock-icon': {
|
|
2623
|
-
base: {width: '14px', height: '14px', fontSize: '14px'},
|
|
2624
|
-
},
|
|
2625
|
-
},
|
|
2626
|
-
});
|
|
2627
|
-
const css = generateThemeTestCSS(theme);
|
|
2628
|
-
|
|
2629
|
-
expect(css).toContain('.astryx-date-time-input-toggle-icon {');
|
|
2630
|
-
expect(css).toContain(
|
|
2631
|
-
'.astryx-date-time-input-toggle-icon[data-state="expanded"]',
|
|
2632
|
-
);
|
|
2633
|
-
expect(css).toContain('.astryx-date-time-input-clock-icon {');
|
|
2634
|
-
expect(css).toContain('width: 14px');
|
|
2635
|
-
expect(css).toContain('color: var(--color-icon-primary)');
|
|
2636
|
-
});
|
|
2637
|
-
});
|
|
2638
|
-
});
|
|
2639
|
-
|
|
2640
|
-
describe('DateTimeInput disabled theme state', () => {
|
|
2641
|
-
it('reflects disabled on the root target so themes can gate paint on it', () => {
|
|
2642
|
-
const {container} = render(
|
|
2643
|
-
<DateTimeInput label="Meeting" onChange={() => {}} isDisabled />,
|
|
2644
|
-
);
|
|
2645
|
-
const root = container.querySelector('.astryx-date-time-input');
|
|
2646
|
-
expect(root).toHaveAttribute('data-disabled', 'disabled');
|
|
2647
|
-
});
|
|
2648
|
-
|
|
2649
|
-
it('omits data-disabled when enabled, like status does', () => {
|
|
2650
|
-
const {container} = render(
|
|
2651
|
-
<DateTimeInput label="Meeting" onChange={() => {}} />,
|
|
2652
|
-
);
|
|
2653
|
-
const root = container.querySelector('.astryx-date-time-input');
|
|
2654
|
-
expect(root).not.toHaveAttribute('data-disabled');
|
|
2655
|
-
});
|
|
2656
|
-
});
|