Skip to content

Commit a5b0bdc

Browse files
feat(select): added new bind:value prop, more tests
1 parent a025698 commit a5b0bdc

File tree

4 files changed

+87
-37
lines changed

4 files changed

+87
-37
lines changed
Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { component$, useSignal } from '@builder.io/qwik';
1+
import { component$, useSignal, $ } from '@builder.io/qwik';
22
import {
33
Select,
44
SelectListbox,
@@ -9,22 +9,28 @@ import {
99

1010
export default component$(() => {
1111
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
12+
const selectedVal = useSignal<string>('Ryan');
1213

1314
return (
14-
<Select value="Jessie" class="relative min-w-40">
15-
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
16-
<SelectValue placeholder="Select an option" />
17-
</SelectTrigger>
18-
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
19-
{usersSig.value.map((user) => (
20-
<SelectOption
21-
class="border-dashed border-blue-400 data-[highlighted]:border-2"
22-
key={user}
23-
>
24-
{user}
25-
</SelectOption>
26-
))}
27-
</SelectListbox>
28-
</Select>
15+
<>
16+
<Select bind:value={selectedVal} class="relative min-w-40">
17+
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
18+
<SelectValue placeholder="Select an option" />
19+
</SelectTrigger>
20+
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
21+
{usersSig.value.map((user) => (
22+
<SelectOption
23+
class="border-dashed border-blue-400 data-[highlighted]:border-2"
24+
key={user}
25+
>
26+
{user}
27+
</SelectOption>
28+
))}
29+
</SelectListbox>
30+
</Select>
31+
<button onClick$={$(() => (selectedVal.value = 'Jessie'))}>
32+
Click me to change val!
33+
</button>
34+
</>
2935
);
3036
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ This element is used to create a drop-down list, it's often used in a form, to c
3434
<Showcase name="wrong-value" />
3535
</div>
3636

37+
## Controlled
38+
39+
<div data-testid="select-controlled-test">
40+
<Showcase name="controlled" />
41+
</div>
42+
3743
## Building blocks
3844

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

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

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,8 @@ test.describe('Props', () => {
517517

518518
test.describe('uncontrolled', () => {
519519
test(`GIVEN an uncontrolled select with a value prop on the root component
520-
WHEN the value data matches the fourth option
521-
THEN the selected value should be the data passed to the value prop`, async ({
520+
WHEN the value data matches the fourth option
521+
THEN the selected value should be the data passed to the value prop`, async ({
522522
page,
523523
}) => {
524524
const { getValue, getOptions } = await setup(page, 'select-uncontrolled-test');
@@ -529,10 +529,8 @@ test.describe('Props', () => {
529529
});
530530

531531
test(`GIVEN an uncontrolled select with a value prop on the root component
532-
WHEN the value prop data matches the fourth option
533-
THEN the fourth option should have data-highlighted set to true`, async ({
534-
page,
535-
}) => {
532+
WHEN the value prop data matches the fourth option
533+
THEN the fourth option should have data-highlighted`, async ({ page }) => {
536534
const { getValue, getOptions } = await setup(page, 'select-uncontrolled-test');
537535

538536
const options = await getOptions();
@@ -541,8 +539,8 @@ test.describe('Props', () => {
541539
});
542540

543541
test(`GIVEN an uncontrolled select with a value prop on the root component
544-
WHEN the value prop data matches the fourth option
545-
THEN the fourth option should have aria-selected set to true`, async ({
542+
WHEN the value prop data matches the fourth option
543+
THEN the fourth option should have aria-selected set to true`, async ({
546544
page,
547545
}) => {
548546
const { getValue, getOptions } = await setup(page, 'select-uncontrolled-test');
@@ -553,8 +551,8 @@ test.describe('Props', () => {
553551
});
554552

555553
test(`GIVEN an uncontrolled select with a value prop on the root component
556-
WHEN the value data does NOT match any option
557-
THEN fallback to the placeholder`, async ({ page }) => {
554+
WHEN the value data does NOT match any option
555+
THEN fallback to the placeholder`, async ({ page }) => {
558556
const { getValue } = await setup(page, 'select-wrong-value-test');
559557

560558
/**
@@ -564,16 +562,40 @@ test.describe('Props', () => {
564562
});
565563
});
566564

567-
// test.describe('controlled', () => {
568-
// test(`GIVEN a controlled select with a bind:value prop on the root component
569-
// WHEN the signal data matches the second option
570-
// THEN the selected value should be the data passed to the value prop`, async ({
571-
// page,
572-
// }) => {
573-
// const { getValue, getOptions } = await setup(page, 'select-uncontrolled-test');
565+
test.describe('controlled', () => {
566+
test(`GIVEN a controlled select with a bind:value prop on the root component
567+
WHEN the signal data matches the second option
568+
THEN the selected value should be the data passed to the value prop`, async ({
569+
page,
570+
}) => {
571+
const { getValue, getOptions } = await setup(page, 'select-controlled-test');
574572

575-
// const options = await getOptions();
576-
// expect(await getValue()).toEqual(await options[1].textContent());
577-
// });
578-
// });
573+
const options = await getOptions();
574+
expect(await getValue()).toEqual(await options[1].textContent());
575+
});
576+
577+
test(`GIVEN a controlled select with a bind:value prop on the root component
578+
WHEN the signal data matches the second option
579+
THEN the selected value should should have data-highlighted`, async ({
580+
page,
581+
}) => {
582+
const { getValue, getOptions } = await setup(page, 'select-controlled-test');
583+
584+
const options = await getOptions();
585+
expect(await getValue()).toEqual(await options[1].textContent());
586+
await expect(options[1]).toHaveAttribute('data-highlighted');
587+
});
588+
589+
test(`GIVEN an controlled select with a bind:value prop on the root component
590+
WHEN the signal data matches the second option
591+
THEN the second option should have aria-selected set to true`, async ({
592+
page,
593+
}) => {
594+
const { getValue, getOptions } = await setup(page, 'select-controlled-test');
595+
596+
const options = await getOptions();
597+
expect(await getValue()).toEqual(await options[1].textContent());
598+
await expect(options[1]).toHaveAttribute('aria-selected', 'true');
599+
});
600+
});
579601
});

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import {
44
type PropsOf,
55
useSignal,
66
useContextProvider,
7+
Signal,
8+
useTask$,
79
} from '@builder.io/qwik';
810
import { type SelectContext } from './select-context';
911
import SelectContextId from './select-context';
1012
import { Opt } from './select-inline';
1113

1214
export type SelectProps = PropsOf<'div'> & {
1315
value?: string;
16+
'bind:value'?: Signal<string>;
1417

1518
// our source of truth for the options. We get this at pre-render time in the inline component, that way we do not need textContent, etc.
1619
_options?: Opt[];
@@ -26,13 +29,26 @@ export const SelectImpl = component$<SelectProps>((props) => {
2629
const triggerRef = useSignal<HTMLButtonElement>();
2730
const popoverRef = useSignal<HTMLElement>();
2831
const listboxRef = useSignal<HTMLUListElement>();
32+
2933
const options = props._options;
34+
const optionsIndex = new Map(options.map((option, index) => [option.value, index]));
3035

3136
// core state
3237
const selectedIndexSig = useSignal<number | null>(props._valuePropIndex ?? null);
3338
const highlightedIndexSig = useSignal<number | null>(props._valuePropIndex ?? null);
3439
const isListboxOpenSig = useSignal<boolean>(false);
3540

41+
// 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.
42+
useTask$(({ track }) => {
43+
const bindValue = track(() => props['bind:value']?.value);
44+
45+
const matchingIndex = optionsIndex.get(bindValue) ?? -1;
46+
if (matchingIndex !== -1) {
47+
selectedIndexSig.value = matchingIndex;
48+
highlightedIndexSig.value = matchingIndex;
49+
}
50+
});
51+
3652
const context: SelectContext = {
3753
triggerRef,
3854
popoverRef,

0 commit comments

Comments
 (0)