Skip to content

Commit acf2ef2

Browse files
Combobox refactor (#838)
* fix: select up arrow key highlights last item * add listbox research * docs: push up latest combobox research * add more research * lockfile changes * fix dumb pnpm issue * feat: initial combobox scaffolding * add example styles * take from select tests, initial tests passing * feat: close combobox listbox on click * feat: combobox inline comp * setup refs * feat: add combobox selecting with mouse * feat: select items input val in combobox * feat: add combobox group labels * feat: add initial combobox keyboard interactions * feat: dismisses listbox * tests: more combobox tests * feat: add selecting options via keyboard * feat: a11y navigation and removing excess select code * feat: popover code and item indicator * feat: looped tests * feat: scroll behavior * label the options * tests: disabled * feat: placeholder * remove nx workspace * feat: onChange$ and onOpenChange$ * test: updated combobox tests * reactive values and scrolling * test: combobox bind tests * a11y: axe validation tests * a11y: major select a11y improvements * feat: combobox groups * test: new combobox a11y tests * small code improvements * feat: initial multiple support * test: all tests passing * fix build * tests: new combobox filter tests * feat: initial filtering * working navigation * fix: combobox css styles * feat: listbox closes when no options are present * test: finish initial filtered options tests * feat: custom filters * fix: filter tests * fix: sync open signal and flaky test * add in onInput$ * docs: initial * initial docs * deprecate listbox * docs(headless/combobox/examples): remove deprecated .Listbox * middle docs * add placeholder * onInput$ and filter docs * docs API * fix(styled combobox): pass children instead of Slot * docs(styled/combobox): update component code * temp fix(contributing): workaround the toc continuous heading bug * add new toggle tests * fix: toggle in single selection mode * automatic empty component * more docs * fix mdx structure * initial merged ref * the ability to pass in refs to any component * feat: reactive-input * add form support * feat: combobox form validation * combobox description + fix validation * refactor: deprecate select listbox * refactored combined refs * fix: grab popover ref * remove refs in hero example * better filtering API, but bugs * correct initial load sig * get correct items map * fix: CSR * make sure navigation is working * update custom filter * fix: initial value and controlled value * fix: initial values * fix: scrollbar issues * fix: use debounce for scrolling * fix: navigation bug between mouse and keyboard * feat: multiple backspace * fix: multiple items reset the input * update multiple example * changelog updates * fix: styled select --------- Co-authored-by: maiieul <[email protected]>
1 parent 69785a8 commit acf2ef2

File tree

137 files changed

+6812
-4885
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+6812
-4885
lines changed

.changeset/stale-mails-do.md

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
---
2+
'@qwik-ui/headless': minor
3+
---
4+
5+
# Combobox v2, New Dropdown Component, and Progress bar reaches beta!
6+
7+
0.5 continues our move towards a 1.0 release. It includes a few breaking changes to the Combobox in order to make sure that the components have a clear API.
8+
9+
Below is a migration guide of API's for the Combobox.
10+
11+
## Combobox
12+
13+
The combobox has been refactored from the ground up, including new features, components, and QOL updates.
14+
15+
### Anatomy changes
16+
17+
The new Combobox anatomy is as follows:
18+
19+
```tsx
20+
import { component$ } from '@builder.io/qwik';
21+
import { Combobox } from '@qwik-ui/headless';
22+
import { LuCheck } from '@qwikest/icons/lucide';
23+
24+
export default component$(() => {
25+
return (
26+
<Combobox.Root>
27+
<Combobox.Label>label</Combobox.Label>
28+
29+
<Combobox.Control>
30+
<Combobox.Input />
31+
<Combobox.Trigger>trigger</Combobox.Trigger>
32+
</Combobox.Control>
33+
34+
<Combobox.Popover>
35+
<Combobox.Item>
36+
<Combobox.ItemLabel>item label</Combobox.ItemLabel>
37+
<Combobox.ItemIndicator>
38+
<LuCheck />
39+
</Combobox.ItemIndicator>
40+
</Combobox.Item>
41+
</Combobox.Popover>
42+
</Combobox.Root>
43+
);
44+
});
45+
```
46+
47+
### Anatomy Changes
48+
49+
1. **Combobox.Option** has been renamed to **Combobox.Item**:
50+
51+
- The item is no longer restricted to a string value; any UI can be placed inside the item.
52+
- Use the `Combobox.ItemLabel` component to display the item's label, which becomes the item's value if no `value` prop is passed to the `Combobox.Item`. (required)
53+
54+
2. **Combobox.Listbox** has been deprecated.
55+
56+
3. **Combobox.ItemLabel** has been added:
57+
58+
- Move the string value that was once inside `Combobox.Option` into `Combobox.ItemLabel`. (required)
59+
60+
4. **Combobox.ItemIndicator** has been added:
61+
62+
- This component is used to render UI based on the selected state of the item. (optional)
63+
64+
5. **Combobox.Description** has been added:
65+
66+
- The text rendered inside the description component is displayed to screen readers as an accessible description of the combobox. (optional)
67+
68+
6. **Combobox.ErrorMessage** has been added:
69+
70+
- When this component is rendered, the Combobox will be marked as invalid. (optional)
71+
72+
7. **Combobox.HiddenNativeSelect** has been added:
73+
74+
- A native select element allows the submission of forms with the combobox. This component is visually hidden and hidden from screen readers. (optional)
75+
76+
8. **Combobox.Group** has been added:
77+
78+
- Used to visually group related items together. (optional)
79+
80+
9. **Combobox.GroupLabel** has been added:
81+
82+
- Provides an accessible name for the group. (optional)
83+
84+
10. **Combobox.Empty** has been added:
85+
- Displays a message when there are no items to display.
86+
- Previously, an empty popup was displayed when the combobox was empty. The new default behavior is to close the popup unless the `Combobox.Empty` component is rendered. (optional)
87+
88+
### API Changes
89+
90+
#### Rendering Items (required)
91+
92+
The `optionRenderer$` prop on the `Combobox.Listbox` component has been deprecated.
93+
94+
Instead:
95+
96+
1. pass a `<Combobox.Item />` as a child of the `<Combobox.Popover />` component.
97+
2. pass a `Combobox.ItemLabel` as a child of the `<Combobox.Item />` component.
98+
99+
It should look something like this:
100+
101+
```tsx
102+
<Combobox.Popover>
103+
<Combobox.Item>
104+
<Combobox.ItemLabel>item label</Combobox.ItemLabel>
105+
{/* other content */}
106+
</Combobox.Item>
107+
</Combobox.Popover>
108+
```
109+
110+
You are now in full control of how the item is rendered. Because you control the rendering of the item, there is no need for the previous API's including the data's key values.
111+
112+
> `optionDisabledKey`, `optionValueKey`, and `optionLabelKey` props have been removed.
113+
114+
There is also no need to pass an `index` prop to the `Combobox.Item` component. It is handled automatically.
115+
116+
#### Pass in distinct values
117+
118+
The `value` prop has been added to the `Combobox.Item` component to allow for passing in a distinct value for the combobox.
119+
120+
For example, identifying a user by their ID, rather than the display username.
121+
122+
#### Add your own filter
123+
124+
Filters are an important part of the combobox. It was a design decision in this refactor to make filtering data as easy as possible to integrate with other tools and libraries.
125+
126+
The `filter$` prop has been replaced. Instead, items are by default filtered by the `includes` function.
127+
128+
To opt-out of the default filter, add the `filter={false}` prop to the `Combobox.Root` component, which will disable the default filter.
129+
130+
```tsx
131+
import { component$, useSignal, useStyles$, useTask$ } from '@builder.io/qwik';
132+
import { Combobox } from '@qwik-ui/headless';
133+
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
134+
import { matchSorter } from 'match-sorter';
135+
136+
export default component$(() => {
137+
useStyles$(styles);
138+
139+
const inputValue = useSignal('');
140+
const filteredItems = useSignal<string[]>([]);
141+
142+
const fruits = [
143+
'Apple',
144+
'Apricot',
145+
'Bilberry',
146+
'Blackberry',
147+
'Blackcurrant',
148+
'Currant',
149+
'Cherry',
150+
'Coconut',
151+
];
152+
153+
useTask$(({ track }) => {
154+
track(() => inputValue.value);
155+
156+
filteredItems.value = matchSorter(fruits, inputValue.value);
157+
});
158+
159+
return (
160+
<Combobox.Root filter={false}>
161+
<Combobox.Label>Fruits</Combobox.Label>
162+
<Combobox.Control>
163+
<Combobox.Input bind:value={inputValue} />
164+
<Combobox.Trigger>
165+
<LuChevronDown />
166+
</Combobox.Trigger>
167+
</Combobox.Control>
168+
<Combobox.Popover gutter={8}>
169+
{filteredItems.value.map((fruit) => (
170+
<Combobox.Item key={fruit}>
171+
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
172+
<Combobox.ItemIndicator>
173+
<LuCheck />
174+
</Combobox.ItemIndicator>
175+
</Combobox.Item>
176+
))}
177+
</Combobox.Popover>
178+
</Combobox.Root>
179+
);
180+
});
181+
```
182+
183+
The above example uses the `matchSorter` function from the `match-sorter` library to filter the items.
184+
185+
#### `bind:value` instead of `bind:selectedIndex`
186+
187+
bind:value has been added in favor of what was previously used to reactively control the combobox, bind:selectedIndex.
188+
189+
> This change was needed to create a more consistent API across components, but also keeping the state in the case of custom filters.
190+
191+
`onChange$` has been added to the `Combobox.Root` component so that you can listen to when the selected value changes.
192+
193+
#### Add initial values to the combobox
194+
195+
The `value` prop has been added to the `Combobox.Root` component to select the initial value of the combobox when it is rendered.
196+
197+
> `defaultLabel` has been removed, as it does not reflect the selected state of the combobox.
198+
199+
#### Input state management
200+
201+
`bind:inputValue` (on the Root) has been replaced by using the `bind:value` prop on the `<Combobox.Input />` component instead.
202+
203+
You can also now listen to when the input value changes by using the `onInput$` prop on the `<Combobox.Root />` component.
204+
205+
#### Passing refs to the combobox
206+
207+
The combobox is the first component to support passing refs! You can now pass a ref of your own to any component inside the combobox.
208+
209+
```tsx
210+
const inputRef = useSignal<HTMLInputElement>();
211+
212+
<Combobox.Input ref={inputRef} />
213+
<button onClick$={() => (inputRef.value?.focus())}>Focus input</button>
214+
```
215+
216+
#### Multiple selections
217+
218+
You can now select multiple items by passing the `multiple` prop to the `<Combobox.Root />` component.
219+
220+
#### removeOnBackspace
221+
222+
When in multiple selection mode, and the `removeOnBackspace` prop has been added to the `Combobox.Root` component, selected items can be removed by pressing the backspace key. (when the input is empty)
223+
224+
#### Managing display values
225+
226+
`bind:displayValue` has been added to the `Combobox.Root` component to allow for grabbing the updated display values of the combobox.
227+
228+
> This allows for full control over each display item. For example, a couple of display values shown as pills.
229+
230+
#### Item indicators
231+
232+
The item indicator shows when the item is selected. Inside can be the UI of choice.
233+
234+
#### `bind:open` instead of `bind:isListboxOpen`
235+
236+
bind:open has been added to control the open state of the listbox, replacing bind:isListboxOpen.
237+
238+
`onOpenChange$` has been added to the `Combobox.Root` component so that you can listen to when the popup opens or closes.
239+
240+
#### Focus State Management
241+
242+
bind:isInputFocused has been deprecated. Instead, if you decide to manage focus state using event handlers like onFocus$ and onBlur$. OR pass a ref to the `Combobox.Input` component.
243+
244+
#### Placeholders
245+
246+
The placeholder prop has been added to the `Combobox.Root` component to allow for a custom placeholder.
247+
248+
#### Environment
249+
250+
Like many of the latest components in Qwik UI, each function of the Combobox has been optimized to run in both SSR or CSR automatically depending on the environment.
251+
252+
#### Looping
253+
254+
Looping is now opt-in by default. To enable looping, add the `loop` prop to the `Combobox.Root` component.
255+
256+
#### Scrolling
257+
258+
When a scrollbar is present, the combobox will now properly scroll to the selected item. The scroll behavior can be customized using the `scrollOptions` prop on the `Combobox.Root` component.
259+
260+
#### Forms
261+
262+
The Combobox now supports form submissions. To enable this:
263+
264+
1. Add the `name` prop to the `Combobox.Root` component, with the name of the Combobox form field.
265+
266+
2. Add the `<Combobox.HiddenNativeSelect />` component inside of the `<Combobox.Root />` component.
267+
268+
#### Validation
269+
270+
The Combobox now supports validation. It was a design decision to make validation as easy as possible to integrate with other tools and libraries, such as Modular Forms.
271+
272+
A component is invalid when the `Combobox.ErrorMessage` component is rendered. This component provides an accessible description of the error to assistive technologies.
273+
274+
### Floating / Top layer items
275+
276+
The props previously on the `Combobox.Listbox`, have been moved to the `Combobox.Popover` component to be more consistent with the rest of the Qwik UI components.
277+
278+
`placement` has been deprecated in favor of `floating`, which can be a boolean or the direction of the floating element.
279+
280+
`autoPlacement` has been removed, as `flip` should be used instead.
281+
282+
Ex: `floating={true}` or `floating="top"`
283+
284+
### Keyboard interactions
285+
286+
Key
287+
Description
288+
289+
| Key | Description |
290+
| --------- | ---------------------------------------------------- |
291+
| Enter | Selects a highlighted item when open. |
292+
| ArrowDown | Opens the combobox or moves focus down. |
293+
| ArrowUp | Opens the combobox or moves focus up. |
294+
| Home | When focus is on an item, moves focus to first item. |
295+
| End | When focus is on an item, moves focus to last item. |
296+
| Esc | Closes the combobox and moves focus to the trigger. |
297+
| Tab | Moves focus to the next focusable element. |
298+
299+
The Enter key will toggle the selection of the highlighted item without closing the combobox if an item is already selected, otherwise it will close the popup.
300+
301+
### Multi Select
302+
303+
When in multi select mode, additional keyboard interactions are available.
304+
305+
| Key | Description |
306+
| ----- | --------------------------------------------------------------------------- |
307+
| Enter | Toggles the selection of the highlighted item without closing the combobox. |
308+
309+
### Data Attributes
310+
311+
- `data-invalid` is added to the combobox when the combobox is invalid.
312+
- `data-open` is added to the combobox when the combobox is open.
313+
- `data-closed` is added to the combobox when the combobox is closed.
314+
- `data-highlighted` is added to the combobox item when the item is highlighted.
315+
- `data-selected` is added to the combobox item when the item is selected.
316+
- `data-disabled` is added to the combobox item when the item is disabled.
317+
318+
### Accessibility
319+
320+
Announcements to the Combobox are more consistent and follow the [WAI-ARIA Combobox design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
321+
322+
So far, the Combobox has been tested with VoiceOver, Axe, and NVDA.
323+
324+
## Select
325+
326+
The select component also includes some improvments
327+
328+
### Accessibility
329+
330+
Announcements to the Select are more consistent and follow the [WAI-ARIA Listbox design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
331+
332+
So far, the Select has been tested with VoiceOver, Axe, and NVDA.
333+
334+
## Dropdown
335+
336+
A new component has been added to Qwik UI, the Dropdown. It is currently in a draft state, and is not yet ready for production use. We will be working on it more deeply in the near future.
337+
338+
### Anatomy
339+
340+
Here is the initial API:
341+
342+
```tsx
343+
import { component$ } from '@builder.io/qwik';
344+
import { Dropdown } from '@qwik-ui/headless';
345+
export default component$(() => {
346+
return (
347+
<Dropdown.Root>
348+
<Dropdown.Trigger>
349+
Open Dropdown
350+
</Dropdown.Trigger>
351+
<Dropdown.Popover>
352+
<Dropdown.Arrow />
353+
<Dropdown.Content>
354+
<Dropdown.Group>
355+
<Dropdown.GroupLabel>
356+
Group 1
357+
</Dropdown.GroupLabel>
358+
</Dropdown.Group>
359+
<Dropdown.Separator />
360+
</Dropdown.Content>
361+
</Dropdown.Popover>
362+
</Dropdown.Root>
363+
```
364+
365+
Feel free to play around with it! Feedback is very appreciated.
366+
367+
## Progress Bar
368+
369+
The progress bar has been around for a while, it has finally reached a **beta state**, make sure to open an issue on the [Qwik UI repo](https://github.com/qwikifiers/qwik-ui/issues) if you run into any problems.

apps/website/adapters/cloudflare-pages/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default extendConfig(baseConfig, () => {
1414
cloudflarePagesAdapter({
1515
ssg: {
1616
include: ['/*'],
17-
origin: 'https://qwikui.com'
17+
origin: 'https://qwikui.com',
1818
},
1919
}),
2020
],

0 commit comments

Comments
 (0)