@astryxdesign/core 0.7.2 → 0.7.3-canary.02f5fe4
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/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +7 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +58 -9
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +34 -54
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +13 -10
- package/src/TimeInput/TimeInput.test.tsx +139 -7
- package/src/TimeInput/TimeInput.tsx +53 -24
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -3,11 +3,11 @@ schema_version: 3
|
|
|
3
3
|
template_version: 4
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:BottomSheetSwitcher
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: imdreamrunner
|
|
10
|
+
approved_at: 2026-10-21
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
13
13
|
verified_by:
|
|
@@ -34,16 +34,18 @@ system_specs: [spec:AST-002, spec:AST-027]
|
|
|
34
34
|
## Intent
|
|
35
35
|
|
|
36
36
|
BottomSheetSwitcher coordinates a mutually exclusive sequence of BottomSheet
|
|
37
|
-
children
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
children, each presented in its own native dialog (DEC-1), without adding a
|
|
38
|
+
prop or changing a default. Where the shipped switcher does not yet meet a
|
|
39
|
+
requirement, the row carries a present-tense implementation gap note. The
|
|
40
|
+
modality, identifier, and theming questions below stay open.
|
|
40
41
|
|
|
41
42
|
## Compatibility and migration
|
|
42
43
|
|
|
43
44
|
- Released default preserved: `yes`
|
|
44
|
-
- Compatibility class:
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
- Compatibility class: nonbreaking; public props, defaults, exports, visual
|
|
46
|
+
output, and transition behavior stay unchanged. The element the `ref` resolves
|
|
47
|
+
to, and that it changes on a handoff, is implementation rather than public
|
|
48
|
+
contract (DEC-2)
|
|
47
49
|
- Controlled/uncontrolled behavior: `activeSheet` remains fully controlled
|
|
48
50
|
- Migration decision: none
|
|
49
51
|
|
|
@@ -53,10 +55,18 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
53
55
|
|
|
54
56
|
**Owns**
|
|
55
57
|
|
|
56
|
-
-
|
|
58
|
+
- The presentation lifecycle of every directly nested BottomSheet child's native
|
|
59
|
+
dialog: which is presented, retained during a handoff, or closed.
|
|
60
|
+
Implementation gap: the switcher renders one dialog of its own and hosts
|
|
61
|
+
every child as a panel inside it; no child owns a dialog yet.
|
|
57
62
|
- Which child sheet is active, retained during a handoff, or hidden.
|
|
58
|
-
-
|
|
59
|
-
and
|
|
63
|
+
- Which sheet dialog hosts the flow (its ref, `id`, and dismissal
|
|
64
|
+
registration) and which paints the flow's one scrim, plus
|
|
65
|
+
Escape/platform-close routing and final focus return for the flow. Focus
|
|
66
|
+
containment is the native modal dialog's; the flow's one page scroll lock is
|
|
67
|
+
held for the whole modal flow (DEC-1).
|
|
68
|
+
- The flow wrapper: the isolated surface that orders non-modal sheet dialogs
|
|
69
|
+
locally (`spec:AST-027` FR2).
|
|
60
70
|
|
|
61
71
|
**Does not own / non-goals**
|
|
62
72
|
|
|
@@ -69,30 +79,27 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
69
79
|
|
|
70
80
|
## Public concepts
|
|
71
81
|
|
|
72
|
-
| Concept | Closed values or states | Meaning
|
|
73
|
-
| ------------------- | ---------------------------------------------------- |
|
|
74
|
-
| Active sheet | `null` or one nested BottomSheet `sheetId` | Selects the only interactive child, or closes the flow
|
|
75
|
-
| Shared presentation | `true` / `false` through `hasScrim` | Selects the current modal scrim-backed host or non-modal no-scrim host
|
|
76
|
-
| Child identity | Non-empty `sheetId` on each direct BottomSheet child | Associates controlled selection, labeling, purpose, and transition state with a child
|
|
77
|
-
| Dismissal request | `onActiveSheetChange(null)` | Reports an allowed implicit close without taking control from the caller
|
|
78
|
-
| Dialog surface | inherited dialog props plus `ref` and `onCancel` |
|
|
82
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
83
|
+
| ------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
|
|
84
|
+
| Active sheet | `null` or one nested BottomSheet `sheetId` | Selects the only interactive child, or closes the flow | All presentations | `null` is closed; the prop is required | `component:BottomSheetSwitcher` | released | A non-matching or duplicate id is not given a safe public fallback by the current contract |
|
|
85
|
+
| Shared presentation | `true` / `false` through `hasScrim` | Selects the current modal scrim-backed host or non-modal no-scrim host | Entire flow | `true` | `component:BottomSheetSwitcher` | released | Boolean only |
|
|
86
|
+
| Child identity | Non-empty `sheetId` on each direct BottomSheet child | Associates controlled selection, labeling, purpose, and transition state with a child | Direct nested BottomSheet children | none | `component:BottomSheet` | released | Empty ids warn and stay hidden; duplicate-id behavior is not specified |
|
|
87
|
+
| Dismissal request | `onActiveSheetChange(null)` | Reports an allowed implicit close without taking control from the caller | Escape/platform close, scrim click, or swipe according to active child purpose | none | `component:BottomSheetSwitcher`; `family:overlay-dismissal` owns ordering | released | The caller may retain its controlled value |
|
|
88
|
+
| Dialog surface | inherited dialog props plus `ref` and `onCancel` | Dialog props and handlers, `onCancel` included, reach every sheet dialog; `ref`, `id`, and `data-testid` resolve to the host dialog, which is never null for an object ref while a sheet is mounted (DEC-2). Implementation gap: every dialog prop, `ref`, `id`, and `data-testid` reaches the switcher's one shared dialog | Entire flow | none | `component:BottomSheetSwitcher` | released | Component-owned semantics and handlers retain precedence |
|
|
79
89
|
|
|
80
90
|
## Behavioral and layout contract
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
| FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test | Settled remediation in this audit |
|
|
94
|
-
| FR8 | A stale gesture from an outgoing sheet cannot change the shared scrim, and unmounting a retained sheet cannot keep the shared dialog open. | Current tests | Verified shipped behavior |
|
|
95
|
-
| FR9 | The switcher forwards its ref, neutral dialog attributes, styling inputs, and composed handlers to the shared dialog while preserving its owned ARIA and dismissal behavior. | Current source, public API architecture, and tests | Verified shipped behavior |
|
|
92
|
+
| ID | Invariant | Basis |
|
|
93
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
94
|
+
| FR1 | Zero or one child sheet is interactive for each controlled `activeSheet` value; all other children are hidden or retained inert and `aria-hidden` during a transition. | Current source, docs, and tests |
|
|
95
|
+
| FR2 | Each child sheet owns one native dialog. Opening a sheet presents its dialog once; a handoff presents the entering sheet's dialog above the retained one and closes the retained dialog only after its panel has faded. Returning to a sheet whose dialog is still open beneath a newer one re-presents it so the interactive sheet is the topmost layer. Implementation gap: the switcher presents one shared dialog once per flow and hosts every child as a panel inside it; a handoff swaps panels in that dialog, and no dialog is re-presented. | DEC-1 |
|
|
96
|
+
| FR3 | During a handoff, the entering sheet is above the retained sheet. A taller retained sheet aligns down to a shorter entering sheet, and the retained sheet fades only after required transform motion completes. | Current source and transition tests |
|
|
97
|
+
| FR4 | Closing retains the outgoing sheet through its exit, then closes the dialog. A modal flow restores focus to the element captured when that modal flow opened. | Current source and focus tests |
|
|
98
|
+
| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. Implementation gap: a script focus trap runs on top of the modal dialog's native containment; a non-modal flow is one shared dialog carrying the page-level value itself, with no flow wrapper. | DEC-1; `spec:AST-027` FR2, FR4, FR8 |
|
|
99
|
+
| FR6 | The active child purpose governs implicit dismissal: `info` allows Escape, scrim click, and swipe; `form` allows Escape; `required` blocks all three and exposes `alertdialog`. | Current BottomSheet docs and switcher tests |
|
|
100
|
+
| FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test |
|
|
101
|
+
| FR8 | One backdrop paints the flow's scrim at a time: the retained sheet's dialog keeps it through a handoff and the entering dialog takes it over without a fade when the retained dialog closes. A stale gesture from an outgoing sheet cannot change the flow's scrim, and unmounting a retained sheet cannot keep its dialog open or the flow's modal layer held. Implementation gap: the one backdrop is the shared dialog's, so no takeover between dialogs happens. | DEC-1; current tests |
|
|
102
|
+
| FR9 | The switcher forwards neutral dialog attributes, styling inputs, and composed handlers, `onCancel` included, to every sheet dialog, so a handler fires for whichever sheet dialog raises the event; renders `id` and `data-testid` on the host dialog only, so each appears once in the document; attaches its ref to the host dialog; and preserves its owned ARIA and dismissal behavior. An object ref's `current` is never `null` while a sheet is mounted. A callback ref follows React's semantics on a handoff: a cleanup it returned runs when it detaches from the old host; a callback ref without a cleanup is called with `null` on detach, then with the new host. Implementation gap: every forwarded input, `ref`, `id`, and `data-testid` reaches the one shared dialog, which never changes on a handoff. | DEC-1, DEC-2; `architecture:public-component-api` INV8 |
|
|
96
103
|
|
|
97
104
|
### Allowed variation
|
|
98
105
|
|
|
@@ -104,14 +111,18 @@ intentional decision. This draft cannot clear the gaps it records.
|
|
|
104
111
|
|
|
105
112
|
### Representative states
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
114
|
+
Where a state names a sheet's own dialog, the retained dialog, or the flow
|
|
115
|
+
wrapper, the shipped switcher meets it with its one shared dialog instead; FR2
|
|
116
|
+
and FR5 record that implementation gap.
|
|
117
|
+
|
|
118
|
+
| State | Required invariant | Allowed variation |
|
|
119
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
120
|
+
| Closed | Every sheet dialog is closed and no child is interactive. | Child panels may remain mounted but hidden. |
|
|
121
|
+
| First open | Matching child is visible and interactive in its own dialog. | Child-owned height and content. |
|
|
122
|
+
| Handoff | Entering child is interactive in its own dialog; retained child's dialog stays open, inert, and hidden from accessibility APIs until it fades, and keeps painting the one scrim. | Alignment offset depends on measured panel geometry. |
|
|
123
|
+
| Closing | Outgoing child is retained inert while exit motion completes; modal focus and scroll ownership remain until close. | Theme-controlled duration. |
|
|
124
|
+
| Modal | Native modal host, backdrop, native focus containment, and the page scroll lock are active. Implementation gap: a script focus trap runs on top of the native containment. | Active child's `purpose` controls implicit dismissal. |
|
|
125
|
+
| Non-modal | Native non-modal host has no backdrop or scroll lock and background content remains interactive. | The current clipping/stacking limitation is a conformance gap, not an allowed exception. |
|
|
115
126
|
|
|
116
127
|
### Transformation and precedence order
|
|
117
128
|
|
|
@@ -124,8 +135,13 @@ intentional decision. This draft cannot clear the gaps it records.
|
|
|
124
135
|
|
|
125
136
|
### Performance and resources
|
|
126
137
|
|
|
127
|
-
- **PR1 — One
|
|
128
|
-
|
|
138
|
+
- **PR1 — One painted scrim.** In a modal flow, at rest exactly one sheet
|
|
139
|
+
dialog is presented; during a handoff two are, with one backdrop painted.
|
|
140
|
+
Native modality holds across a modal flow's child handoffs because a sheet
|
|
141
|
+
dialog is modal throughout, and the one page scroll lock is held for the
|
|
142
|
+
whole flow rather than per step. A non-modal flow paints no backdrop and
|
|
143
|
+
holds no lock. Implementation gap: one shared dialog is presented
|
|
144
|
+
throughout, so a handoff has one dialog open, not two.
|
|
129
145
|
- **PR2 — Stable child registration.** Parent rerenders and changing consumer ref
|
|
130
146
|
identities do not unregister a mounted child or cancel an active handoff.
|
|
131
147
|
|
|
@@ -141,11 +157,11 @@ intentional decision. This draft cannot clear the gaps it records.
|
|
|
141
157
|
|
|
142
158
|
## Design relationships
|
|
143
159
|
|
|
144
|
-
| Anatomy or state | Design requirement
|
|
145
|
-
| ---------------- |
|
|
146
|
-
| Shared dialog | Transparent host for the flow's interaction and accessibility boundary
|
|
147
|
-
| Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet
|
|
148
|
-
| Scrim | Optional native backdrop that dims and blocks the page in modal presentation
|
|
160
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
161
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | -------------- | ------------------ |
|
|
162
|
+
| Shared dialog | Transparent host for the flow's interaction and accessibility boundary, one per child sheet (DEC-1). Implementation gap: one such host serves the whole flow | Current source and public docs; DEC-1 for the per-sheet clause | Structural | FR2, FR5, AR1 |
|
|
163
|
+
| Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet | `component:BottomSheet` as observational context | Prominent | FR1, FR3 |
|
|
164
|
+
| Scrim | Optional native backdrop that dims and blocks the page in modal presentation; one sheet dialog paints it at a time (DEC-1) | Current source and public docs; DEC-1 for the one-painter clause | Supporting | FR5, FR8 |
|
|
149
165
|
|
|
150
166
|
### Theming anatomy
|
|
151
167
|
|
|
@@ -163,7 +179,7 @@ intentional decision. This draft cannot clear the gaps it records.
|
|
|
163
179
|
},
|
|
164
180
|
"Scrim": {
|
|
165
181
|
"none": {
|
|
166
|
-
"reason": "reachability-gap: The switcher-
|
|
182
|
+
"reason": "reachability-gap: The switcher-coordinated native backdrop paints the scrim but has no current public theming target."
|
|
167
183
|
}
|
|
168
184
|
}
|
|
169
185
|
}
|
|
@@ -179,21 +195,72 @@ intentional decision. This draft cannot clear the gaps it records.
|
|
|
179
195
|
- `architecture:public-component-api` and `spec:AST-002` own the released prop,
|
|
180
196
|
DOM, ref, and compatibility surface.
|
|
181
197
|
- `spec:AST-027` identifies the non-modal `show()` plus page-level `z-index` path
|
|
182
|
-
as a migration gap
|
|
198
|
+
as a migration gap, not an exception.
|
|
183
199
|
|
|
184
200
|
## Verification map
|
|
185
201
|
|
|
186
|
-
| Contract | Verification
|
|
187
|
-
| ---------------- |
|
|
188
|
-
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx`
|
|
189
|
-
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`;
|
|
190
|
-
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks
|
|
191
|
-
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests
|
|
202
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
203
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
|
204
|
+
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx` | closed, first open, handoffs in either completion order, rapid replacement, unmount, close | A child becomes interactive at the wrong time, a retained child disappears early, a second backdrop paints, or a sheet dialog's lifecycle breaks. | `audit:BottomSheetSwitcher/behavior` |
|
|
205
|
+
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` (implementation gap: not yet in the tree) | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
|
|
206
|
+
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref across open/handoff/close/unmount, callback-ref cleanup, `id` once, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or duplicated, an object ref is null while a sheet is mounted or skips a cleanup, or an input overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
|
|
207
|
+
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests | dialog, delegated sheet panel, scrim | A non-painting host gains a target, a delegated panel loses its owner, or the scrim gap disappears without review. | `audit:BottomSheetSwitcher/theming` |
|
|
192
208
|
|
|
193
209
|
## Decision log
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
|
|
211
|
+
### DEC-1 — Each step owns its native dialog; the switcher coordinates
|
|
212
|
+
|
|
213
|
+
**Reference:** `component:BottomSheetSwitcher/DEC-1`
|
|
214
|
+
**Decider:** imdreamrunner, 2026-10-20
|
|
215
|
+
|
|
216
|
+
A switcher step is a BottomSheet with its own native dialog, presented and
|
|
217
|
+
closed like a standalone sheet, so one DOM shape serves standalone sheets and
|
|
218
|
+
switcher steps alike and the browser supplies modality per step: an inert
|
|
219
|
+
page, focus containment, `cancel` for Escape and the platform close, and the
|
|
220
|
+
backdrop. The switcher owns only what no single dialog can decide: which dialog
|
|
221
|
+
is presented, retained, or closed; the one scrim handed from the retained
|
|
222
|
+
dialog to the entering one without a fade; re-presenting a sheet that is still
|
|
223
|
+
open beneath a newer one when the flow returns to it; focus kept in the active
|
|
224
|
+
sheet when a retained dialog closes; focus returned to the flow's opener;
|
|
225
|
+
`cancel` gated by the active step's `purpose` through the shared dismissal
|
|
226
|
+
stack; and the flow's one page scroll lock, held from the moment a modal flow
|
|
227
|
+
is presented until after its exit, so a handoff between two open dialogs never
|
|
228
|
+
releases and re-acquires it. The switcher's one element is a flow wrapper with
|
|
229
|
+
no box in a modal flow and, in a non-modal flow, the isolated surface inside
|
|
230
|
+
which the sheet dialogs order themselves with local values.
|
|
231
|
+
Rejected: one switcher-owned dialog hosting every step as a panel, because a
|
|
232
|
+
step is then not a sheet and every standalone-sheet mechanism needs a second,
|
|
233
|
+
switcher-only implementation.
|
|
234
|
+
Rejected: a script focus trap on top of native containment, because the
|
|
235
|
+
browser already contains focus in a modal dialog and the trap fights its
|
|
236
|
+
inertness.
|
|
237
|
+
Rejected: a page-level `z-index` on a non-modal step's dialog, because
|
|
238
|
+
`spec:AST-027` FR4 forbids a new page band; the wrapper is the isolation owner.
|
|
239
|
+
|
|
240
|
+
### DEC-2 — The host dialog's identity is implementation, not contract
|
|
241
|
+
|
|
242
|
+
**Reference:** `component:BottomSheetSwitcher/DEC-2`
|
|
243
|
+
**Decider:** imdreamrunner, 2026-10-20
|
|
244
|
+
|
|
245
|
+
The `ref`, `id`, and `data-testid` of the switcher reach the dialog hosting
|
|
246
|
+
the flow: the active sheet's, the exiting sheet's through its exit, and, while
|
|
247
|
+
the flow is closed, the dialog that hosted it last or the first mounted
|
|
248
|
+
sheet's, so an object ref is never `null` while a sheet is mounted and the
|
|
249
|
+
identifying attributes appear once. Which element that is, and that it changes
|
|
250
|
+
on a handoff, is implementation. The released documentation (v0.7.1
|
|
251
|
+
`BottomSheetSwitcher.doc.mjs`) describes the ref as "Ref forwarded to the one
|
|
252
|
+
shared native dialog." and the switcher as "Its ref and shared DOM props
|
|
253
|
+
target that dialog." Code that copies `ref.current` once at mount keeps a
|
|
254
|
+
real `<dialog>`, but after a handoff that element may no longer be the host;
|
|
255
|
+
read `ref.current` at use time, or use a callback ref, to follow the flow. DOM
|
|
256
|
+
props and handlers given to the switcher (`className`, `style`, `onCancel`,
|
|
257
|
+
`onToggle`, `aria-*`, and the rest) reach every sheet dialog, with `id` and
|
|
258
|
+
`data-testid` on the host only, so a handler fires for whichever sheet dialog
|
|
259
|
+
raises the event. The owner rules this hosting — the moving host and the
|
|
260
|
+
per-dialog props alike — nonbreaking under `spec:AST-017` FR3 because no
|
|
261
|
+
visual output and no public API surface changes.
|
|
262
|
+
Rejected: classifying the moving ref as a breaking change, because no public
|
|
263
|
+
API, documented interaction, or visual output changes.
|
|
197
264
|
|
|
198
265
|
## Open questions
|
|
199
266
|
|
|
@@ -708,6 +708,32 @@ describe('BottomSheetSwitcher', () => {
|
|
|
708
708
|
expect(onActiveSheetChange).toHaveBeenNthCalledWith(2, null);
|
|
709
709
|
});
|
|
710
710
|
|
|
711
|
+
it('keeps the flow open when a file picker inside a step is dismissed, and still closes on its own close request', () => {
|
|
712
|
+
// A file input fires a bubbling `cancel` at itself when its picker is
|
|
713
|
+
// dismissed. A step renders no dialog, so it reaches the switcher's.
|
|
714
|
+
const onActiveSheetChange = vi.fn();
|
|
715
|
+
render(
|
|
716
|
+
<BottomSheetSwitcher
|
|
717
|
+
activeSheet="upload"
|
|
718
|
+
onActiveSheetChange={onActiveSheetChange}>
|
|
719
|
+
<BottomSheet sheetId="upload" label="Upload" purpose="form">
|
|
720
|
+
<input type="file" aria-label="Picture" />
|
|
721
|
+
</BottomSheet>
|
|
722
|
+
</BottomSheetSwitcher>,
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const fromPicker = new Event('cancel', {bubbles: true, cancelable: true});
|
|
726
|
+
fireEvent(screen.getByLabelText('Picture'), fromPicker);
|
|
727
|
+
|
|
728
|
+
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
729
|
+
expect(fromPicker.defaultPrevented).toBe(false);
|
|
730
|
+
|
|
731
|
+
fireEvent(getSharedDialog(), new Event('cancel', {cancelable: true}));
|
|
732
|
+
|
|
733
|
+
expect(onActiveSheetChange).toHaveBeenCalledTimes(1);
|
|
734
|
+
expect(onActiveSheetChange).toHaveBeenCalledWith(null);
|
|
735
|
+
});
|
|
736
|
+
|
|
711
737
|
it('honors purpose=required for a switcher-managed sheet', () => {
|
|
712
738
|
const onActiveSheetChange = vi.fn();
|
|
713
739
|
render(
|
|
@@ -591,8 +591,14 @@ export function BottomSheetSwitcher({
|
|
|
591
591
|
? undefined
|
|
592
592
|
: sheetLabels.get(visibleTransition.retainedSheet));
|
|
593
593
|
|
|
594
|
+
// A `cancel` that bubbles up from a step is not a close request for the
|
|
595
|
+
// flow: a file input fires one when its picker is dismissed, and a step
|
|
596
|
+
// renders no dialog of its own, so it lands here. It is left alone.
|
|
594
597
|
const handleCancel = useCallback(
|
|
595
598
|
(event: SyntheticEvent<HTMLDialogElement>) => {
|
|
599
|
+
if (event.target !== event.currentTarget) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
596
602
|
event.preventDefault();
|
|
597
603
|
if (shouldDismissOnCloseRequest()) {
|
|
598
604
|
dismissOnEscape();
|
|
@@ -382,25 +382,35 @@ describe('Calendar', () => {
|
|
|
382
382
|
// ─── Date Constraints ────────────────────────────────────────
|
|
383
383
|
|
|
384
384
|
it('respects min date constraint', () => {
|
|
385
|
-
|
|
385
|
+
const onChange = vi.fn();
|
|
386
|
+
render(
|
|
387
|
+
<Calendar focusDate="2026-01-01" min="2026-01-10" onChange={onChange} />,
|
|
388
|
+
);
|
|
386
389
|
|
|
387
390
|
// Day 5 should be disabled (before min)
|
|
388
391
|
const day5 = getDayButton(5);
|
|
389
|
-
expect(day5).
|
|
392
|
+
expect(day5).toHaveAttribute('aria-disabled', 'true');
|
|
393
|
+
// spec:AST-085 FR1: focusable through aria-disabled, not natively disabled;
|
|
394
|
+
// a click still selects nothing.
|
|
395
|
+
expect(day5).not.toHaveAttribute('disabled');
|
|
396
|
+
day5.focus();
|
|
397
|
+
expect(day5).toHaveFocus();
|
|
398
|
+
day5.click();
|
|
399
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
390
400
|
|
|
391
401
|
// Day 15 should be enabled (after min)
|
|
392
402
|
const day15 = getDayButton(15);
|
|
393
|
-
expect(day15).not.
|
|
403
|
+
expect(day15).not.toHaveAttribute('aria-disabled', 'true');
|
|
394
404
|
});
|
|
395
405
|
|
|
396
406
|
it('respects max date constraint', () => {
|
|
397
407
|
render(<Calendar focusDate="2026-01-01" max="2026-01-20" />);
|
|
398
408
|
|
|
399
409
|
const day25 = getDayButton(25);
|
|
400
|
-
expect(day25).
|
|
410
|
+
expect(day25).toHaveAttribute('aria-disabled', 'true');
|
|
401
411
|
|
|
402
412
|
const day15 = getDayButton(15);
|
|
403
|
-
expect(day15).not.
|
|
413
|
+
expect(day15).not.toHaveAttribute('aria-disabled', 'true');
|
|
404
414
|
});
|
|
405
415
|
|
|
406
416
|
// ─── Initial Visible Month ───────────────────────────────────
|
|
@@ -424,14 +434,20 @@ describe('Calendar', () => {
|
|
|
424
434
|
render(<Calendar min="2027-03-04" max="2027-06-30" />);
|
|
425
435
|
|
|
426
436
|
expect(screen.getByText('March 2027')).toBeInTheDocument();
|
|
427
|
-
expect(getDayButton(4, 'March', 2027)).not.
|
|
437
|
+
expect(getDayButton(4, 'March', 2027)).not.toHaveAttribute(
|
|
438
|
+
'aria-disabled',
|
|
439
|
+
'true',
|
|
440
|
+
);
|
|
428
441
|
});
|
|
429
442
|
|
|
430
443
|
it('opens on max when the window is entirely in the past', () => {
|
|
431
444
|
render(<Calendar min="2019-01-01" max="2019-04-30" />);
|
|
432
445
|
|
|
433
446
|
expect(screen.getByText('April 2019')).toBeInTheDocument();
|
|
434
|
-
expect(getDayButton(30, 'April', 2019)).not.
|
|
447
|
+
expect(getDayButton(30, 'April', 2019)).not.toHaveAttribute(
|
|
448
|
+
'aria-disabled',
|
|
449
|
+
'true',
|
|
450
|
+
);
|
|
435
451
|
});
|
|
436
452
|
|
|
437
453
|
it('keeps max in the last pane in the two-month layout', () => {
|
|
@@ -466,7 +482,7 @@ describe('Calendar', () => {
|
|
|
466
482
|
|
|
467
483
|
// January 4, 2026 is a Sunday - should be disabled
|
|
468
484
|
const sunday = getDayButton(4);
|
|
469
|
-
expect(sunday).
|
|
485
|
+
expect(sunday).toHaveAttribute('aria-disabled', 'true');
|
|
470
486
|
});
|
|
471
487
|
|
|
472
488
|
it('caps the end date to maxRangeSpan once a start is picked', async () => {
|
|
@@ -475,15 +491,43 @@ describe('Calendar', () => {
|
|
|
475
491
|
render(<Calendar mode="range" focusDate="2026-01-01" maxRangeSpan={7} />);
|
|
476
492
|
|
|
477
493
|
// Before a start is picked every day is selectable.
|
|
478
|
-
expect(getDayButton(20)).not.
|
|
494
|
+
expect(getDayButton(20)).not.toHaveAttribute('aria-disabled', 'true');
|
|
479
495
|
|
|
480
496
|
// Pick Jan 10 as the start. A 7-day window spans Jan 4–16 (start ± 6).
|
|
481
497
|
await user.click(getDayButton(10));
|
|
482
498
|
|
|
483
|
-
expect(getDayButton(16)).not.
|
|
484
|
-
expect(getDayButton(17)).
|
|
485
|
-
expect(getDayButton(4)).not.
|
|
486
|
-
expect(getDayButton(3)).
|
|
499
|
+
expect(getDayButton(16)).not.toHaveAttribute('aria-disabled', 'true'); // 6 days after — the edge
|
|
500
|
+
expect(getDayButton(17)).toHaveAttribute('aria-disabled', 'true'); // 7 days after — beyond the cap
|
|
501
|
+
expect(getDayButton(4)).not.toHaveAttribute('aria-disabled', 'true'); // 6 days before — symmetric
|
|
502
|
+
expect(getDayButton(3)).toHaveAttribute('aria-disabled', 'true'); // 7 days before — beyond the cap
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('keeps one Tab stop when the span cap disables the day that held it', async () => {
|
|
506
|
+
const user = userEvent.setup();
|
|
507
|
+
|
|
508
|
+
render(<Calendar mode="range" focusDate="2026-01-01" maxRangeSpan={7} />);
|
|
509
|
+
|
|
510
|
+
// Arrow from Jan 1 to Jan 25 so Jan 25 holds the grid's Tab stop.
|
|
511
|
+
getDayButton(1).focus();
|
|
512
|
+
await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}');
|
|
513
|
+
for (let i = 0; i < 3; i += 1) {
|
|
514
|
+
await user.keyboard('{ArrowRight}');
|
|
515
|
+
}
|
|
516
|
+
expect(getDayButton(25)).toHaveFocus();
|
|
517
|
+
|
|
518
|
+
// Picking Jan 10 as the start disables Jan 25 (beyond the 7-day cap).
|
|
519
|
+
await user.click(getDayButton(10));
|
|
520
|
+
expect(getDayButton(25)).toHaveAttribute('aria-disabled', 'true');
|
|
521
|
+
|
|
522
|
+
// spec:AST-085 FR1: a disabled day stays focusable but never keeps the
|
|
523
|
+
// grid's Tab stop, so the grid still has exactly one, on an enabled day.
|
|
524
|
+
const grid = screen.getByRole('grid');
|
|
525
|
+
const stops = Array.from(
|
|
526
|
+
grid.querySelectorAll<HTMLElement>('button[data-date][tabindex="0"]'),
|
|
527
|
+
);
|
|
528
|
+
expect(stops).toHaveLength(1);
|
|
529
|
+
expect(stops[0]).not.toHaveAttribute('aria-disabled', 'true');
|
|
530
|
+
expect(getDayButton(25)).toHaveAttribute('tabindex', '-1');
|
|
487
531
|
});
|
|
488
532
|
|
|
489
533
|
it('enforces minRangeSpan once a start is picked', async () => {
|
|
@@ -496,11 +540,11 @@ describe('Calendar', () => {
|
|
|
496
540
|
|
|
497
541
|
// The picked start itself stays enabled — it is the active selection
|
|
498
542
|
// anchor, not an unreachable end.
|
|
499
|
-
expect(getDayButton(10)).not.
|
|
500
|
-
expect(getDayButton(11)).
|
|
501
|
-
expect(getDayButton(12)).not.
|
|
502
|
-
expect(getDayButton(8)).not.
|
|
503
|
-
expect(getDayButton(9)).
|
|
543
|
+
expect(getDayButton(10)).not.toHaveAttribute('aria-disabled', 'true');
|
|
544
|
+
expect(getDayButton(11)).toHaveAttribute('aria-disabled', 'true'); // 1 day apart — too short
|
|
545
|
+
expect(getDayButton(12)).not.toHaveAttribute('aria-disabled', 'true'); // 3-day span — allowed
|
|
546
|
+
expect(getDayButton(8)).not.toHaveAttribute('aria-disabled', 'true'); // 3-day span the other way
|
|
547
|
+
expect(getDayButton(9)).toHaveAttribute('aria-disabled', 'true'); // 2-day span — too short
|
|
504
548
|
});
|
|
505
549
|
|
|
506
550
|
it('commits a same-day range with the default minimum', async () => {
|
|
@@ -565,8 +609,8 @@ describe('Calendar', () => {
|
|
|
565
609
|
|
|
566
610
|
// …and the start is cleared: the days that minRangeSpan disabled around
|
|
567
611
|
// the old anchor are selectable again, so a new start can be placed there.
|
|
568
|
-
expect(getDayButton(11)).not.
|
|
569
|
-
expect(getDayButton(9)).not.
|
|
612
|
+
expect(getDayButton(11)).not.toHaveAttribute('aria-disabled', 'true');
|
|
613
|
+
expect(getDayButton(9)).not.toHaveAttribute('aria-disabled', 'true');
|
|
570
614
|
});
|
|
571
615
|
|
|
572
616
|
it('announces the cleared range in the provider locale', async () => {
|
|
@@ -603,7 +647,7 @@ describe('Calendar', () => {
|
|
|
603
647
|
|
|
604
648
|
// A picked single date must not disable far-away days.
|
|
605
649
|
await user.click(getDayButton(10));
|
|
606
|
-
expect(getDayButton(25)).not.
|
|
650
|
+
expect(getDayButton(25)).not.toHaveAttribute('aria-disabled', 'true');
|
|
607
651
|
});
|
|
608
652
|
|
|
609
653
|
it('keeps a controlled value that is wider than maxRangeSpan (selection-only)', () => {
|
|
@@ -621,7 +665,7 @@ describe('Calendar', () => {
|
|
|
621
665
|
|
|
622
666
|
// No selection is in progress (both endpoints are set), so no anchor →
|
|
623
667
|
// every day is pickable and both endpoints render selected.
|
|
624
|
-
expect(getDayButton(20)).not.
|
|
668
|
+
expect(getDayButton(20)).not.toHaveAttribute('aria-disabled', 'true');
|
|
625
669
|
expect(getDayButton(5).closest('[role="gridcell"]')).toHaveAttribute(
|
|
626
670
|
'aria-selected',
|
|
627
671
|
'true',
|
|
@@ -1713,7 +1757,7 @@ describe('Calendar', () => {
|
|
|
1713
1757
|
render(<Calendar focusDate="2026-01-01" min="2026-01-05" />);
|
|
1714
1758
|
|
|
1715
1759
|
const day1 = getDayButton(1);
|
|
1716
|
-
expect(day1).
|
|
1760
|
+
expect(day1).toHaveAttribute('aria-disabled', 'true');
|
|
1717
1761
|
|
|
1718
1762
|
const day8 = getDayButton(8);
|
|
1719
1763
|
day8.focus();
|
|
@@ -1741,7 +1785,7 @@ describe('Calendar', () => {
|
|
|
1741
1785
|
render(<Calendar focusDate="2026-01-01" dateConstraints={[disableJan8]} />);
|
|
1742
1786
|
|
|
1743
1787
|
const day8 = getDayButton(8);
|
|
1744
|
-
expect(day8).
|
|
1788
|
+
expect(day8).toHaveAttribute('aria-disabled', 'true');
|
|
1745
1789
|
|
|
1746
1790
|
const day15 = getDayButton(15);
|
|
1747
1791
|
day15.focus();
|
|
@@ -1372,7 +1372,8 @@ function DayCell({
|
|
|
1372
1372
|
// Mark today's cell programmatically (APG date-picker pattern), not just
|
|
1373
1373
|
// visually, so screen-reader users can identify the current date.
|
|
1374
1374
|
aria-current={state.isToday ? 'date' : undefined}
|
|
1375
|
-
disabled
|
|
1375
|
+
// A disabled day stays focusable through aria-disabled (spec:AST-085
|
|
1376
|
+
// FR1); arrows still skip it and its click and hover are guarded.
|
|
1376
1377
|
// Initial roving tab-stop seed; useGridFocus owns it after mount.
|
|
1377
1378
|
tabIndex={isTabbableDay ? 0 : -1}
|
|
1378
1379
|
onClick={() => !state.effectivelyDisabled && onDayClick(date)}
|