@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,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 4
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:BaseTypeahead
|
|
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/Typeahead/BaseTypeahead.test.tsx,
|
|
16
|
+
packages/core/src/Typeahead/Typeahead.test.tsx,
|
|
17
|
+
packages/core/src/Tokenizer/Tokenizer.test.tsx,
|
|
18
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
19
|
+
scripts/check-knowledge.mjs,
|
|
20
|
+
]
|
|
21
|
+
modules: []
|
|
22
|
+
families: [family:overlay-dismissal]
|
|
23
|
+
design_specs: []
|
|
24
|
+
architecture:
|
|
25
|
+
[
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
architecture:component-theming-surface,
|
|
28
|
+
architecture:layer-runtime,
|
|
29
|
+
]
|
|
30
|
+
contributing: []
|
|
31
|
+
system_specs: []
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# BaseTypeahead component contract
|
|
35
|
+
|
|
36
|
+
## Intent
|
|
37
|
+
|
|
38
|
+
BaseTypeahead is the exported combobox engine beneath Typeahead and Tokenizer. It
|
|
39
|
+
renders a bare input, search and bootstrap behavior, keyboard navigation, a
|
|
40
|
+
styled result listbox, loading feedback, and selection callbacks. A direct caller
|
|
41
|
+
supplies the visible input wrapper, accessible name, and selected-value
|
|
42
|
+
presentation.
|
|
43
|
+
|
|
44
|
+
This draft records verified shipped or audit-remediated behavior. It does not
|
|
45
|
+
approve a new prop, default, compatibility promise, visual treatment, or
|
|
46
|
+
ownership boundary.
|
|
47
|
+
|
|
48
|
+
## Compatibility and migration
|
|
49
|
+
|
|
50
|
+
- Released default preserved: `yes`
|
|
51
|
+
- Compatibility class: patch corrections preserve the existing public surface
|
|
52
|
+
- Controlled/uncontrolled behavior: controlled `value` remains unchanged
|
|
53
|
+
- Migration decision: none; unresolved public-surface cleanup requires a
|
|
54
|
+
separate compatibility decision
|
|
55
|
+
|
|
56
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
57
|
+
|
|
58
|
+
## Ownership boundary
|
|
59
|
+
|
|
60
|
+
**Owns**
|
|
61
|
+
|
|
62
|
+
- Query text, search/bootstrap scheduling, stale-response rejection, result
|
|
63
|
+
ordering, highlight, and selection callbacks.
|
|
64
|
+
- Combobox, listbox, option, busy, selected, and empty-result semantics.
|
|
65
|
+
- The anchored result popup and current dropdown/empty-state visual treatment.
|
|
66
|
+
- Direct-caller loading feedback when a composed wrapper does not take over the
|
|
67
|
+
busy indicator lane.
|
|
68
|
+
|
|
69
|
+
**Does not own / non-goals**
|
|
70
|
+
|
|
71
|
+
- The visible input wrapper, label presentation, field border, or focus ring.
|
|
72
|
+
- Selected-value or token presentation in Typeahead and Tokenizer.
|
|
73
|
+
- Caller-rendered result content.
|
|
74
|
+
- Spinner presentation, owned by `component:Spinner`.
|
|
75
|
+
- Shared top-layer hosting, positioning, and dismissal behavior, owned by
|
|
76
|
+
`architecture:layer-runtime` and `family:overlay-dismissal`.
|
|
77
|
+
|
|
78
|
+
## Public concepts
|
|
79
|
+
|
|
80
|
+
| Concept | Closed values or states | Meaning | Availability by state | Default | Owner | Stability | Invalid-value behavior |
|
|
81
|
+
| --------------- | ---------------------------------------- | ------------------------------------------------------------ | -------------------------- | -------------------------------- | ---------------------------- | --------- | ---------------------------------------------------------------------------------------- |
|
|
82
|
+
| selection | `T` or `null` | Caller-controlled selected result | all states | required | `component:BaseTypeahead` | released | caller retains control |
|
|
83
|
+
| search source | `search`, `bootstrap`, optional `cancel` | Supplies query and focus results | enabled input | required | `component:BaseTypeahead` | released | rejected work clears current results |
|
|
84
|
+
| focus bootstrap | on or off | Offers bootstrap results before query input | empty focused input | off | `component:BaseTypeahead` | released | off keeps the menu closed |
|
|
85
|
+
| query threshold | non-negative number | Minimum visible-character count before search | non-empty query | `1` | `component:BaseTypeahead` | released | below threshold cancels work and closes results |
|
|
86
|
+
| debounce | milliseconds | Delays query search | typed query | `150` | `component:BaseTypeahead` | released | non-positive runs immediately |
|
|
87
|
+
| result cap | number | Limits fetched results shown | completed search/bootstrap | `10` | `component:BaseTypeahead` | released | source order is preserved |
|
|
88
|
+
| result content | default, `renderItem`, or `item.element` | Chooses content inside the stable option row | result present | default TypeaheadItem | caller and component | released | `item.element` takes precedence |
|
|
89
|
+
| disabled state | native or focusable-disabled | Blocks query mutation; native disabled blocks all activation | disabled | native disabled | component and caller wrapper | released | an already-open focusable-disabled list can still select with Enter (retained violation) |
|
|
90
|
+
| popup width | intrinsic or fixed pixels | Sets result popup width before viewport clamping | popup present | intrinsic, at least anchor width | `component:BaseTypeahead` | released | viewport fit wins |
|
|
91
|
+
| size | `sm`, `md`, `lg` | Selects option-row padding | popup options | `md` | `component:BaseTypeahead` | released | TypeScript rejects other values |
|
|
92
|
+
|
|
93
|
+
## Behavioral and layout contract
|
|
94
|
+
|
|
95
|
+
Draft requirements identify their observational or current-authority basis.
|
|
96
|
+
|
|
97
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
98
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------- |
|
|
99
|
+
| FR1 | The input is controlled by internal query state while `value` is caller-controlled; selecting a result calls `onChange(item)`, clears query/results, closes the popup, and returns focus to the input. | Current source and tests | verified shipped behavior |
|
|
100
|
+
| FR2 | Search starts only at the grapheme-count threshold, uses the configured debounce, and never presents an empty result for a query that was not searched. | Public API, current i18n character utility, tests | verified audit remediation |
|
|
101
|
+
| FR3 | A newer query, selection, clear, or unmount invalidates stale asynchronous work. Escape hides the current popup but does not invalidate pending work, so a late response currently reopens it (retained violation). | Current source, focused retained-red probe, and tests | verified shipped behavior |
|
|
102
|
+
| FR4 | The popup is a named listbox whose result rows are options. A completed empty search renders one disabled option so the listbox retains a valid owned child. | APG combobox pattern, axe, tests | verified audit remediation |
|
|
103
|
+
| FR5 | Arrow keys wrap the highlight; Home/End move to the first/last option; Enter selects; Escape and Tab hide the current popup; IME-owned key events do not activate combobox commands. Escape dismissal is not durable while source work remains pending (FR3). | Current source and tests | verified shipped behavior; Escape gap retained |
|
|
104
|
+
| FR6 | Pending asynchronous source work sets `aria-busy` and renders one named Spinner unless the composed wrapper owns the busy indicator lane. | Current source, input-family FR7, tests | verified shipped behavior |
|
|
105
|
+
| FR7 | Supported BaseProps DOM, ARIA, data, class, style, event, and `xstyle` inputs reach the combobox input while component-owned role, state, value, and behavior remain authoritative. | `architecture:public-component-api/INV5–INV7`, tests | verified audit remediation |
|
|
106
|
+
| FR8 | The popup remains within the inline viewport at 320 CSS px, including long default results and a requested width larger than the available viewport. | WCAG 1.4.10, real Chromium | verified audit remediation |
|
|
107
|
+
| FR9 | Native disabled removes ordinary focus and activation. Focusable-disabled uses `aria-disabled` plus `readOnly` and blocks text/query mutation, but applying it after the popup opens does not currently block Enter selection (retained violation). | Current source, tests, and focused retained-red probe | verified shipped behavior |
|
|
108
|
+
|
|
109
|
+
### Allowed variation
|
|
110
|
+
|
|
111
|
+
- Search and bootstrap may complete synchronously or asynchronously.
|
|
112
|
+
- Results may be grouped or ungrouped and may use default or caller-rendered
|
|
113
|
+
content.
|
|
114
|
+
- A direct caller may anchor the popup to the input or to its own wrapper.
|
|
115
|
+
- Typeahead and Tokenizer may own the visible wrapper and busy-indicator lane.
|
|
116
|
+
|
|
117
|
+
### Representative states
|
|
118
|
+
|
|
119
|
+
| State | Required invariant | Allowed variation |
|
|
120
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
121
|
+
| rest | named combobox is closed and not busy | caller-owned wrapper and label |
|
|
122
|
+
| focused bootstrap | popup opens only when enabled and results exist | synchronous or asynchronous source |
|
|
123
|
+
| query pending | combobox is busy and duplicate stale work is rejected | direct or wrapper-owned Spinner |
|
|
124
|
+
| results | one highlighted option and valid active descendant | grouped/default/custom content |
|
|
125
|
+
| completed empty | one disabled empty option; no active descendant | caller-supplied empty text |
|
|
126
|
+
| selected result | matching option exposes `aria-selected=true` | generic check presentation |
|
|
127
|
+
| disabled | native disabled blocks activation; focusable-disabled blocks query mutation but can still select an already-open highlight with Enter | native or focusable-disabled semantics |
|
|
128
|
+
| dismissed pending | Escape hides the popup; a late pending response currently reopens it | source completion timing |
|
|
129
|
+
| narrow viewport | popup and result content remain inside two viewport gutters | intrinsic or requested width |
|
|
130
|
+
|
|
131
|
+
### Transformation and precedence order
|
|
132
|
+
|
|
133
|
+
- **ORD1 — Result content.** `item.element` → caller `renderItem` →
|
|
134
|
+
TypeaheadItem.
|
|
135
|
+
- **ORD2 — Input props.** Consumer rest props → defined legacy aliases (`inputId`,
|
|
136
|
+
`ariaDescribedBy`, `ariaLabelledBy`, `inputTabIndex`) → component-owned
|
|
137
|
+
semantics and handlers → component styles → consumer class/style escape
|
|
138
|
+
hatches. An omitted legacy alias preserves its equivalent native BaseProp.
|
|
139
|
+
|
|
140
|
+
### Performance and resources
|
|
141
|
+
|
|
142
|
+
- **PR1 — Search lifetime.** One generation counter rejects stale responses;
|
|
143
|
+
debounce timers and optional source cancellation are cleared on replacement and
|
|
144
|
+
unmount.
|
|
145
|
+
|
|
146
|
+
## Accessibility contract
|
|
147
|
+
|
|
148
|
+
- **AR1 — Combobox pattern.** DOM focus stays on the input while
|
|
149
|
+
`aria-activedescendant` identifies the highlighted option in the named listbox.
|
|
150
|
+
- **AR2 — Accessible name.** A direct caller supplies `aria-label` or a valid
|
|
151
|
+
`aria-labelledby` relationship; composed owners supply their visible label ID.
|
|
152
|
+
- **AR3 — State.** Expanded, busy, disabled, active-descendant, and selected
|
|
153
|
+
states are exposed programmatically.
|
|
154
|
+
- **AR4 — Result feedback.** Active queries announce the localized result count
|
|
155
|
+
or empty-result message through the shared announcer.
|
|
156
|
+
- **AR5 — Input method.** IME composition commands remain with the candidate
|
|
157
|
+
window; ordinary keyboard and pointer selection remain equivalent.
|
|
158
|
+
|
|
159
|
+
## Design relationships
|
|
160
|
+
|
|
161
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
162
|
+
| -------------------- | ----------------------------------------------- | -------------------------------- | ----------------- | ------------------ |
|
|
163
|
+
| Input | caller-owned visible chrome and focus treatment | unsettled ownership for bare use | prominent | AR2 |
|
|
164
|
+
| Loading status | shared Spinner | `component:Spinner` | supporting | FR6 |
|
|
165
|
+
| Dropdown | Popover surface with bounded listbox | current source and layer runtime | prominent | FR4, FR8 |
|
|
166
|
+
| Highlighted result | overlay plus forced-color outline | objective accessibility standard | prominent | FR5 |
|
|
167
|
+
| Empty state | disabled option message | APG/axe | supporting | FR4 |
|
|
168
|
+
| Default item content | TypeaheadItem | `component:Typeahead` | prominent | ORD1 |
|
|
169
|
+
| Caller item content | caller-owned | caller | context-dependent | ORD1 |
|
|
170
|
+
|
|
171
|
+
### Theming anatomy
|
|
172
|
+
|
|
173
|
+
<!-- anatomy-theming:v1 -->
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"Input": {
|
|
178
|
+
"none": {
|
|
179
|
+
"reason": "intentional: The direct caller owns the bare input chrome and styles it through supported input styling props."
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"Loading status": {
|
|
183
|
+
"delegatesTo": {"owner": "component:Spinner", "target": "spinner"}
|
|
184
|
+
},
|
|
185
|
+
"Dropdown": {
|
|
186
|
+
"delegatesTo": {
|
|
187
|
+
"owner": "component:Typeahead",
|
|
188
|
+
"target": "typeahead-dropdown"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"Empty state": {
|
|
192
|
+
"delegatesTo": {
|
|
193
|
+
"owner": "component:Typeahead",
|
|
194
|
+
"target": "typeahead-empty-state"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"Result group heading": {
|
|
198
|
+
"none": {
|
|
199
|
+
"reason": "reachability-gap: No current public target reaches the visible group heading."
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"Result row": {
|
|
203
|
+
"none": {
|
|
204
|
+
"reason": "reachability-gap: The stable option row owns highlight and selection but has no current target."
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"Default item content": {
|
|
208
|
+
"delegatesTo": {
|
|
209
|
+
"owner": "component:Typeahead",
|
|
210
|
+
"target": "typeahead-item"
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"Caller-rendered item content": {
|
|
214
|
+
"none": {
|
|
215
|
+
"reason": "intentional: Caller-rendered result content remains caller-owned."
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"Selected result state": {
|
|
219
|
+
"none": {
|
|
220
|
+
"reason": "reachability-gap: No current Typeahead target or state reaches the outer selected option."
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Family and system relationships
|
|
227
|
+
|
|
228
|
+
- `family:overlay-dismissal` owns Escape and platform-close ordering while the
|
|
229
|
+
popup is present.
|
|
230
|
+
- `architecture:layer-runtime` owns top-layer hosting, anchoring, viewport
|
|
231
|
+
positioning, and native light dismissal.
|
|
232
|
+
- `architecture:public-component-api` owns reachable exports and BaseProps
|
|
233
|
+
passthrough semantics.
|
|
234
|
+
- `architecture:component-theming-surface` owns anatomy qualification and target
|
|
235
|
+
disposition.
|
|
236
|
+
|
|
237
|
+
## Verification map
|
|
238
|
+
|
|
239
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
240
|
+
| --------------- | ------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------- |
|
|
241
|
+
| FR1–FR2 | BaseTypeahead and Typeahead tests | query, overlap, select, clear | current work replaces newer results or selection does not close | `audit:BaseTypeahead/behavior` |
|
|
242
|
+
| FR3 | shared stale-response tests plus retained-red probe | newer query, clear, unmount, Escape while pending | late work replaces current results or reopens after explicit dismissal | `audit:BaseTypeahead/a11y` |
|
|
243
|
+
| FR4, AR1–AR4 | focused tests, component-scoped axe, live-region tests | results, empty, busy, selected | invalid listbox ownership or state/announcement disappears | `audit:BaseTypeahead/a11y` |
|
|
244
|
+
| FR5, AR5 | keyboard, focus-out, IME, and retained-red tests | arrows, Home/End, Enter, Escape, Tab, composition | command selects or dismisses at the wrong time | `audit:BaseTypeahead/a11y` |
|
|
245
|
+
| FR6 | busy-lane tests | direct and composed pending source | duplicate/missing Spinner or stale busy state | `audit:BaseTypeahead/a11y` |
|
|
246
|
+
| FR7 | focused passthrough tests and strict lint | native-only, alias collisions, DOM/style/events | supported consumer input is dropped or owned semantics are replaced | `audit:BaseTypeahead/api` |
|
|
247
|
+
| FR8 | real-Chromium 320px sensor receipt | long result, wide request, LTR/RTL | popup or its content crosses either viewport gutter | `audit:BaseTypeahead/responsive` |
|
|
248
|
+
| FR9 | shared disabled tests plus retained-red probe | native disabled, focusable-disabled after open | disabled mode mutates query or accepts an already-open selection | `audit:BaseTypeahead/a11y` |
|
|
249
|
+
| Theming anatomy | knowledge and theming-target checks | all mapped parts | target inventory or disposition drifts | `audit:BaseTypeahead/theming` |
|
|
250
|
+
|
|
251
|
+
## Decision log
|
|
252
|
+
|
|
253
|
+
None. This draft records current or objectively remediated behavior and makes no
|
|
254
|
+
component-local API, design, compatibility, or ownership decision.
|
|
255
|
+
|
|
256
|
+
## Open questions
|
|
257
|
+
|
|
258
|
+
- **OQ1 — How should the released public props type remove package-internal
|
|
259
|
+
composition knobs such as `__queryEntries`, `isFocusableDisabled`, and
|
|
260
|
+
`inputTabIndex`?** (`human-api`)
|
|
261
|
+
- **OQ2 — Should `inputXStyle` be deprecated now that the inherited `xstyle`
|
|
262
|
+
contract correctly reaches the same input?** (`human-api`)
|
|
263
|
+
- **OQ3 — Does bare BaseTypeahead own a default focus-visible ring, or must every
|
|
264
|
+
direct caller provide the ring on its wrapper?** (`human-design`)
|
|
265
|
+
|
|
266
|
+
## Content boundary
|
|
267
|
+
|
|
268
|
+
This file does not duplicate the prop reference, examples, current audit score,
|
|
269
|
+
shared layer mechanics, or shared accessibility and theming rules.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file BaseTypeahead.test.tsx
|
|
5
|
+
* @input BaseTypeahead public props and a synchronous SearchSource
|
|
6
|
+
* @output Focused contract tests for the public combobox engine
|
|
7
|
+
* @position Colocated verification for BaseTypeahead
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest';
|
|
11
|
+
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
|
|
12
|
+
import * as stylex from '@stylexjs/stylex';
|
|
13
|
+
import {BaseTypeahead} from './BaseTypeahead';
|
|
14
|
+
import type {SearchSource, SearchableItem} from './types';
|
|
15
|
+
|
|
16
|
+
const popoverOpenState = new WeakMap<HTMLElement, boolean>();
|
|
17
|
+
const originalMatchesDescriptor = Object.getOwnPropertyDescriptor(
|
|
18
|
+
HTMLElement.prototype,
|
|
19
|
+
'matches',
|
|
20
|
+
);
|
|
21
|
+
const originalMatches = HTMLElement.prototype.matches;
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
HTMLElement.prototype.showPopover = function () {
|
|
25
|
+
popoverOpenState.set(this, true);
|
|
26
|
+
const event = new Event('toggle');
|
|
27
|
+
Object.defineProperty(event, 'newState', {value: 'open'});
|
|
28
|
+
this.dispatchEvent(event);
|
|
29
|
+
};
|
|
30
|
+
HTMLElement.prototype.hidePopover = function () {
|
|
31
|
+
popoverOpenState.set(this, false);
|
|
32
|
+
const event = new Event('toggle');
|
|
33
|
+
Object.defineProperty(event, 'newState', {value: 'closed'});
|
|
34
|
+
this.dispatchEvent(event);
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(HTMLElement.prototype, 'matches', {
|
|
37
|
+
...originalMatchesDescriptor,
|
|
38
|
+
value(this: HTMLElement, selector: string) {
|
|
39
|
+
if (selector === ':popover-open') {
|
|
40
|
+
return popoverOpenState.get(this) ?? false;
|
|
41
|
+
}
|
|
42
|
+
return originalMatches.call(this, selector);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
if (originalMatchesDescriptor) {
|
|
49
|
+
Object.defineProperty(
|
|
50
|
+
HTMLElement.prototype,
|
|
51
|
+
'matches',
|
|
52
|
+
originalMatchesDescriptor,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emptySource: SearchSource<SearchableItem> = {
|
|
58
|
+
search: () => [],
|
|
59
|
+
bootstrap: () => [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const resultItem: SearchableItem = {id: '1', label: 'Result'};
|
|
63
|
+
|
|
64
|
+
const testStyles = stylex.create({
|
|
65
|
+
input: {textTransform: 'uppercase'},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('BaseTypeahead', () => {
|
|
69
|
+
it('forwards supported DOM, styling, and event props to the combobox input', () => {
|
|
70
|
+
const onBlur = vi.fn();
|
|
71
|
+
const onFocus = vi.fn();
|
|
72
|
+
const onPointerDown = vi.fn();
|
|
73
|
+
|
|
74
|
+
render(
|
|
75
|
+
<BaseTypeahead
|
|
76
|
+
searchSource={emptySource}
|
|
77
|
+
value={null}
|
|
78
|
+
onChange={() => {}}
|
|
79
|
+
aria-label="Find a framework"
|
|
80
|
+
aria-expanded="true"
|
|
81
|
+
className="consumer-input"
|
|
82
|
+
data-audit-state="forwarded"
|
|
83
|
+
onBlur={onBlur}
|
|
84
|
+
onFocus={onFocus}
|
|
85
|
+
onPointerDown={onPointerDown}
|
|
86
|
+
style={{letterSpacing: '0.08em'}}
|
|
87
|
+
xstyle={testStyles.input}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const input = screen.getByRole('combobox', {name: 'Find a framework'});
|
|
92
|
+
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
93
|
+
expect(input).toHaveAttribute('data-audit-state', 'forwarded');
|
|
94
|
+
expect(input).toHaveClass('consumer-input');
|
|
95
|
+
expect(input).toHaveStyle({letterSpacing: '0.08em'});
|
|
96
|
+
expect(getComputedStyle(input).textTransform).toBe('uppercase');
|
|
97
|
+
|
|
98
|
+
fireEvent.pointerDown(input);
|
|
99
|
+
fireEvent.focus(input);
|
|
100
|
+
fireEvent.blur(input);
|
|
101
|
+
expect(onPointerDown).toHaveBeenCalledOnce();
|
|
102
|
+
expect(onFocus).toHaveBeenCalledOnce();
|
|
103
|
+
expect(onBlur).toHaveBeenCalledOnce();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('preserves native input attributes when legacy aliases are undefined', () => {
|
|
107
|
+
render(
|
|
108
|
+
<BaseTypeahead
|
|
109
|
+
searchSource={emptySource}
|
|
110
|
+
value={null}
|
|
111
|
+
onChange={() => {}}
|
|
112
|
+
id="native-input"
|
|
113
|
+
aria-describedby="native-description"
|
|
114
|
+
aria-labelledby="native-label"
|
|
115
|
+
tabIndex={3}
|
|
116
|
+
inputId={undefined}
|
|
117
|
+
ariaDescribedBy={undefined}
|
|
118
|
+
ariaLabelledBy={undefined}
|
|
119
|
+
inputTabIndex={undefined}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const input = screen.getByRole('combobox');
|
|
124
|
+
expect(input).toHaveAttribute('id', 'native-input');
|
|
125
|
+
expect(input).toHaveAttribute('aria-describedby', 'native-description');
|
|
126
|
+
expect(input).toHaveAttribute('aria-labelledby', 'native-label');
|
|
127
|
+
expect(input).toHaveAttribute('tabindex', '3');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('lets defined legacy aliases override their native equivalents', () => {
|
|
131
|
+
render(
|
|
132
|
+
<BaseTypeahead
|
|
133
|
+
searchSource={emptySource}
|
|
134
|
+
value={null}
|
|
135
|
+
onChange={() => {}}
|
|
136
|
+
id="native-input"
|
|
137
|
+
aria-describedby="native-description"
|
|
138
|
+
aria-labelledby="native-label"
|
|
139
|
+
tabIndex={3}
|
|
140
|
+
inputId="legacy-input"
|
|
141
|
+
ariaDescribedBy="legacy-description"
|
|
142
|
+
ariaLabelledBy="legacy-label"
|
|
143
|
+
inputTabIndex={-1}
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const input = screen.getByRole('combobox');
|
|
148
|
+
expect(input).toHaveAttribute('id', 'legacy-input');
|
|
149
|
+
expect(input).toHaveAttribute('aria-describedby', 'legacy-description');
|
|
150
|
+
expect(input).toHaveAttribute('aria-labelledby', 'legacy-label');
|
|
151
|
+
expect(input).toHaveAttribute('tabindex', '-1');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('counts grapheme clusters when enforcing minQueryLength', async () => {
|
|
155
|
+
const search = vi.fn(() => [resultItem]);
|
|
156
|
+
render(
|
|
157
|
+
<BaseTypeahead
|
|
158
|
+
searchSource={{search, bootstrap: () => []}}
|
|
159
|
+
value={null}
|
|
160
|
+
onChange={() => {}}
|
|
161
|
+
debounceMs={0}
|
|
162
|
+
minQueryLength={2}
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const input = screen.getByRole('combobox');
|
|
167
|
+
fireEvent.change(input, {target: {value: '😀'}});
|
|
168
|
+
await Promise.resolve();
|
|
169
|
+
expect(search).not.toHaveBeenCalled();
|
|
170
|
+
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
171
|
+
|
|
172
|
+
fireEvent.change(input, {target: {value: '😀a'}});
|
|
173
|
+
await waitFor(() => expect(search).toHaveBeenCalledExactlyOnceWith('😀a'));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('exposes a completed empty search as a disabled listbox option', async () => {
|
|
177
|
+
render(
|
|
178
|
+
<BaseTypeahead
|
|
179
|
+
searchSource={emptySource}
|
|
180
|
+
value={null}
|
|
181
|
+
onChange={() => {}}
|
|
182
|
+
debounceMs={0}
|
|
183
|
+
emptySearchResultsText="No matching frameworks"
|
|
184
|
+
/>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
fireEvent.change(screen.getByRole('combobox'), {
|
|
188
|
+
target: {value: 'none'},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(
|
|
193
|
+
screen.getByRole('option', {
|
|
194
|
+
hidden: true,
|
|
195
|
+
name: 'No matching frameworks',
|
|
196
|
+
}),
|
|
197
|
+
).toHaveAttribute('aria-disabled', 'true');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|