Skip to content

Commit 2b2eeab

Browse files
feat(select): getting option values at ssr
1 parent 24c3d68 commit 2b2eeab

File tree

12 files changed

+117
-17
lines changed

12 files changed

+117
-17
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default component$(() => {
1414
<Select class="relative min-w-40">
1515
<p>This one is the disabled</p>
1616
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
17-
<SelectValue />
17+
<SelectValue placeholder="Select an option" />
1818
</SelectTrigger>
1919
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
2020
{usersSig.value.map((user, index) => (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default component$(() => {
1313
return (
1414
<Select class="relative min-w-40">
1515
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
16-
<SelectValue />
16+
<SelectValue placeholder="Select an option" />
1717
</SelectTrigger>
1818
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
1919
{usersSig.value.map((user) => (
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
10+
export default component$(() => {
11+
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
12+
13+
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>
29+
);
30+
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ This element is used to create a drop-down list, it's often used in a form, to c
1818
<Showcase name="disabled" />
1919
</div>
2020

21+
<div data-testid="select-uncontrolled-test">
22+
<Showcase name="uncontrolled" />
23+
</div>
24+
2125
## Building blocks
2226

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

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export function createTestDriver<T extends DriverLocator>(locator: T) {
2020
};
2121

2222
const getValue = async () => {
23-
return await getTrigger().locator('[data-value]').textContent();
23+
// annoyingly, it seems we need to check if the listbox is hidden in playwright, or else the value does not update
24+
await expect(getListbox()).toBeHidden();
25+
26+
return await getTrigger()
27+
.locator('[data-value]', { includeHidden: true })
28+
.textContent();
2429
};
2530

2631
const openListbox = async (key: OpenKeys | 'click') => {

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ test.describe('Keyboard Behavior', () => {
370370
AND the Enter key is pressed
371371
THEN option value should be the selected value
372372
AND should have an aria-selected of true`, async ({ page }) => {
373-
const { getTrigger, getOptions, getValue, getListbox, openListbox } = await setup(
373+
const { getTrigger, getOptions, getValue, openListbox } = await setup(
374374
page,
375375
'select-hero-test',
376376
);
@@ -382,8 +382,6 @@ test.describe('Keyboard Behavior', () => {
382382
const optStr = await options[0].textContent();
383383
await getTrigger().press('Enter');
384384

385-
// seems we need to await for the listbox to be hidden, otherwise the getValue does not update. ¯\_(ツ)_/¯
386-
await expect(getListbox()).toBeHidden();
387385
const value = await getValue();
388386
expect(optStr).toEqual(value);
389387
});
@@ -488,3 +486,37 @@ test.describe('Disabled', () => {
488486
await expect(options[options.length - 2]).toHaveAttribute('data-highlighted');
489487
});
490488
});
489+
490+
test.describe('Props', () => {
491+
test(`GIVEN a basic select
492+
WHEN there is a placeholder
493+
THEN the placeholder should be presented instead of a selected value`, async ({
494+
page,
495+
}) => {
496+
const { getValue } = await setup(page, 'select-hero-test');
497+
498+
await expect(await getValue()).toEqual('Select an option');
499+
});
500+
501+
test(`GIVEN an uncontrolled select with a value prop on the root component
502+
WHEN the value data matches the fourth option
503+
THEN the selected value should be the data passed to the value prop`, async ({
504+
page,
505+
}) => {
506+
const { getValue } = await setup(page, 'select-uncontrolled-test');
507+
508+
await expect(await getValue()).toEqual('Jessie');
509+
});
510+
511+
// test(`GIVEN an uncontrolled select with a value prop on the root component
512+
// WHEN the value data matches the fourth option
513+
// THEN the fourth option should have aria-selected set to true`, async ({
514+
// page,
515+
// }) => {
516+
// const { getValue, getOptions } = await setup(page, 'select-uncontrolled-test');
517+
518+
// await expect(await getValue()).toEqual('Jessie');
519+
// const options = await getOptions();
520+
// await expect(await options[3]).toBeSelected();
521+
// });
522+
});

packages/kit-headless/src/components/select/notes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ What do they all have in common? How do people use them? What are the most impor
6262

6363
### Behavior
6464

65+
name: placeholder
66+
type: string
67+
description: sets a placeholder instead of a selected value.
68+
6569
name: disabled
6670
type: boolean
6771
description: When true, the option is disabled.

packages/kit-headless/src/components/select/select-context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ export type SelectContext = {
1818
highlightedIndexSig: Signal<number | null>;
1919
isListboxOpenSig: Signal<boolean>;
2020
selectedIndexSig: Signal<number | null>;
21+
value: string | undefined;
2122
};

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SelectOption } from './select-option';
99
*/
1010
export const Select: FunctionComponent<SelectProps> = (props) => {
1111
const { children: myChildren, ...rest } = props;
12+
let valuePropIndex = 0;
1213

1314
const childrenToProcess = (
1415
Array.isArray(myChildren) ? [...myChildren] : [myChildren]
@@ -45,9 +46,17 @@ export const Select: FunctionComponent<SelectProps> = (props) => {
4546
}
4647
case SelectOption: {
4748
child.props.index = currentIndex;
49+
if (child.props.children === props.value) {
50+
valuePropIndex = currentIndex;
51+
}
52+
4853
currentIndex++;
4954
}
5055
}
5156
}
52-
return <SelectImpl {...rest}>{props.children}</SelectImpl>;
57+
return (
58+
<SelectImpl {...rest} valuePropIndex={valuePropIndex}>
59+
{props.children}
60+
</SelectImpl>
61+
);
5362
};

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useTask$,
77
useSignal,
88
$,
9+
useComputed$,
910
} from '@builder.io/qwik';
1011
import SelectContextId from './select-context';
1112

@@ -21,8 +22,13 @@ export const SelectOption = component$<SelectOptionProps>((props) => {
2122
const optionRef = useSignal<HTMLLIElement>();
2223
const localIndexSig = useSignal<number | null>(null);
2324

24-
const isHighlighted = !disabled && context.highlightedIndexSig.value === index;
25-
const isSelected = !disabled && context.selectedIndexSig.value === index;
25+
const isSelectedSig = useComputed$(() => {
26+
return !disabled && context.selectedIndexSig.value === index;
27+
});
28+
29+
const isHighlightedSig = useComputed$(() => {
30+
return !disabled && context.highlightedIndexSig.value === index;
31+
});
2632

2733
useTask$(function getIndexTask() {
2834
if (index === undefined)
@@ -55,10 +61,10 @@ export const SelectOption = component$<SelectOptionProps>((props) => {
5561
onPointerOver$={[handlePointerOver$, props.onPointerOver$]}
5662
ref={optionRef}
5763
tabIndex={-1}
58-
aria-selected={isSelected}
64+
aria-selected={isSelectedSig.value}
5965
aria-disabled={disabled === true ? 'true' : 'false'}
60-
data-selected={isSelected ? '' : undefined}
61-
data-highlighted={isHighlighted ? '' : undefined}
66+
data-selected={isSelectedSig.value ? '' : undefined}
67+
data-highlighted={isHighlightedSig.value ? '' : undefined}
6268
data-disabled={disabled ? '' : undefined}
6369
role="option"
6470
>

0 commit comments

Comments
 (0)