Skip to content

Commit 185ce42

Browse files
committed
refactor(combobox): resolve options at start
1 parent 43cb309 commit 185ce42

File tree

11 files changed

+510
-447
lines changed

11 files changed

+510
-447
lines changed

apps/website/src/routes/_components/router-head/router-head.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ export const RouterHead = component$(() => {
4545
<meta name="twitter:site" content="@QwikDev" />
4646
<meta name="twitter:title" content="Qwik" />
4747

48-
{head.meta.map((m) => (
49-
<meta {...m} />
48+
{head.meta.map((m, i) => (
49+
<meta key={i} {...m} />
5050
))}
5151

52-
{head.links.map((l) => (
53-
<link {...l} />
52+
{head.links.map((l, i) => (
53+
<link key={i} {...l} />
5454
))}
5555

56-
{head.styles.map((s) => (
57-
<style {...s.props} dangerouslySetInnerHTML={s.style} />
56+
{head.styles.map((s, i) => (
57+
<style key={i} {...s.props} dangerouslySetInnerHTML={s.style} />
5858
))}
5959

6060
<CSSThemeScript />

apps/website/src/routes/docs.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
}
4444

4545
blockquote {
46-
@apply bg-[#BFDBFE] dark:bg-[#312E81] p-4 border-l-[3px] dark:border-[#F3F3F3] border-[#333333] rounded-sm my-4;
46+
@apply bg-[#BFDBFE] dark:bg-[#312E81] p-4 lg:p-6 dark:border-[#F3F3F3] border-[#333333] rounded-xl my-4;
4747
}
4848

4949
ul {

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

Lines changed: 72 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $, component$, Slot, useSignal } from '@builder.io/qwik';
1+
import { component$, Slot } from '@builder.io/qwik';
22
import {
33
Combobox,
44
ComboboxControl,
@@ -9,7 +9,10 @@ import {
99
ComboboxTrigger,
1010
} from '@qwik-ui/headless';
1111

12-
import { ComboboxOption } from '../../../../../../../../packages/kit-headless/src/components/combobox';
12+
import {
13+
ComboboxOption,
14+
type ResolvedOption,
15+
} from '../../../../../../../../packages/kit-headless/src/components/combobox';
1316

1417
import { PreviewCodeExample } from '../../../_components/preview-code-example/preview-code-example';
1518

@@ -46,27 +49,25 @@ const objectExample: Array<Trainer> = [
4649
];
4750

4851
export const HeroExample = component$(() => {
49-
const objectExampleSig = useSignal(objectExample);
50-
5152
return (
5253
<>
5354
<PreviewCodeExample>
5455
<div class="flex flex-col gap-4" q:slot="actualComponent">
55-
<Combobox
56-
options={objectExampleSig.value}
56+
<Combobox<Trainer>
57+
options={objectExample}
5758
optionValueKey="testValue"
5859
optionLabelKey="testLabel"
5960
optionDisabledKey="disabled"
60-
optionComponent$={(option: Trainer, key: number, index: number) => (
61+
renderOption$={(resolved, index: number) => (
6162
<ComboboxOption
62-
key={key}
63+
key={resolved.key}
64+
resolved={resolved}
6365
index={index}
64-
option={option}
65-
style={option.disabled ? { color: 'gray' } : {}}
66+
style={resolved.disabled ? { color: 'gray' } : {}}
6667
class="rounded-sm px-2 hover:bg-[#496080] aria-selected:bg-[#496080] border-2 border-transparent aria-selected:border-[#abbbce] group"
6768
>
6869
<span class="block group-aria-selected:translate-x-[3px] transition-transform duration-350">
69-
{option.testLabel}
70+
{resolved.option.testLabel}
7071
</span>
7172
</ComboboxOption>
7273
)}
@@ -95,7 +96,11 @@ export const HeroExample = component$(() => {
9596
</ComboboxTrigger>
9697
</ComboboxControl>
9798
<ComboboxPortal>
98-
<ComboboxListbox class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]" />
99+
<ComboboxListbox
100+
position="bottom"
101+
gap={8}
102+
class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]"
103+
/>
99104
</ComboboxPortal>
100105
</Combobox>
101106
</div>
@@ -126,58 +131,60 @@ export const StringCombobox = component$(() => {
126131
'Cucumber',
127132
];
128133

129-
const fruitsSig = useSignal(fruits);
130-
131134
return (
132135
<PreviewCodeExample>
133-
<div class="flex flex-col gap-4" q:slot="actualComponent">
134-
<div>
136+
<div class="flex flex-col items-center gap-4 p-4" q:slot="actualComponent">
137+
<div class=" text-left">
135138
This uses a custom filter to only filter from the beginning of the options.
136139
</div>
137-
<Combobox
138-
options={fruitsSig.value}
139-
defaultLabel="Currant"
140-
filter$={(value: string, options) =>
141-
options.filter(({ option }) => {
142-
return option.toLowerCase().startsWith(value.toLowerCase());
143-
})
144-
}
145-
optionComponent$={$((option: string, index: number) => (
146-
<ComboboxOption
147-
class="rounded-sm px-2 hover:bg-[#496080] aria-selected:bg-[#496080] border-2 border-transparent aria-selected:border-[#abbbce] group"
148-
index={index}
149-
option={option}
150-
>
151-
{option}
152-
</ComboboxOption>
153-
))}
154-
>
155-
<ComboboxLabel class=" font-semibold dark:text-white text-[#333333]">
156-
Fruits 🍓
157-
</ComboboxLabel>
158-
<ComboboxControl class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
159-
<ComboboxInput
160-
class="px-2 w-44 bg-inherit px-d2 pr-6 text-white"
161-
placeholder="Papaya"
162-
/>
163-
<ComboboxTrigger class="w-6 h-6 group absolute right-0">
164-
<svg
165-
xmlns="http://www.w3.org/2000/svg"
166-
viewBox="0 0 24 24"
167-
fill="none"
168-
class="stroke-white group-aria-expanded:-rotate-180 transition-transform duration-[450ms]"
169-
stroke-linecap="round"
170-
stroke-width="2"
171-
stroke-linejoin="round"
140+
<div>
141+
<Combobox
142+
class="w-fit"
143+
options={fruits}
144+
defaultLabel="Currant"
145+
filter$={(value: string, options) =>
146+
options.filter(({ option }) => {
147+
return option.toLowerCase().startsWith(value.toLowerCase());
148+
})
149+
}
150+
renderOption$={(resolved: ResolvedOption, index: number) => (
151+
<ComboboxOption
152+
key={resolved.key}
153+
class="rounded-sm px-2 hover:bg-[#496080] aria-selected:bg-[#496080] border-2 border-transparent aria-selected:border-[#abbbce] group"
154+
index={index}
155+
resolved={resolved}
172156
>
173-
<polyline points="6 9 12 15 18 9"></polyline>
174-
</svg>
175-
</ComboboxTrigger>
176-
</ComboboxControl>
177-
<ComboboxPortal>
178-
<ComboboxListbox class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]" />
179-
</ComboboxPortal>
180-
</Combobox>
157+
{resolved.label}
158+
</ComboboxOption>
159+
)}
160+
>
161+
<ComboboxLabel class=" font-semibold dark:text-white text-[#333333]">
162+
Fruits 🍓
163+
</ComboboxLabel>
164+
<ComboboxControl class="bg-[#1f2532] flex items-center rounded-sm border-[#7d95b3] border-[1px] relative">
165+
<ComboboxInput
166+
class="px-2 w-44 bg-inherit px-d2 pr-6 text-white"
167+
placeholder="Papaya"
168+
/>
169+
<ComboboxTrigger class="w-6 h-6 group absolute right-0">
170+
<svg
171+
xmlns="http://www.w3.org/2000/svg"
172+
viewBox="0 0 24 24"
173+
fill="none"
174+
class="stroke-white group-aria-expanded:-rotate-180 transition-transform duration-[450ms]"
175+
stroke-linecap="round"
176+
stroke-width="2"
177+
stroke-linejoin="round"
178+
>
179+
<polyline points="6 9 12 15 18 9"></polyline>
180+
</svg>
181+
</ComboboxTrigger>
182+
</ComboboxControl>
183+
<ComboboxPortal>
184+
<ComboboxListbox class="text-white w-44 bg-[#1f2532] px-4 py-2 rounded-sm border-[#7d95b3] border-[1px]" />
185+
</ComboboxPortal>
186+
</Combobox>
187+
</div>
181188
</div>
182189

183190
<div q:slot="codeExample">
@@ -189,7 +196,7 @@ export const StringCombobox = component$(() => {
189196

190197
// Using context example.
191198

192-
import { createContextId, useContextProvider, useContext } from '@builder.io/qwik';
199+
import { createContextId, useContext, useContextProvider } from '@builder.io/qwik';
193200

194201
// Create a context ID
195202
export const AnimalContext = createContextId<string[]>('animal-context');
@@ -204,24 +211,23 @@ export const ParentComponent = component$(() => {
204211

205212
export const ContextExample = component$(() => {
206213
const animals = useContext(AnimalContext);
207-
const animalsSig = useSignal(animals);
208214

209215
return (
210216
<PreviewCodeExample>
211217
<div class="flex flex-col gap-4" q:slot="actualComponent">
212218
<Combobox
213-
options={animalsSig.value}
214-
optionComponent$={$((option: string, index: number) => (
219+
options={animals}
220+
renderOption$={(resolved: ResolvedOption, index: number) => (
215221
<ComboboxOption
216222
index={index}
217-
option={option}
223+
resolved={resolved}
218224
class="rounded-sm px-2 hover:bg-[#496080] aria-selected:bg-[#496080] border-2 border-transparent aria-selected:border-[#abbbce] group"
219225
>
220226
<span class="block group-aria-selected:translate-x-[3px] transition-transform duration-350">
221-
{option}
227+
{resolved.label}
222228
</span>
223229
</ComboboxOption>
224-
))}
230+
)}
225231
class="relative"
226232
>
227233
<ComboboxLabel class="font-semibold dark:text-white text-[#333333]">

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

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -65,35 +65,18 @@ import {statusByComponent} from '../../../../../_state/component-statuses';
6565
```
6666
</HeroExample>
6767

68-
<div class="bg-red-200 dark:bg-red-900 p-4 rounded-sm border-l-[3px] dark:border-[#F3F3F3] border-[#333333]">
69-
<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>
70-
<CodeExample classes="!mb-0 !p-6 !rounded-md">
71-
```tsx
72-
<body>
73-
<QwikUIProvider>
74-
<slot />
75-
</QwikUIProvider>
76-
</body>
77-
```
78-
</CodeExample>
68+
Qwik UI's Combobox implementation follows the [WAI-Aria Combobox specifications](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/), along with some additional API's that enhance the flexibility, types, and performance.
7969

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>
90-
</div>
70+
<div class="mb-6 flex flex-col gap-2">
71+
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/combobox)
9172

92-
<br/>
73+
[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues)
9374

94-
> 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)
75+
[Edit This Page 🗒️](<https://github.com/qwikifiers/qwik-ui/edit/main/apps/website/src/routes/docs/headless/(components)/combobox/index.mdx>)
9576

96-
<br/>
77+
</div>
78+
79+
<br />
9780

9881
##### ✨ Features
9982

@@ -107,18 +90,45 @@ import {statusByComponent} from '../../../../../_state/component-statuses';
10790

10891
<br/>
10992

93+
> 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)
11094
111-
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.
112-
113-
<div class="mb-6 flex flex-col gap-2">
114-
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/combobox)
95+
<br />
11596

116-
[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues)
97+
<div class="bg-red-200 dark:bg-red-900 p-4 lg:p-6 rounded-xl dark:border-[#F3F3F3] border-[#333333]">
98+
<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>
99+
<CodeExample classes="!mb-0 !p-6 !rounded-md">
100+
```tsx
101+
import { QwikUIProvider } from '@qwik-ui/headless'; // import Provider component
117102

118-
[Edit This Page 🗒️](<https://github.com/qwikifiers/qwik-ui/edit/main/apps/website/src/routes/docs/headless/(components)/combobox/index.mdx>)
103+
// wrap as a direct child to the body tag
104+
<body>
105+
<QwikUIProvider>
106+
<slot />
107+
</QwikUIProvider>
108+
</body>
109+
```
110+
</CodeExample>
119111

112+
<div class="pt-12 pb-4">
113+
#### <ins>**Context Caveats**</ins>
114+
<p class="pt-2 pb-4">Portals are still currently in **Beta**, as a result, you may experience an issue using your own context to pass data into the portal children.</p>
115+
<p class="pb-4">If you do experience any context related issues, add the following **contextIds** prop to the **ComboboxPortal** component. It takes in an array of string context id's as a prop. We also have a live example below with context.</p>
116+
<p class="pb-4">If you are not using context inside the portal children, this will not be an issue.</p>
117+
118+
<CodeExample classes="!mb-0 !p-6 !rounded-md">
119+
```tsx
120+
<ComboboxPortal contextIds={["my-context-id", "my-context-id-2"]}>
121+
<ComboboxListbox>
122+
<ComboboxOption>Option</ComboboxOption>
123+
</ComboboxListbox>
124+
</ComboboxPortal>
125+
```
126+
</CodeExample>
127+
</div>
120128
</div>
121129

130+
<br/>
131+
122132
## Building blocks
123133

124134
<CodeExample>
@@ -128,22 +138,23 @@ Qwik UI's Combobox implementation follows the [WAI-Aria](https://www.w3.org/WAI/
128138

129139
export default component$(() => {
130140
return (
131-
<Combobox>
132-
<ComboboxLabel>Label</ComboboxLabel>
141+
<Combobox renderOption$={() => (
142+
<ComboboxOption>
143+
Option
144+
</ComboboxOption>
145+
)}>
146+
<ComboboxLabel>Label Element</ComboboxLabel>
133147
<ComboboxControl>
134148
<ComboboxInput />
135149
<ComboboxTrigger>
136-
Button
150+
Opens Listbox
137151
</ComboboxTrigger>
138152
</ComboboxControl>
139153
<ComboboxPortal>
140-
<ComboboxListbox>
141-
<ComboboxOption>Option</ComboboxOption>
142-
</ComboboxListbox>
154+
<ComboboxListbox />
143155
</ComboboxPortal>
144156
</Combobox>
145157
)
146-
})
147158
```
148159
149160
</CodeExample>

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { QRL, Signal } from '@builder.io/qwik';
2-
import { JSX } from '@builder.io/qwik/jsx-runtime';
1+
import type { Signal } from '@builder.io/qwik';
2+
import type { ComboboxProps, ResolvedOption } from './combobox';
33

44
export interface ComboboxContext<O extends Option = Option> {
55
// user's source of truth
6-
optionsSig: Signal<{ option: O; key: number }[]>;
7-
optionComponent$?: QRL<(option: O, key: number, filteredIndex: number) => JSX.Element>;
6+
filteredOptionsSig: Signal<ResolvedOption<O>[]>;
7+
renderOption$?: ComboboxProps<O>['renderOption$'];
88

99
// element state
1010
inputValueSig: Signal<string>;

0 commit comments

Comments
 (0)