Skip to content

Commit a47e524

Browse files
thejacksheltonwmertens
authored andcommitted
feat(combobox): filter API, major refactor, type changes, docs additions
1 parent 093833e commit a47e524

File tree

16 files changed

+307
-211
lines changed

16 files changed

+307
-211
lines changed

apps/website/src/routes/docs/_components/status-banner/status-banner.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
component$,
44
useSignal,
55
useStylesScoped$,
6-
$,
76
} from '@builder.io/qwik';
87
import { ComponentStatus } from 'apps/website/src/_state/component-status.type';
98
import { getClassByStatus } from '../component-status-badge/component-status-badge';
@@ -86,7 +85,7 @@ export const StatusBanner = component$((props: StatusBannerProps) => {
8685
animation: fadeOut 0.5s ease forwards;
8786
margin-top: var(--dynamic-banner-height);
8887
}
89-
88+
9089
@keyframes fadeOut {
9190
from {
9291
opacity: 1;
@@ -103,7 +102,7 @@ export const StatusBanner = component$((props: StatusBannerProps) => {
103102
ref={ref}
104103
hidden={isBannerClosedSig.value}
105104
onAnimationEnd$={() => (isBannerClosedSig.value = true)}
106-
class={`${getBackgroundByStatus(props.status)} px-6 py-4
105+
class={`${getBackgroundByStatus(props.status)} px-6 py-4
107106
rounded-xl md:items-center relative md:flex-row normal-state
108107
shadow-depth dark:shadow-depth-dark`}
109108
style={{ marginBottom: `${marginBottom}px` }}

apps/website/src/routes/docs/headless/(components)/accordion/index.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ Qwik UI's Accordion implementation follows the [WAI-Aria](https://www.w3.org/WAI
9595

9696
##### ✨ Features
9797

98-
- Full keyboard navigation.
99-
- Single or Multi Accordion.
100-
- Controlled or uncontrolled.
101-
- Animatable, dynamic, and resumable.
98+
- Full keyboard navigation
99+
- Single or Multi Accordion
100+
- Controlled or uncontrolled
101+
- Animatable, dynamic, and resumable
102102

103103
<div class="mb-6 flex flex-col gap-2">
104104
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/accordion)

apps/website/src/routes/docs/headless/(components)/combobox/examples.tsx

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import {
55
ComboboxInput,
66
ComboboxLabel,
77
ComboboxListbox,
8-
ComboboxOption,
98
ComboboxPortal,
109
ComboboxTrigger,
1110
} from '@qwik-ui/headless';
1211

12+
import { ComboboxOption } from '../../../../../../../../packages/kit-headless/src/components/combobox';
13+
1314
import { PreviewCodeExample } from '../../../_components/preview-code-example/preview-code-example';
1415

1516
const stringsExample = [
@@ -45,36 +46,20 @@ const objectExample: Array<Trainer> = [
4546
];
4647

4748
export const HeroExample = component$(() => {
48-
const stringsExampleSig = useSignal(stringsExample);
4949
const objectExampleSig = useSignal(objectExample);
50-
const isComboboxVisibleSig = useSignal(true);
51-
52-
const onInputChange$ = $((value: string) => {
53-
objectExampleSig.value = objectExample.filter((option) => {
54-
return option.testLabel.toLowerCase().includes(value.toLowerCase());
55-
});
56-
});
5750

5851
return (
59-
<PreviewCodeExample>
60-
<div class="flex flex-col gap-4" q:slot="actualComponent">
61-
<button
62-
onClick$={() => {
63-
isComboboxVisibleSig.value = !isComboboxVisibleSig.value;
64-
}}
65-
>
66-
Toggle Client Side
67-
</button>
68-
{isComboboxVisibleSig.value && (
52+
<>
53+
<PreviewCodeExample>
54+
<div class="flex flex-col gap-4" q:slot="actualComponent">
6955
<Combobox
70-
options={objectExampleSig}
71-
defaultLabel="Randy"
72-
onInputChange$={onInputChange$}
56+
options={objectExampleSig.value}
7357
optionValueKey="testValue"
7458
optionLabelKey="testLabel"
7559
optionDisabledKey="disabled"
76-
optionComponent$={$((option: Trainer, index: number) => (
60+
optionComponent$={(option: Trainer, key: number, index: number) => (
7761
<ComboboxOption
62+
key={key}
7863
index={index}
7964
option={option}
8065
style={option.disabled ? { color: 'gray' } : {}}
@@ -84,7 +69,7 @@ export const HeroExample = component$(() => {
8469
{option.testLabel}
8570
</span>
8671
</ComboboxOption>
87-
))}
72+
)}
8873
class="relative"
8974
>
9075
<ComboboxLabel class=" font-semibold dark:text-white text-[#333333]">
@@ -113,13 +98,13 @@ export const HeroExample = component$(() => {
11398
<ComboboxListbox class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]" />
11499
</ComboboxPortal>
115100
</Combobox>
116-
)}
117-
</div>
101+
</div>
118102

119-
<div q:slot="codeExample">
120-
<Slot />
121-
</div>
122-
</PreviewCodeExample>
103+
<div q:slot="codeExample">
104+
<Slot />
105+
</div>
106+
</PreviewCodeExample>
107+
</>
123108
);
124109
});
125110

@@ -143,19 +128,17 @@ export const StringCombobox = component$(() => {
143128

144129
const fruitsSig = useSignal(fruits);
145130

146-
const onInputChange$ = $((value: string) => {
147-
fruitsSig.value = fruits.filter((option) => {
148-
return option.toLowerCase().includes(value.toLowerCase());
149-
});
150-
});
151-
152131
return (
153132
<PreviewCodeExample>
154133
<div class="flex flex-col gap-4" q:slot="actualComponent">
155134
<Combobox
156-
options={fruitsSig}
135+
options={fruitsSig.value}
157136
defaultLabel="Currant"
158-
onInputChange$={onInputChange$}
137+
filter$={(value: string, options) =>
138+
options.filter(({ option }) => {
139+
return option.toLowerCase().startsWith(value.toLowerCase());
140+
})
141+
}
159142
optionComponent$={$((option: string, index: number) => (
160143
<ComboboxOption
161144
class="rounded-sm px-2 hover:bg-[#496080] aria-selected:bg-[#496080] border-2 border-transparent aria-selected:border-[#abbbce] group"
@@ -201,6 +184,71 @@ export const StringCombobox = component$(() => {
201184
);
202185
});
203186

204-
export const Example03 = component$(() => {
205-
return <PreviewCodeExample></PreviewCodeExample>;
187+
// Using context example.
188+
189+
import { createContextId, useContextProvider, useContext } from '@builder.io/qwik';
190+
191+
// Create a context ID
192+
export const AnimalContext = createContextId<string[]>('animal-context');
193+
194+
export const ParentComponent = component$(() => {
195+
const animals = ['Armadillo', 'Donkey', 'Baboon', 'Badger', 'Barracuda', 'Bat', 'Bear'];
196+
// Provide the animals array to the context under the context ID
197+
useContextProvider(AnimalContext, animals);
198+
199+
return <ContextExample />;
200+
});
201+
202+
export const ContextExample = component$(() => {
203+
const animals = useContext(AnimalContext);
204+
const animalsSig = useSignal(animals);
205+
206+
return (
207+
<PreviewCodeExample>
208+
<div class="flex flex-col gap-4" q:slot="actualComponent">
209+
<Combobox
210+
options={animalsSig.value}
211+
optionComponent$={$((option: string, index: number) => (
212+
<ComboboxOption
213+
index={index}
214+
option={option}
215+
class="rounded-sm px-2 hover:bg-[#496080] aria-selected:bg-[#496080] border-2 border-transparent aria-selected:border-[#abbbce] group"
216+
>
217+
<span class="block group-aria-selected:translate-x-[3px] transition-transform duration-350">
218+
{option}
219+
</span>
220+
</ComboboxOption>
221+
))}
222+
class="relative"
223+
>
224+
<ComboboxLabel class="font-semibold dark:text-white text-[#333333]">
225+
Animals 🐖
226+
</ComboboxLabel>
227+
<ComboboxControl class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
228+
<ComboboxInput class="px-2 w-44 bg-inherit px-d2 pr-6 text-white" />
229+
<ComboboxTrigger class="w-6 h-6 group absolute right-0">
230+
<svg
231+
xmlns="http://www.w3.org/2000/svg"
232+
viewBox="0 0 24 24"
233+
fill="none"
234+
stroke-width="2"
235+
class="stroke-white group-aria-expanded:-rotate-180 transition-transform duration-[450ms]"
236+
stroke-linecap="round"
237+
stroke-linejoin="round"
238+
>
239+
<polyline points="6 9 12 15 18 9"></polyline>
240+
</svg>
241+
</ComboboxTrigger>
242+
</ComboboxControl>
243+
<ComboboxPortal>
244+
<ComboboxListbox class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]" />
245+
</ComboboxPortal>
246+
</Combobox>
247+
</div>
248+
249+
<div q:slot="codeExample">
250+
<Slot />
251+
</div>
252+
</PreviewCodeExample>
253+
);
206254
});

apps/website/src/routes/docs/headless/(components)/combobox/index.mdx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
ComboboxOption,
1212
} from '@qwik-ui/headless';
1313

14-
import { HeroExample, StringCombobox, Example03 } from './examples';
14+
import { HeroExample, ParentComponent, StringCombobox, Example03 } from './examples';
1515
import { CodeExample } from '../../../_components/code-example/code-example';
1616
import { KeyboardInteractionTable } from '../../../_components/keyboard-interaction-table/keyboard-interaction-table';
1717
import { APITable } from '../../../_components/api-table/api-table';
@@ -65,7 +65,7 @@ import {statusByComponent} from '../../../../../_state/component-statuses';
6565
```
6666
</HeroExample>
6767

68-
<div class="bg-[#9A3412] p-4 rounded-sm border-l-[3px] dark:border-[#F3F3F3] border-[#333333]">
68+
<div class="bg-red-200 dark:bg-red-900 p-4 rounded-sm border-l-[3px] dark:border-[#F3F3F3] border-[#333333]">
6969
<p class="pb-4">The Combobox component makes use of **portals**. A basic use case for a portal is to prevent overflow issues in your UI. To support portals in Qwik UI, please add the following around your layout.tsx.</p>
7070
<CodeExample classes="!mb-0 !p-6 !rounded-md">
7171
```tsx
@@ -76,16 +76,37 @@ import {statusByComponent} from '../../../../../_state/component-statuses';
7676
</body>
7777
```
7878
</CodeExample>
79+
80+
<p class="pt-4">Portals are still currently in **Beta**, as a result, if you'd like to use context within these components, you must pass your context ID using the `contextIds` prop on the `ComboboxPortal` component. If you're not using context, you don't need to do this step.</p>
81+
<CodeExample classes="!mb-0 !p-6 !rounded-md">
82+
```tsx
83+
<ComboboxPortal contextIds={["my-context-id", "my-context-id-2"]}>
84+
<ComboboxListbox>
85+
<ComboboxOption>Option</ComboboxOption>
86+
</ComboboxListbox>
87+
</ComboboxPortal>
88+
```
89+
</CodeExample>
7990
</div>
8091

92+
<br/>
93+
8194
> In Beta, we're currently supporting the **Autocomplete** configuration. We aim to support other combobox configurations according to our [roadmap](https://github.com/qwikifiers/qwik-ui/issues/405)
8295
96+
<br/>
97+
8398
##### ✨ Features
8499

85-
- Full keyboard navigation.
86-
- Single or Multi Accordion.
87-
- Controlled or uncontrolled.
88-
- Animatable, dynamic, and resumable.
100+
- Full WAI-Aria compliance
101+
- Full keyboard navigation
102+
- Autocomplete configuration
103+
- Can add your own custom filter
104+
- Controlled or uncontrolled
105+
- Supports disabled options
106+
- Animatable, dynamic, and resumable
107+
108+
<br/>
109+
89110

90111
Qwik UI's Combobox implementation follows the [WAI-Aria](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) design pattern, along with some additional API's that enhance the flexibility, types, and performance.
91112

@@ -127,9 +148,10 @@ Qwik UI's Combobox implementation follows the [WAI-Aria](https://www.w3.org/WAI/
127148

128149
</CodeExample>
129150

130-
## Examples
151+
## Passing context
152+
153+
<ParentComponent>```tsx ```</ParentComponent>
131154

132-
### EXAMPLE: Frequently Asked Questions
133155

134156
<StringCombobox>```tsx ```</StringCombobox>
135157

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createContextId } from '@builder.io/qwik';
22
import { ComboboxContext } from './combobox-context.type';
33

4-
const ComboboxContextId = createContextId<ComboboxContext>('combobox');
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
const ComboboxContextId = createContextId<ComboboxContext<any>>('combobox');
56

67
export default ComboboxContextId;

packages/kit-headless/src/components/combobox/combobox-context.type.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { JSXNode, QRL, Signal } from '@builder.io/qwik';
1+
import { QRL, Signal } from '@builder.io/qwik';
2+
import { JSX } from '@builder.io/qwik/jsx-runtime';
23

3-
export interface ComboboxContext {
4+
export interface ComboboxContext<O extends Option = Option> {
45
// user's source of truth
5-
options: Signal<Array<string | Record<string, any>>>;
6-
optionComponent$?: QRL<(option: any, index: number) => JSXNode>;
6+
optionsSig: Signal<{ option: O; key: number }[]>;
7+
optionComponent$?: QRL<(option: O, key: number, filteredIndex: number) => JSX.Element>;
78

89
// element state
10+
inputValueSig: Signal<string>;
911
localId: string;
1012
labelRef: Signal<HTMLLabelElement | undefined>;
1113
listboxRef: Signal<HTMLUListElement | undefined>;
@@ -24,11 +26,10 @@ export interface ComboboxContext {
2426
selectedOptionIndexSig: Signal<number>;
2527

2628
// option settings
27-
onInputChange$?: QRL<(value: string) => void>;
28-
optionValueKey?: string;
29-
optionLabelKey?: string;
30-
optionDisabledKey?: string;
29+
optionValueKey: string;
30+
optionLabelKey: string;
31+
optionDisabledKey: string;
3132
}
3233

3334
// Whether it is a string or an object we want to be able to access the value
34-
export type Option = ComboboxContext['options']['value'][number];
35+
export type Option = string | Record<string, unknown>;

0 commit comments

Comments
 (0)