Skip to content

Commit cf2ffeb

Browse files
alexcarpenterclaude
andcommitted
feat(headless): add Autocomplete primitive and floating-tree integration test
Combobox with virtual focus (aria-activedescendant), inline List variant for embedding inside other floating elements, and full keyboard navigation. Also adds cross-primitive floating-tree integration tests exercising Dialog/Popover/Select/Tooltip nesting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1dabedd commit cf2ffeb

7 files changed

Lines changed: 2143 additions & 0 deletions

File tree

packages/headless/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"private": true,
55
"type": "module",
66
"exports": {
7+
"./autocomplete": {
8+
"import": "./dist/primitives/autocomplete/index.js",
9+
"types": "./dist/primitives/autocomplete/index.d.ts"
10+
},
711
"./menu": {
812
"import": "./dist/primitives/menu/index.js",
913
"types": "./dist/primitives/menu/index.d.ts"
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Autocomplete
2+
3+
A combobox input with a filterable dropdown list. Supports virtual focus (focus stays on the input), keyboard navigation, and controlled/uncontrolled input and selection values.
4+
5+
## When to Use
6+
7+
- Search inputs with suggestions, tag pickers, or any input that filters a list of options.
8+
- When the user needs to type to narrow down choices, unlike `Select` which is for picking from a static list.
9+
- When you need `aria-autocomplete` behavior with `aria-activedescendant` virtual focus.
10+
11+
## Usage
12+
13+
```tsx
14+
import { Autocomplete } from '@/primitives/autocomplete';
15+
16+
const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];
17+
18+
function MyAutocomplete() {
19+
const [inputValue, setInputValue] = useState('');
20+
const filtered = fruits.filter(f => f.toLowerCase().includes(inputValue.toLowerCase()));
21+
22+
return (
23+
<Autocomplete
24+
inputValue={inputValue}
25+
onInputValueChange={setInputValue}
26+
>
27+
<Autocomplete.Input placeholder='Search fruits...' />
28+
<Autocomplete.Positioner>
29+
<Autocomplete.Popup>
30+
{filtered.map(fruit => (
31+
<Autocomplete.Option
32+
key={fruit}
33+
value={fruit}
34+
label={fruit}
35+
/>
36+
))}
37+
</Autocomplete.Popup>
38+
</Autocomplete.Positioner>
39+
</Autocomplete>
40+
);
41+
}
42+
```
43+
44+
### Inline List (inside another floating element)
45+
46+
Use `Autocomplete.List` when the autocomplete input lives inside an outer floating surface such as a Popover or Dialog. In this mode, the outer primitive owns placement and dismissal for the overall panel, while `Autocomplete` still owns the combobox/listbox semantics between the input and the results list.
47+
48+
```tsx
49+
<Popover>
50+
<Popover.Trigger>Pick a country</Popover.Trigger>
51+
<Popover.Positioner>
52+
<Popover.Popup>
53+
<Autocomplete open>
54+
<Autocomplete.Input
55+
placeholder='Search countries...'
56+
autoFocus
57+
/>
58+
<Autocomplete.List>
59+
<Autocomplete.Option
60+
value='us'
61+
label='United States'
62+
/>
63+
</Autocomplete.List>
64+
</Autocomplete>
65+
</Popover.Popup>
66+
</Popover.Positioner>
67+
</Popover>
68+
```
69+
70+
In this pattern, keep the outer `Popover` or `Dialog` as the source of truth for whether the panel is visible. `Autocomplete` should render the input and inline listbox inside that surface, and selecting an option can close the outer shell if desired.
71+
72+
## Parts
73+
74+
| Part | Default Element | Description |
75+
| ------------------------- | --------------- | ---------------------------------------- |
76+
| `Autocomplete` || Root context provider |
77+
| `Autocomplete.Input` | `<input>` | Text input that drives filtering |
78+
| `Autocomplete.Portal` || Portals children (accepts `root` prop) |
79+
| `Autocomplete.Positioner` | `<div>` | Floating positioned container |
80+
| `Autocomplete.Popup` | `<div>` | Visual wrapper for the option list |
81+
| `Autocomplete.List` | `<div>` | Inline alternative to Positioner + Popup |
82+
| `Autocomplete.Option` | `<div>` | A selectable option |
83+
| `Autocomplete.Arrow` | `<svg>` | Optional floating arrow |
84+
85+
## Props
86+
87+
### `Autocomplete` (root)
88+
89+
| Prop | Type | Default | Description |
90+
| -------------------- | ------------------------- | ---------------- | ------------------------------------- |
91+
| `inputValue` | `string` || Controlled input text |
92+
| `defaultInputValue` | `string` | `""` | Initial input text (uncontrolled) |
93+
| `onInputValueChange` | `(value: string) => void` || Called when input text changes |
94+
| `value` | `string` || Controlled selected value |
95+
| `defaultValue` | `string` || Initial selected value (uncontrolled) |
96+
| `onValueChange` | `(value: string) => void` || Called when an option is selected |
97+
| `open` | `boolean` || Controlled open state |
98+
| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
99+
| `onOpenChange` | `(open: boolean) => void` || Called when open state changes |
100+
| `placement` | `Placement` | `"bottom-start"` | Floating UI placement |
101+
| `sideOffset` | `number` | `4` | Gap between input and popup (px) |
102+
103+
### `Autocomplete.Option`
104+
105+
| Prop | Type | Default | Description |
106+
| ---------- | --------- | --------------------- | ---------------------------------------------------- |
107+
| `value` | `string` | **required** | The option's value |
108+
| `label` | `string` | falls back to `value` | Display label, also used for input text on selection |
109+
| `disabled` | `boolean` || Prevents selection |
110+
111+
### `Autocomplete.Input`, `Autocomplete.Positioner`, `Autocomplete.Popup`, `Autocomplete.List`
112+
113+
No additional props beyond standard HTML attributes and the `render` prop.
114+
115+
### `Autocomplete.Arrow`
116+
117+
Accepts all `FloatingArrow` props. `ref` and `context` are injected automatically.
118+
119+
## Keyboard Navigation
120+
121+
| Key | Action |
122+
| ----------- | ------------------------------------- |
123+
| `ArrowDown` | Move to next option |
124+
| `ArrowUp` | Move to previous option |
125+
| `Enter` | Select the active option, close popup |
126+
| `Escape` | Close the popup |
127+
128+
Navigation loops and auto-scrolls the active option into view.
129+
130+
## Data Attributes
131+
132+
| Attribute | Applies To | Description |
133+
| --------------------------------- | ----------------- | --------------------------------------------- |
134+
| `data-cl-slot` | All parts | Part identifier (e.g. `"autocomplete-input"`) |
135+
| `data-cl-open` / `data-cl-closed` | Input | Popup open state |
136+
| `data-cl-selected` | Option | The currently selected option |
137+
| `data-cl-active` | Option | The keyboard-highlighted option |
138+
| `data-cl-disabled` | Option | Disabled option |
139+
| `data-cl-side` | Positioner, Arrow | Resolved placement side |
140+
141+
## Open/Close Behavior
142+
143+
- Typing a non-empty string opens the popup automatically.
144+
- Clearing the input closes the popup.
145+
- Clicking an option closes the popup and returns focus to the input.
146+
- Outside click and Escape close the popup.
147+
148+
## ARIA
149+
150+
- Input: `aria-autocomplete="list"`, `aria-activedescendant` (virtual focus)
151+
- Options: `role="option"`, `aria-selected`, `aria-disabled`
152+
- Focus manager: non-modal, `initialFocus={-1}` (focus stays on input)

0 commit comments

Comments
 (0)