Skip to content

Commit 7f5d8d7

Browse files
feat(select): onChange$ and onOpenChange$
1 parent dbe0eda commit 7f5d8d7

File tree

6 files changed

+142
-4
lines changed

6 files changed

+142
-4
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { component$, useSignal, $ } from '@builder.io/qwik';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@qwik-ui/headless';
9+
export default component$(() => {
10+
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
11+
const counterSig = useSignal(0);
12+
13+
const handleChange$ = $((): void => {
14+
counterSig.value++;
15+
});
16+
17+
return (
18+
<>
19+
<Select onChange$={handleChange$} class="relative min-w-40">
20+
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
21+
<SelectValue placeholder="Select an option" />
22+
</SelectTrigger>
23+
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
24+
{usersSig.value.map((user) => (
25+
<SelectOption
26+
class="border-dashed border-blue-400 data-[highlighted]:border-2"
27+
key={user}
28+
>
29+
{user}
30+
</SelectOption>
31+
))}
32+
</SelectListbox>
33+
</Select>
34+
<p>You have changed {counterSig.value} times</p>
35+
</>
36+
);
37+
});

apps/website/src/routes/docs/headless/select/examples/controlled.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import {
66
SelectTrigger,
77
SelectValue,
88
} from '@qwik-ui/headless';
9-
109
export default component$(() => {
1110
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
1211
const selectedVal = useSignal<string>('Ryan');
1312

1413
return (
1514
<>
16-
<Select bind:value={selectedVal} class="relative min-w-40">
15+
<Select
16+
onChange$={$(() => console.log('Changed!'))}
17+
bind:value={selectedVal}
18+
class="relative min-w-40"
19+
>
1720
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
1821
<SelectValue placeholder="Select an option" />
1922
</SelectTrigger>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { component$, useSignal, $ } from '@builder.io/qwik';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@qwik-ui/headless';
9+
export default component$(() => {
10+
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
11+
const openChangeSig = useSignal(0);
12+
13+
const handleOpenChange$ = $((): void => {
14+
openChangeSig.value++;
15+
});
16+
17+
return (
18+
<>
19+
<Select onOpenChange$={handleOpenChange$} class="relative min-w-40">
20+
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
21+
<SelectValue placeholder="Select an option" />
22+
</SelectTrigger>
23+
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
24+
{usersSig.value.map((user) => (
25+
<SelectOption
26+
class="border-dashed border-blue-400 data-[highlighted]:border-2"
27+
key={user}
28+
>
29+
{user}
30+
</SelectOption>
31+
))}
32+
</SelectListbox>
33+
</Select>
34+
<p>The listbox opened and closed {openChangeSig.value} time(s)</p>
35+
</>
36+
);
37+
});

apps/website/src/routes/docs/headless/select/index.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ This element is used to create a drop-down list, it's often used in a form, to c
4646
<Showcase name="add-users" />
4747
</div>
4848

49+
## onChange$
50+
51+
<div data-testid="select-change-test">
52+
<Showcase name="change-value" />
53+
</div>
54+
55+
## onOpenChange$
56+
57+
<div data-testid="select-open-change-test">
58+
<Showcase name="open-change" />
59+
</div>
60+
4961
## Building blocks
5062

5163
<CodeSnippet name="building-blocks" />

apps/website/src/routes/docs/headless/select/select.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,35 @@ test.describe('Props', () => {
536536
await expect(await getValue()).toEqual('Select an option');
537537
});
538538

539+
test(`GIVEN a select with an onChange$ prop
540+
WHEN the select value changes
541+
THEN the placeholder should be presented instead of a selected value`, async ({
542+
page,
543+
}) => {
544+
const { openListbox, getOptions, getRoot } = await setup(page, 'select-change-test');
545+
546+
await openListbox('click');
547+
548+
const options = await getOptions();
549+
await options[3].click();
550+
551+
const sibling = getRoot().locator('+ p');
552+
await expect(sibling).toHaveText('You have changed 1 times');
553+
});
554+
555+
test(`GIVEN a select with an onOpenChange$ prop
556+
WHEN the select value changes
557+
THEN the placeholder should be presented instead of a selected value`, async ({
558+
page,
559+
}) => {
560+
const { getRoot, openListbox } = await setup(page, 'select-open-change-test');
561+
562+
await openListbox('click');
563+
564+
const sibling = getRoot().locator('+ p');
565+
await expect(sibling).toHaveText('The listbox opened and closed 1 time(s)');
566+
});
567+
539568
test.describe('uncontrolled', () => {
540569
test(`GIVEN an uncontrolled select with a value prop on the root component
541570
WHEN the value data matches the fourth option
@@ -586,7 +615,7 @@ test.describe('Props', () => {
586615
test.describe('controlled', () => {
587616
test(`GIVEN a controlled select with a bind:value prop on the root component
588617
WHEN the signal data matches the second option
589-
THEN the selected value should be the data passed to the value prop`, async ({
618+
THEN the selected value should be the data passed to the bind:value prop`, async ({
590619
page,
591620
}) => {
592621
const { getValue, getOptions } = await setup(page, 'select-controlled-test');

packages/kit-headless/src/components/select/select.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
Signal,
88
useTask$,
99
useComputed$,
10+
type QRL,
1011
} from '@builder.io/qwik';
1112
import { type SelectContext } from './select-context';
1213
import SelectContextId from './select-context';
1314
import { Opt } from './select-inline';
15+
import { isBrowser } from '@builder.io/qwik/build';
1416

1517
export type SelectProps = PropsOf<'div'> & {
1618
value?: string;
@@ -21,6 +23,9 @@ export type SelectProps = PropsOf<'div'> & {
2123

2224
// when a value is passed, we check if it's an actual option value, and get its index at pre-render time.
2325
_valuePropIndex?: number | null;
26+
27+
onChange$?: QRL<() => void>;
28+
onOpenChange$?: QRL<() => void>;
2429
};
2530

2631
/* root component in select-inline.tsx */
@@ -47,7 +52,7 @@ export const SelectImpl = component$<SelectProps>((props) => {
4752
const isListboxOpenSig = useSignal<boolean>(false);
4853

4954
// Maps are apparently great for this index accessing. Will learn more about them this week and refactor this to have a more consistent API and eliminate redundancy / duplication.
50-
useTask$(({ track }) => {
55+
useTask$(function controlledValueTask({ track }) {
5156
const controlledValue = track(() => props['bind:value']?.value);
5257
if (!controlledValue) return;
5358

@@ -58,6 +63,21 @@ export const SelectImpl = component$<SelectProps>((props) => {
5863
}
5964
});
6065

66+
useTask$(async function onChangeTask({ track }) {
67+
track(() => selectedIndexSig.value);
68+
if (isBrowser) {
69+
await props.onChange$?.();
70+
}
71+
});
72+
73+
useTask$(function onOpenChangeTask({ track }) {
74+
track(() => isListboxOpenSig.value);
75+
76+
if (isBrowser) {
77+
props.onOpenChange$?.();
78+
}
79+
});
80+
6181
const context: SelectContext = {
6282
triggerRef,
6383
popoverRef,

0 commit comments

Comments
 (0)