@astryxdesign/core 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -3
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
- package/dist/Center/Center.d.ts +23 -16
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +7 -5
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +12 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +12 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +1 -0
- package/dist/Field/InputClearButton.d.ts +2 -2
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +5 -1
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +16 -4
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +12 -1
- package/dist/HoverCard/useHoverCard.js +2 -2
- package/dist/Indicator/CheckboxIndicator.js +2 -2
- package/dist/Indicator/RadioIndicator.js +2 -2
- package/dist/Layer/layerStack.d.ts +10 -0
- package/dist/Layer/layerStack.d.ts.map +1 -1
- package/dist/Layer/layerStack.js +21 -9
- package/dist/Layer/useLayerDismissal.d.ts +2 -3
- package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
- package/dist/Layer/useLayerDismissal.js +2 -3
- package/dist/NavIcon/NavIcon.js +2 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +12 -2
- package/dist/Popover/usePopover.d.ts +3 -2
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +4 -2
- package/dist/ProgressBar/ProgressBar.js +2 -2
- package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
- package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
- package/dist/ScrollableArea/ScrollableArea.js +144 -0
- package/dist/ScrollableArea/index.d.ts +11 -0
- package/dist/ScrollableArea/index.d.ts.map +1 -0
- package/dist/ScrollableArea/index.js +11 -0
- package/dist/StatusDot/StatusDot.js +2 -2
- package/dist/TextArea/TextArea.js +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +19 -4
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +12 -2
- package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +56 -20
- package/dist/astryx.css +15 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scrollGeometry.d.ts +24 -0
- package/dist/hooks/scrollGeometry.d.ts.map +1 -0
- package/dist/hooks/scrollGeometry.js +86 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
- package/dist/hooks/scrollOwnerRegistry.js +24 -0
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +22 -11
- package/dist/hooks/useScrollableArea.d.ts +51 -0
- package/dist/hooks/useScrollableArea.d.ts.map +1 -0
- package/dist/hooks/useScrollableArea.js +287 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +2 -6
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/derivedVarRegistry.js +1 -1
- package/dist/theme/localTokens.d.ts +8 -11
- package/dist/theme/localTokens.d.ts.map +1 -1
- package/dist/theme/localTokens.js +17 -71
- package/dist/theme/themeAdaptations.d.ts.map +1 -1
- package/dist/theme/themeAdaptations.js +4 -4
- package/dist/utils/themeProps.d.ts +10 -10
- package/dist/utils/themeProps.d.ts.map +1 -1
- package/dist/utils/themeProps.js +27 -10
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -2
- package/src/AppShell/AppShell.test.tsx +36 -0
- package/src/AppShell/AppShell.tsx +4 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +3 -1
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
- package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
- package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
- package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
- package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
- package/src/Center/Center.doc.mjs +32 -28
- package/src/Center/Center.spec.md +225 -0
- package/src/Center/Center.test.tsx +42 -4
- package/src/Center/Center.tsx +24 -17
- package/src/Chat/ChatSystemMessage.test.tsx +2 -9
- package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/DateInput/DateInput.test.tsx +4 -4
- package/src/DateInput/DateInput.tsx +15 -4
- package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
- package/src/DateTimeInput/DateTimeInput.tsx +18 -7
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
- package/src/Field/Field.test.tsx +42 -0
- package/src/Field/Field.tsx +6 -0
- package/src/Field/InputClearButton.test.tsx +35 -1
- package/src/Field/InputClearButton.tsx +7 -3
- package/src/Field/PanelSearchInput.tsx +21 -8
- package/src/FieldStatus/FieldStatus.spec.md +27 -17
- package/src/FieldStatus/FieldStatus.test.tsx +7 -5
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
- package/src/FileInput/FileInput.tsx +10 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
- package/src/HoverCard/HoverCard.doc.mjs +4 -4
- package/src/HoverCard/useHoverCard.tsx +2 -2
- package/src/Indicator/CheckboxIndicator.tsx +2 -2
- package/src/Indicator/Indicator.doc.mjs +2 -2
- package/src/Indicator/Indicator.test.tsx +1 -1
- package/src/Indicator/RadioIndicator.tsx +2 -2
- package/src/Layer/layerStack.ts +20 -9
- package/src/Layer/useLayerDismissal.ts +2 -3
- package/src/MultiSelector/MultiSelector.test.tsx +4 -4
- package/src/NavIcon/NavIcon.doc.mjs +4 -4
- package/src/NavIcon/NavIcon.tsx +2 -2
- package/src/NumberInput/NumberInput.tsx +18 -7
- package/src/Popover/Popover.doc.mjs +10 -10
- package/src/Popover/Popover.spec.md +55 -65
- package/src/Popover/Popover.test.tsx +29 -0
- package/src/Popover/usePopover.doc.mjs +4 -4
- package/src/Popover/usePopover.tsx +7 -4
- package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
- package/src/ProgressBar/ProgressBar.test.tsx +1 -31
- package/src/ProgressBar/ProgressBar.tsx +2 -2
- package/src/RadioList/RadioList.test.tsx +5 -144
- package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
- package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
- package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
- package/src/ScrollableArea/ScrollableArea.tsx +259 -0
- package/src/ScrollableArea/index.ts +26 -0
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
- package/src/Selector/Selector.test.tsx +4 -4
- package/src/Spinner/Spinner.test.tsx +0 -18
- package/src/StatusDot/StatusDot.doc.mjs +4 -4
- package/src/StatusDot/StatusDot.tsx +2 -2
- package/src/TabList/TabList.test.tsx +5 -9
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
- package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
- package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
- package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
- package/src/Table/Table.doc.mjs +2 -2
- package/src/TextArea/TextArea.doc.mjs +4 -4
- package/src/TextArea/TextArea.tsx +2 -2
- package/src/TextInput/TextInput.doc.mjs +2 -1
- package/src/TextInput/TextInput.test.tsx +94 -0
- package/src/TextInput/TextInput.tsx +22 -6
- package/src/TimeInput/TimeInput.tsx +18 -7
- package/src/Toast/ToastViewport.test.tsx +1 -39
- package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
- package/src/Typeahead/BaseTypeahead.spec.md +269 -0
- package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
- package/src/Typeahead/BaseTypeahead.tsx +99 -30
- package/src/hooks/index.ts +13 -0
- package/src/hooks/scrollGeometry.ts +155 -0
- package/src/hooks/scrollOwnerRegistry.ts +47 -0
- package/src/hooks/useFocusTrap.ts +22 -11
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
- package/src/hooks/useScrollableArea.doc.mjs +108 -0
- package/src/hooks/useScrollableArea.test.tsx +437 -0
- package/src/hooks/useScrollableArea.ts +469 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +65 -105
- package/src/theme/defineTheme.ts +3 -9
- package/src/theme/derivedVarRegistry.ts +1 -1
- package/src/theme/localTokens.ts +25 -96
- package/src/theme/publicThemeHelperContract.test.ts +2 -2
- package/src/theme/themeAdaptations.test.ts +16 -42
- package/src/theme/themeAdaptations.ts +6 -9
- package/src/utils/themeProps.test.ts +29 -10
- package/src/utils/themeProps.ts +36 -17
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 4
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:BottomSheetSwitcher
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx,
|
|
16
|
+
apps/storybook/stories/BottomSheetSwitcher.stories.tsx,
|
|
17
|
+
scripts/check-knowledge.mjs,
|
|
18
|
+
]
|
|
19
|
+
modules: []
|
|
20
|
+
families: [family:overlay-dismissal]
|
|
21
|
+
design_specs: []
|
|
22
|
+
architecture:
|
|
23
|
+
[
|
|
24
|
+
architecture:component-theming-surface,
|
|
25
|
+
architecture:layer-runtime,
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
]
|
|
28
|
+
contributing: []
|
|
29
|
+
system_specs: [spec:AST-002, spec:AST-027]
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# BottomSheetSwitcher component contract
|
|
33
|
+
|
|
34
|
+
## Intent
|
|
35
|
+
|
|
36
|
+
BottomSheetSwitcher coordinates a mutually exclusive sequence of BottomSheet
|
|
37
|
+
children inside one shared native dialog. This draft records verified shipped and
|
|
38
|
+
remediated behavior without adding a prop, changing a default, or settling the
|
|
39
|
+
open modality, hosting, identifier, or theming decisions below.
|
|
40
|
+
|
|
41
|
+
## Compatibility and migration
|
|
42
|
+
|
|
43
|
+
- Released default preserved: `yes`
|
|
44
|
+
- Compatibility class: observational backfill plus a shared-dismissal conformance
|
|
45
|
+
fix; public props, defaults, exports, DOM ownership, and transition behavior stay
|
|
46
|
+
unchanged
|
|
47
|
+
- Controlled/uncontrolled behavior: `activeSheet` remains fully controlled
|
|
48
|
+
- Migration decision: none
|
|
49
|
+
|
|
50
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
51
|
+
|
|
52
|
+
## Ownership boundary
|
|
53
|
+
|
|
54
|
+
**Owns**
|
|
55
|
+
|
|
56
|
+
- One shared native dialog for all directly nested BottomSheet children.
|
|
57
|
+
- Which child sheet is active, retained during a handoff, or hidden.
|
|
58
|
+
- Shared scrim, focus containment, scroll lock, Escape/platform-close routing,
|
|
59
|
+
and final focus return for the flow.
|
|
60
|
+
|
|
61
|
+
**Does not own / non-goals**
|
|
62
|
+
|
|
63
|
+
- A child sheet's panel, content, handle, height, snap points, swipe mechanics, or
|
|
64
|
+
local visual styling — owned by `component:BottomSheet`.
|
|
65
|
+
- Page-level stacking or clipping escape — owned by
|
|
66
|
+
`architecture:layer-runtime` and `spec:AST-027`.
|
|
67
|
+
- An ordered visible sheet stack — proposed separately by open `BottomSheetStack`
|
|
68
|
+
work and not part of this component.
|
|
69
|
+
|
|
70
|
+
## Public concepts
|
|
71
|
+
|
|
72
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
73
|
+
| ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
|
|
74
|
+
| 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 |
|
|
75
|
+
| 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 |
|
|
76
|
+
| 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 |
|
|
77
|
+
| 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 |
|
|
78
|
+
| Dialog surface | inherited dialog props plus `ref` and `onCancel` | Reaches the one shared native dialog | Entire flow | none | `component:BottomSheetSwitcher` | released | Component-owned semantics and handlers retain precedence |
|
|
79
|
+
|
|
80
|
+
## Behavioral and layout contract
|
|
81
|
+
|
|
82
|
+
Draft requirements identify their basis so observed code is not mistaken for an
|
|
83
|
+
intentional decision. This draft cannot clear the gaps it records.
|
|
84
|
+
|
|
85
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
86
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
87
|
+
| 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 | Verified shipped behavior |
|
|
88
|
+
| FR2 | All child sheets share one native dialog. Opening the first sheet opens that dialog once; handoffs do not replace it. | Current source, docs, and tests | Verified shipped behavior |
|
|
89
|
+
| 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 | Verified shipped behavior; exact visual treatment remains human-reviewed |
|
|
90
|
+
| 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 | Verified shipped behavior |
|
|
91
|
+
| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, focus containment, and page scroll lock. `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. | Current source, docs, tests, and Chromium evidence | Verified shipped behavior; the non-modal host remains a `spec:AST-027` conformance gap |
|
|
92
|
+
| 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 | Verified shipped behavior |
|
|
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 |
|
|
96
|
+
|
|
97
|
+
### Allowed variation
|
|
98
|
+
|
|
99
|
+
- **AV1 — Child content and panel geometry.** Child content, height, snap points,
|
|
100
|
+
and panel styling vary under `component:BottomSheet` without changing the
|
|
101
|
+
switcher's one-active-child protocol.
|
|
102
|
+
- **AV2 — Transition timing.** Theme motion tokens may vary timing while preserving
|
|
103
|
+
the ordering and inertness in FR3.
|
|
104
|
+
|
|
105
|
+
### Representative states
|
|
106
|
+
|
|
107
|
+
| State | Required invariant | Allowed variation |
|
|
108
|
+
| ---------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
109
|
+
| Closed | Shared dialog is closed and no child is interactive. | Child panels may remain mounted but hidden. |
|
|
110
|
+
| First open | Matching child is visible and interactive in one shared dialog. | Child-owned height and content. |
|
|
111
|
+
| Handoff | Entering child is interactive; retained child is visible but inert and hidden from accessibility APIs until it fades. | Alignment offset depends on measured panel geometry. |
|
|
112
|
+
| Closing | Outgoing child is retained inert while exit motion completes; modal focus and scroll ownership remain until close. | Theme-controlled duration. |
|
|
113
|
+
| Modal | Native modal host, backdrop, focus containment, and scroll lock are active. | Active child's `purpose` controls implicit dismissal. |
|
|
114
|
+
| 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
|
+
|
|
116
|
+
### Transformation and precedence order
|
|
117
|
+
|
|
118
|
+
- **ORD1 — Handoff completion.** Select next child → mark previous child retained
|
|
119
|
+
and inert → start entering transform and any required retained alignment → wait
|
|
120
|
+
for both transforms → fade retained child → hide it.
|
|
121
|
+
- **ORD2 — Label precedence.** Consumer `aria-label` wins; otherwise consumer
|
|
122
|
+
`aria-labelledby` wins; otherwise the active or retained child label names the
|
|
123
|
+
dialog.
|
|
124
|
+
|
|
125
|
+
### Performance and resources
|
|
126
|
+
|
|
127
|
+
- **PR1 — One shared host.** A flow keeps one dialog, focus boundary, scroll lock,
|
|
128
|
+
and backdrop across child handoffs rather than mounting a host for every child.
|
|
129
|
+
- **PR2 — Stable child registration.** Parent rerenders and changing consumer ref
|
|
130
|
+
identities do not unregister a mounted child or cancel an active handoff.
|
|
131
|
+
|
|
132
|
+
## Accessibility contract
|
|
133
|
+
|
|
134
|
+
- **AR1 — Dialog semantics.** Modal presentations expose `aria-modal`; a required
|
|
135
|
+
child changes the implicit dialog role to `alertdialog`; every visible flow has
|
|
136
|
+
a consumer-provided or active-child-derived accessible name.
|
|
137
|
+
- **AR2 — Focus and inertness.** Modal focus remains inside the dialog, retained
|
|
138
|
+
children are inert and `aria-hidden`, and focus returns after the final exit.
|
|
139
|
+
- **AR3 — Ordered dismissal.** Escape and platform close follow
|
|
140
|
+
`family:overlay-dismissal`, including IME protection and topmost-layer ordering.
|
|
141
|
+
|
|
142
|
+
## Design relationships
|
|
143
|
+
|
|
144
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
145
|
+
| ---------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------ | -------------- | ------------------ |
|
|
146
|
+
| Shared dialog | Transparent host for the flow's interaction and accessibility boundary | Current source and public docs | Structural | FR2, FR5, AR1 |
|
|
147
|
+
| Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet | `component:BottomSheet` draft as observational context | Prominent | FR1, FR3 |
|
|
148
|
+
| Scrim | Optional native backdrop that dims and blocks the page in modal presentation | Current source and public docs | Supporting | FR5 |
|
|
149
|
+
|
|
150
|
+
### Theming anatomy
|
|
151
|
+
|
|
152
|
+
<!-- anatomy-theming:v1 -->
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"Shared dialog": {
|
|
157
|
+
"none": {
|
|
158
|
+
"reason": "intentional: The transparent dialog is hosting, positioning, event, and focus machinery rather than a stable painted part."
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"Sheet panels": {
|
|
162
|
+
"delegatesTo": {"owner": "component:BottomSheet", "target": "bottom-sheet"}
|
|
163
|
+
},
|
|
164
|
+
"Scrim": {
|
|
165
|
+
"none": {
|
|
166
|
+
"reason": "reachability-gap: The switcher-owned native backdrop paints the scrim but has no current public theming target."
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Family and system relationships
|
|
173
|
+
|
|
174
|
+
- `family:overlay-dismissal` owns topmost Escape and platform-close ordering; the
|
|
175
|
+
switcher adopts that shared owner while visible.
|
|
176
|
+
- `architecture:layer-runtime` owns native dialog behavior and current hosting.
|
|
177
|
+
- `architecture:component-theming-surface` owns anatomy qualification and target
|
|
178
|
+
disposition.
|
|
179
|
+
- `architecture:public-component-api` and `spec:AST-002` own the released prop,
|
|
180
|
+
DOM, ref, and compatibility surface.
|
|
181
|
+
- `spec:AST-027` identifies the non-modal `show()` plus page-level `z-index` path
|
|
182
|
+
as a migration gap; this draft does not treat it as an exception.
|
|
183
|
+
|
|
184
|
+
## Verification map
|
|
185
|
+
|
|
186
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
187
|
+
| ---------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
|
188
|
+
| 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, or the shared host lifecycle breaks. | `audit:BottomSheetSwitcher/behavior` |
|
|
189
|
+
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; Chromium evidence | modal/non-modal, info/form/required, nested layer, IME, focus return | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges. | `audit:BottomSheetSwitcher/accessibility` |
|
|
190
|
+
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
|
|
191
|
+
| 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
|
+
|
|
193
|
+
## Decision log
|
|
194
|
+
|
|
195
|
+
None. This draft records observed behavior and one remediation already required by
|
|
196
|
+
current shared authority; it introduces no component-local design or API decision.
|
|
197
|
+
|
|
198
|
+
## Open questions
|
|
199
|
+
|
|
200
|
+
- **OQ1 — Should modality and scrim paint remain coupled behind `hasScrim`, or
|
|
201
|
+
become independent public concepts?** (`human-api`)
|
|
202
|
+
- **OQ2 — What safe behavior should a non-matching or duplicate child `sheetId`
|
|
203
|
+
have?** (`human-api`)
|
|
204
|
+
- **OQ3 — Should the switcher-owned scrim gain a public theming target?**
|
|
205
|
+
(`human-api`)
|
|
206
|
+
|
|
207
|
+
## Content boundary
|
|
208
|
+
|
|
209
|
+
This file does not duplicate consumer prop tables, audit scores, screenshots,
|
|
210
|
+
BottomSheet panel mechanics, shared dismissal rules, or layer-hosting migration
|
|
211
|
+
steps. It links to their owners.
|
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {fireEvent, render, screen} from '@testing-library/react';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createRef,
|
|
16
|
+
useState,
|
|
17
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
18
|
+
} from 'react';
|
|
15
19
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
16
|
-
import {useFocusTrap} from '../hooks';
|
|
20
|
+
import {hasActiveFocusTrapEscape, useFocusTrap} from '../hooks';
|
|
21
|
+
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
17
22
|
import {BottomSheet} from './BottomSheet';
|
|
18
23
|
import {BottomSheetSwitcher} from './BottomSheetSwitcher';
|
|
19
24
|
|
|
@@ -131,6 +136,22 @@ function NestedEscapeTrap({onEscape}: {onEscape: () => void}) {
|
|
|
131
136
|
);
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
function NestedDismissibleLayer({onDismiss}: {onDismiss: () => void}) {
|
|
140
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
141
|
+
useLayerDismissal({
|
|
142
|
+
isActive: isOpen,
|
|
143
|
+
onDismiss: () => {
|
|
144
|
+
setIsOpen(false);
|
|
145
|
+
onDismiss();
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return (
|
|
149
|
+
<button type="button">
|
|
150
|
+
{isOpen ? 'Nested layer open' : 'Nested layer closed'}
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
const panelRefA = (_element: HTMLDivElement | null) => {};
|
|
135
156
|
const panelRefB = (_element: HTMLDivElement | null) => {};
|
|
136
157
|
|
|
@@ -798,6 +819,117 @@ describe('BottomSheetSwitcher', () => {
|
|
|
798
819
|
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
799
820
|
});
|
|
800
821
|
|
|
822
|
+
it('preserves the exported focus-trap Escape signal for a modal switcher', () => {
|
|
823
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
824
|
+
|
|
825
|
+
const {unmount} = render(
|
|
826
|
+
<BottomSheetSwitcher activeSheet="details" onActiveSheetChange={() => {}}>
|
|
827
|
+
<BottomSheet sheetId="details" label="Details">
|
|
828
|
+
Content
|
|
829
|
+
</BottomSheet>
|
|
830
|
+
</BottomSheetSwitcher>,
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
expect(hasActiveFocusTrapEscape()).toBe(true);
|
|
834
|
+
unmount();
|
|
835
|
+
expect(hasActiveFocusTrapEscape()).toBe(false);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('dismisses a non-modal flow when a consumer stops Escape propagation', () => {
|
|
839
|
+
const onActiveSheetChange = vi.fn();
|
|
840
|
+
const onKeyDown = vi.fn((event: ReactKeyboardEvent) => {
|
|
841
|
+
event.stopPropagation();
|
|
842
|
+
});
|
|
843
|
+
render(
|
|
844
|
+
<BottomSheetSwitcher
|
|
845
|
+
activeSheet="details"
|
|
846
|
+
onActiveSheetChange={onActiveSheetChange}
|
|
847
|
+
hasScrim={false}
|
|
848
|
+
onKeyDown={onKeyDown}>
|
|
849
|
+
<BottomSheet sheetId="details" label="Details">
|
|
850
|
+
Content
|
|
851
|
+
</BottomSheet>
|
|
852
|
+
</BottomSheetSwitcher>,
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
fireEvent.keyDown(getSharedDialog(), {key: 'Escape'});
|
|
856
|
+
|
|
857
|
+
expect(onKeyDown).toHaveBeenCalledTimes(1);
|
|
858
|
+
expect(onActiveSheetChange).toHaveBeenCalledWith(null);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('honors a consumer that prevents the default Escape dismissal', () => {
|
|
862
|
+
const onActiveSheetChange = vi.fn();
|
|
863
|
+
const onKeyDown = vi.fn((event: ReactKeyboardEvent) => {
|
|
864
|
+
event.preventDefault();
|
|
865
|
+
});
|
|
866
|
+
render(
|
|
867
|
+
<BottomSheetSwitcher
|
|
868
|
+
activeSheet="details"
|
|
869
|
+
onActiveSheetChange={onActiveSheetChange}
|
|
870
|
+
hasScrim={false}
|
|
871
|
+
onKeyDown={onKeyDown}>
|
|
872
|
+
<BottomSheet sheetId="details" label="Details">
|
|
873
|
+
Content
|
|
874
|
+
</BottomSheet>
|
|
875
|
+
</BottomSheetSwitcher>,
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
fireEvent.keyDown(getSharedDialog(), {key: 'Escape'});
|
|
879
|
+
|
|
880
|
+
expect(onKeyDown).toHaveBeenCalledTimes(1);
|
|
881
|
+
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('lets a nested registered layer handle Escape before a non-modal switcher', () => {
|
|
885
|
+
const onActiveSheetChange = vi.fn();
|
|
886
|
+
const onNestedDismiss = vi.fn();
|
|
887
|
+
render(
|
|
888
|
+
<BottomSheetSwitcher
|
|
889
|
+
activeSheet="details"
|
|
890
|
+
onActiveSheetChange={onActiveSheetChange}
|
|
891
|
+
hasScrim={false}>
|
|
892
|
+
<BottomSheet sheetId="details" label="Details">
|
|
893
|
+
<NestedDismissibleLayer onDismiss={onNestedDismiss} />
|
|
894
|
+
</BottomSheet>
|
|
895
|
+
</BottomSheetSwitcher>,
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const trigger = screen.getByRole('button', {name: 'Nested layer open'});
|
|
899
|
+
trigger.focus();
|
|
900
|
+
fireEvent.keyDown(trigger, {key: 'Escape'});
|
|
901
|
+
|
|
902
|
+
expect(onNestedDismiss).toHaveBeenCalledTimes(1);
|
|
903
|
+
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
904
|
+
|
|
905
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
906
|
+
|
|
907
|
+
expect(onNestedDismiss).toHaveBeenCalledTimes(1);
|
|
908
|
+
expect(onActiveSheetChange).toHaveBeenCalledWith(null);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('keeps a non-modal switcher open when a platform close targets it under a nested layer', () => {
|
|
912
|
+
const onActiveSheetChange = vi.fn();
|
|
913
|
+
const onNestedDismiss = vi.fn();
|
|
914
|
+
render(
|
|
915
|
+
<BottomSheetSwitcher
|
|
916
|
+
activeSheet="details"
|
|
917
|
+
onActiveSheetChange={onActiveSheetChange}
|
|
918
|
+
hasScrim={false}>
|
|
919
|
+
<BottomSheet sheetId="details" label="Details">
|
|
920
|
+
<NestedDismissibleLayer onDismiss={onNestedDismiss} />
|
|
921
|
+
</BottomSheet>
|
|
922
|
+
</BottomSheetSwitcher>,
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const event = new Event('cancel', {cancelable: true});
|
|
926
|
+
getSharedDialog().dispatchEvent(event);
|
|
927
|
+
|
|
928
|
+
expect(event.defaultPrevented).toBe(true);
|
|
929
|
+
expect(onNestedDismiss).not.toHaveBeenCalled();
|
|
930
|
+
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
931
|
+
});
|
|
932
|
+
|
|
801
933
|
it('returns focus to the original opener after a multi-sheet flow ends', () => {
|
|
802
934
|
render(<Flow />);
|
|
803
935
|
const opener = screen.getByRole('button', {name: 'Start flow'});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file BottomSheetSwitcher.tsx
|
|
7
|
-
* @input Uses React context, StyleX, theme tokens, focus/scroll-lock hooks, BottomSheetSwitcherContext
|
|
7
|
+
* @input Uses React context, StyleX, theme tokens, shared layer dismissal, focus/scroll-lock hooks, BottomSheetSwitcherContext
|
|
8
8
|
* @output Exports BottomSheetSwitcher and BottomSheetSwitcherProps
|
|
9
9
|
* @position Core switcher for mutually exclusive BottomSheet flows
|
|
10
10
|
*
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* - /packages/core/src/BottomSheet/BottomSheet.tsx
|
|
25
25
|
* - /packages/core/src/BottomSheet/BottomSheetEdgeTint.tsx
|
|
26
26
|
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.doc.mjs
|
|
27
|
+
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.spec.md
|
|
27
28
|
* - /packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx
|
|
28
29
|
* - /packages/core/src/BottomSheet/index.ts
|
|
29
30
|
* - /apps/storybook/stories/BottomSheetSwitcher.stories.tsx
|
|
@@ -45,8 +46,15 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
45
46
|
import type {BaseProps} from '../BaseProps';
|
|
46
47
|
import type {DialogPurpose} from '../Dialog';
|
|
47
48
|
import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
49
|
+
import {useScrollLock} from '../hooks';
|
|
50
|
+
import {
|
|
51
|
+
useFocusTrap,
|
|
52
|
+
useFocusTrapEscapeCompatibilitySignal,
|
|
53
|
+
} from '../hooks/useFocusTrap';
|
|
54
|
+
import {LayerDepthProvider} from '../Layer/LayerDepthContext';
|
|
55
|
+
import {dispatchLayerEscapeKeyDown} from '../Layer/layerStack';
|
|
56
|
+
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
57
|
+
import {composeEventHandlers, mergeProps} from '../utils';
|
|
50
58
|
import {BottomSheetEdgeTint} from './BottomSheetEdgeTint';
|
|
51
59
|
import {
|
|
52
60
|
BottomSheetSwitcherContext,
|
|
@@ -203,6 +211,16 @@ export interface BottomSheetSwitcherProps extends BaseProps<HTMLDialogElement> {
|
|
|
203
211
|
* Coordinates a set of BottomSheets so zero or one is active at a time inside
|
|
204
212
|
* one shared native dialog. During a handoff the previous panel stays visible
|
|
205
213
|
* and inert beneath the entering panel, then fades after motion completes.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```
|
|
217
|
+
* <BottomSheetSwitcher
|
|
218
|
+
* activeSheet={activeSheet}
|
|
219
|
+
* onActiveSheetChange={setActiveSheet}>
|
|
220
|
+
* <BottomSheet sheetId="details" label="Details">…</BottomSheet>
|
|
221
|
+
* <BottomSheet sheetId="confirm" label="Confirm">…</BottomSheet>
|
|
222
|
+
* </BottomSheetSwitcher>
|
|
223
|
+
* ```
|
|
206
224
|
*/
|
|
207
225
|
export function BottomSheetSwitcher({
|
|
208
226
|
activeSheet,
|
|
@@ -282,7 +300,17 @@ export function BottomSheetSwitcher({
|
|
|
282
300
|
}, [allowsLightDismiss, onActiveSheetChange]);
|
|
283
301
|
const {containerRef} = useFocusTrap<HTMLDialogElement>({
|
|
284
302
|
isActive: isModal,
|
|
285
|
-
|
|
303
|
+
});
|
|
304
|
+
// Before the shared dismissal stack, this modal trap supplied `onEscape`, so
|
|
305
|
+
// the released compatibility shim reported it as active. Keep that signal
|
|
306
|
+
// without registering the switcher twice in the one shared stack.
|
|
307
|
+
useFocusTrapEscapeCompatibilitySignal(isModal);
|
|
308
|
+
const {shouldDismissOnCloseRequest} = useLayerDismissal({
|
|
309
|
+
isActive: isFlowVisible,
|
|
310
|
+
escapeBehavior: allowsEscapeDismiss ? 'close' : 'block',
|
|
311
|
+
onDismiss: dismissOnEscape,
|
|
312
|
+
getContainer: () => dialogRef.current,
|
|
313
|
+
isPresent: () => dialogRef.current?.open ?? false,
|
|
286
314
|
});
|
|
287
315
|
useScrollLock(isModal);
|
|
288
316
|
|
|
@@ -565,26 +593,21 @@ export function BottomSheetSwitcher({
|
|
|
565
593
|
const handleCancel = useCallback(
|
|
566
594
|
(event: SyntheticEvent<HTMLDialogElement>) => {
|
|
567
595
|
event.preventDefault();
|
|
568
|
-
|
|
596
|
+
if (shouldDismissOnCloseRequest()) {
|
|
597
|
+
dismissOnEscape();
|
|
598
|
+
}
|
|
569
599
|
},
|
|
570
|
-
[dismissOnEscape],
|
|
600
|
+
[dismissOnEscape, shouldDismissOnCloseRequest],
|
|
571
601
|
);
|
|
572
602
|
const handleKeyDown = useCallback(
|
|
573
603
|
(event: ReactKeyboardEvent<HTMLDialogElement>) => {
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
event.key === 'Escape' &&
|
|
580
|
-
!isImeKeyEvent(event.nativeEvent) &&
|
|
581
|
-
!hasActiveFocusTrapEscape()
|
|
582
|
-
) {
|
|
583
|
-
event.preventDefault();
|
|
584
|
-
dismissOnEscape();
|
|
585
|
-
}
|
|
604
|
+
// A consumer may stop propagation without claiming Escape. Route that
|
|
605
|
+
// unprevented press now so the shared stack still chooses the top layer;
|
|
606
|
+
// its preventDefault marker makes the document listener a no-op if the
|
|
607
|
+
// event does continue bubbling.
|
|
608
|
+
dispatchLayerEscapeKeyDown(event.nativeEvent);
|
|
586
609
|
},
|
|
587
|
-
[
|
|
610
|
+
[],
|
|
588
611
|
);
|
|
589
612
|
const handleClick = useCallback(
|
|
590
613
|
(event: ReactMouseEvent<HTMLDialogElement>) => {
|
|
@@ -626,7 +649,7 @@ export function BottomSheetSwitcher({
|
|
|
626
649
|
{...(activeSheetPurpose === 'required'
|
|
627
650
|
? {role: 'alertdialog'}
|
|
628
651
|
: undefined)}>
|
|
629
|
-
{children}
|
|
652
|
+
<LayerDepthProvider>{children}</LayerDepthProvider>
|
|
630
653
|
<BottomSheetEdgeTint />
|
|
631
654
|
</dialog>
|
|
632
655
|
</BottomSheetSwitcherContext>
|
|
@@ -8,6 +8,10 @@ export const docs = {
|
|
|
8
8
|
displayName: 'Breadcrumb Item',
|
|
9
9
|
isHiddenFromOverview: true,
|
|
10
10
|
description: 'Individual breadcrumb item that renders as a link when href is provided, or as plain text for the current page.',
|
|
11
|
+
usage: {
|
|
12
|
+
description:
|
|
13
|
+
'BreadcrumbItem represents one destination, action, current location, or sibling-menu trigger inside a Breadcrumbs trail.',
|
|
14
|
+
},
|
|
11
15
|
props: [
|
|
12
16
|
{
|
|
13
17
|
name: 'children',
|
|
@@ -28,8 +32,8 @@ export const docs = {
|
|
|
28
32
|
{
|
|
29
33
|
name: 'isCurrent',
|
|
30
34
|
type: 'boolean',
|
|
31
|
-
description:
|
|
32
|
-
|
|
35
|
+
description:
|
|
36
|
+
'Marks this item as the current page, applying aria-current="page". When omitted, the last item is auto-detected if no item is explicitly current; pass false to opt out.',
|
|
33
37
|
},
|
|
34
38
|
{
|
|
35
39
|
name: 'startIcon',
|
|
@@ -90,8 +94,8 @@ export const docsZh = {
|
|
|
90
94
|
{
|
|
91
95
|
name: 'isCurrent',
|
|
92
96
|
type: 'boolean',
|
|
93
|
-
description:
|
|
94
|
-
|
|
97
|
+
description:
|
|
98
|
+
'将此项标记为当前页面,应用 aria-current="page"。省略时,如果没有显式的当前项,则自动将最后一项标记为当前项;传入 false 可退出自动检测。',
|
|
95
99
|
},
|
|
96
100
|
{
|
|
97
101
|
name: 'startIcon',
|
|
@@ -127,7 +131,8 @@ export const docsDense = {
|
|
|
127
131
|
children: 'label content',
|
|
128
132
|
href: 'link URL; omit for non-navigable items',
|
|
129
133
|
onClick: 'click handler',
|
|
130
|
-
isCurrent:
|
|
134
|
+
isCurrent:
|
|
135
|
+
'marks current page w/ aria-current="page"; omitted auto-detects the last item; false opts out',
|
|
131
136
|
startIcon: 'icon before label',
|
|
132
137
|
menu: 'DropdownMenuOption[] | children; opens a menu trigger (aria-haspopup="menu"); reuses the DropdownMenu item API',
|
|
133
138
|
menuSize: "menu item size; defaults from variant (supporting→sm, else md)",
|