Skip to content

Commit a6c9f57

Browse files
test(select): new select option tests
1 parent 9fe9cfd commit a6c9f57

File tree

7 files changed

+195
-24
lines changed

7 files changed

+195
-24
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import { component$, useSignal } from '@builder.io/qwik';
2-
import { Select, SelectListbox, SelectOption, SelectTrigger } from '@qwik-ui/headless';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@qwik-ui/headless';
39

410
export default component$(() => {
511
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
612

713
return (
814
<Select class="relative min-w-40">
915
<p>This one is the disabled</p>
10-
<SelectTrigger class="w-full border-2 border-dashed border-red-400" />
16+
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
17+
<SelectValue />
18+
</SelectTrigger>
1119
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
1220
{usersSig.value.map((user, index) => (
1321
<SelectOption
14-
class="border-dashed border-blue-400 data-[highlighted]:border-2"
22+
class="border-dashed border-blue-400 data-[highlighted]:border-2 data-[disabled]:bg-slate-600 data-[disabled]:opacity-30"
1523
key={user}
1624
disabled={index === 0 || index === usersSig.value.length - 1 ? true : false}
1725
>

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { component$, useSignal } from '@builder.io/qwik';
2-
import { Select, SelectListbox, SelectOption, SelectTrigger } from '@qwik-ui/headless';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@qwik-ui/headless';
39

410
export default component$(() => {
511
const usersSig = useSignal<string[]>(['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby']);
612

713
return (
814
<Select class="relative min-w-40">
9-
<SelectTrigger class="w-full border-2 border-dashed border-red-400" />
15+
<SelectTrigger class="w-full border-2 border-dashed border-red-400">
16+
<SelectValue />
17+
</SelectTrigger>
1018
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
1119
{usersSig.value.map((user) => (
1220
<SelectOption

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,29 @@ export function createTestDriver<T extends DriverLocator>(locator: T) {
77
return locator.getByRole('combobox');
88
};
99

10+
const getTrigger = () => {
11+
return getRoot().getByRole('button');
12+
};
13+
14+
const getListbox = () => {
15+
return getRoot().getByRole('listbox');
16+
};
17+
18+
const getOptions = () => {
19+
return getRoot().getByRole('option', { includeHidden: true }).all();
20+
};
21+
22+
const getValue = () => {
23+
return getTrigger().locator('[data-value]').textContent();
24+
};
25+
1026
return {
1127
...locator,
1228
locator,
1329
getRoot,
14-
getListbox() {
15-
return getRoot().getByRole('listbox');
16-
},
17-
getTrigger() {
18-
return getRoot().getByRole('button');
19-
},
20-
// get all options
21-
getOptions() {
22-
return getRoot().getByRole('option').all();
23-
},
30+
getTrigger,
31+
getListbox,
32+
getOptions,
33+
getValue,
2434
};
2535
}

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

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ async function setup(page: Page, selector: string) {
55

66
const driver = createTestDriver(page.getByTestId(selector));
77

8-
const { getListbox, getTrigger, getOptions } = driver;
8+
const { getListbox, getTrigger, getOptions, getValue } = driver;
99

1010
return {
1111
driver,
1212
getListbox,
1313
getTrigger,
1414
getOptions,
15+
getValue,
1516
};
1617
}
1718

@@ -40,6 +41,56 @@ test.describe('Mouse Behavior', () => {
4041
await expect(getListbox()).toBeHidden();
4142
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
4243
});
44+
45+
test(`GIVEN a hero select with an open listbox
46+
WHEN an option is clicked
47+
THEN close the listbox AND aria-expanded should be false`, async ({ page }) => {
48+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
49+
50+
await getTrigger().click();
51+
// should be open initially
52+
await expect(getListbox()).toBeVisible();
53+
54+
const options = await getOptions();
55+
options[0].click();
56+
57+
await expect(getListbox()).toBeHidden();
58+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
59+
});
60+
61+
test(`GIVEN a hero select with an open listbox
62+
WHEN the 2nd option is clicked
63+
THEN the 2nd option should have aria-selected`, async ({ page }) => {
64+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
65+
66+
await getTrigger().click();
67+
// should be open initially
68+
await expect(getListbox()).toBeVisible();
69+
70+
const options = await getOptions();
71+
options[1].click();
72+
73+
await expect(options[1]).toHaveAttribute('aria-selected', 'true');
74+
});
75+
76+
test(`GIVEN a hero select with an open listbox
77+
WHEN the 3rd option is clicked
78+
THEN the 3rd option should be the selected value`, async ({ page }) => {
79+
const { getTrigger, getListbox, getOptions, getValue } = await setup(
80+
page,
81+
'select-hero-test',
82+
);
83+
84+
await getTrigger().click();
85+
// should be open initially
86+
await expect(getListbox()).toBeVisible();
87+
88+
const options = await getOptions();
89+
await options[2].click();
90+
91+
await expect(options[2]).toHaveAttribute('aria-selected', 'true');
92+
expect(options[2].textContent()).toEqual(getValue());
93+
});
4394
});
4495

4596
test.describe('Keyboard Behavior', () => {
@@ -274,7 +325,6 @@ test.describe('Keyboard Behavior', () => {
274325
// should be open initially
275326
await expect(getListbox()).toBeVisible();
276327

277-
// third option highlighted
278328
const options = await getOptions();
279329
await expect(options[0]).toHaveAttribute('data-highlighted');
280330
await getTrigger().press('ArrowDown');
@@ -284,10 +334,9 @@ test.describe('Keyboard Behavior', () => {
284334
await expect(options[1]).toHaveAttribute('data-highlighted');
285335
});
286336

287-
test(`GIVEN an open hero select
288-
WHEN the listbox is closed with a chosen option
289-
AND the down arrow key is pressed
290-
THEN the data-highlighted option should not change on re-open`, async ({
337+
test(`GIVEN a hero select with a chosen option
338+
AND the down arrow key is pressed
339+
THEN the data-highlighted option should not change on re-open`, async ({
291340
page,
292341
}) => {
293342
const { getTrigger, getListbox, getOptions } = await setup(
@@ -311,9 +360,83 @@ test.describe('Keyboard Behavior', () => {
311360
await expect(options[1]).toHaveAttribute('data-highlighted');
312361
});
313362
});
363+
364+
test.describe('selecting options', () => {
365+
test(`GIVEN an open hero select
366+
WHEN an option has data-highlighted
367+
AND the Enter key is pressed
368+
THEN the listbox should be closed and aria-expanded false`, async ({
369+
page,
370+
}) => {
371+
const { getTrigger, getListbox, getOptions } = await setup(
372+
page,
373+
'select-hero-test',
374+
);
375+
376+
await getTrigger().focus();
377+
await getTrigger().press('Enter');
378+
// should be open initially
379+
await expect(getListbox()).toBeVisible();
380+
381+
const options = await getOptions();
382+
await expect(options[0]).toHaveAttribute('data-highlighted');
383+
await getTrigger().press('Enter');
384+
await expect(getListbox()).toBeHidden();
385+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
386+
});
387+
388+
test(`GIVEN an open hero select
389+
WHEN an option has data-highlighted
390+
AND the Enter key is pressed
391+
THEN option value should be the selected value
392+
AND should have an aria-selected of true`, async ({ page }) => {
393+
const { getTrigger, getListbox, getOptions, getValue } = await setup(
394+
page,
395+
'select-hero-test',
396+
);
397+
398+
await getTrigger().focus();
399+
await getTrigger().press('Enter');
400+
// should be open initially
401+
await expect(getListbox()).toBeVisible();
402+
403+
const options = await getOptions();
404+
await expect(options[0]).toHaveAttribute('data-highlighted');
405+
const optStr = await options[0].textContent();
406+
await getTrigger().press('Enter');
407+
408+
console.log(optStr);
409+
console.log(await getValue());
410+
expect(optStr).toEqual(await getValue());
411+
});
412+
413+
test(`GIVEN an open hero select
414+
WHEN an option has data-highlighted
415+
AND the Space key is pressed
416+
THEN the listbox should be closed and aria-expanded false`, async ({
417+
page,
418+
}) => {
419+
const { getTrigger, getListbox, getOptions } = await setup(
420+
page,
421+
'select-hero-test',
422+
);
423+
424+
await getTrigger().focus();
425+
await getTrigger().press('Space');
426+
// should be open initially
427+
await expect(getListbox()).toBeVisible();
428+
429+
// second option highlighted
430+
const options = await getOptions();
431+
await expect(options[0]).toHaveAttribute('data-highlighted');
432+
await getTrigger().press('Space');
433+
await expect(getListbox()).toBeHidden();
434+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
435+
});
436+
});
314437
});
315438

316-
test.describe('disabled', () => {
439+
test.describe('Disabled', () => {
317440
test(`GIVEN an open disabled select with the first option disabled
318441
WHEN clicking the disabled option
319442
It should have aria-disabled`, async ({ page }) => {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './select-listbox';
44
export * from './select-option';
55
export * from './select-popover';
66
export * from './select-trigger';
7+
export * from './select-value';

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { component$, type PropsOf, useContext, sync$, $ } from '@builder.io/qwik';
1+
import { component$, type PropsOf, useContext, sync$, $, Slot } from '@builder.io/qwik';
22
import SelectContextId from './select-context';
33
import { getNextEnabledOptionIndex, getPrevEnabledOptionIndex } from './utils';
44

@@ -77,6 +77,11 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
7777
options,
7878
);
7979
}
80+
81+
// select options
82+
if (e.key === 'Enter' || e.key === ' ') {
83+
context.selectedIndexSig.value = context.highlightedIndexSig.value;
84+
}
8085
}
8186
});
8287

@@ -90,7 +95,7 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
9095
data-closed={!context.isListboxOpenSig.value ? '' : undefined}
9196
aria-expanded={context.isListboxOpenSig.value}
9297
>
93-
{context.selectedOptionRef.value?.textContent ?? 'Select an option'}
98+
<Slot />
9499
</button>
95100
);
96101
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { component$, useContext, type PropsOf } from '@builder.io/qwik';
2+
3+
import SelectContextId from './select-context';
4+
5+
type SelectValueProps = PropsOf<'span'>;
6+
7+
export const SelectValue = component$((props: SelectValueProps) => {
8+
const context = useContext(SelectContextId);
9+
context;
10+
11+
return (
12+
<span data-value {...props}>
13+
{context.selectedOptionRef.value?.textContent ?? 'Select an option'}
14+
</span>
15+
);
16+
});

0 commit comments

Comments
 (0)