Skip to content

Commit 4b0f720

Browse files
feat(select): nate and I struggling on down arrow key
1 parent f86fa5c commit 4b0f720

File tree

8 files changed

+319
-26
lines changed

8 files changed

+319
-26
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ export default component$(() => {
88
<Select class="relative min-w-40">
99
<SelectTrigger class="w-full border-2 border-dashed border-red-400" />
1010
<SelectListbox class="absolute w-full border-2 border-dashed border-green-400 bg-slate-900 p-2">
11-
<SelectOption class="border-2 border-dashed border-blue-400">
11+
{/* <SelectOption
12+
disabled
13+
class="border-dashed border-blue-400 data-[highlighted]:border-2"
14+
>
1215
first option!
13-
</SelectOption>
16+
</SelectOption> */}
1417
{usersSig.value.map((user) => (
15-
<SelectOption key={user}>{user}</SelectOption>
18+
<SelectOption
19+
class="border-dashed border-blue-400 data-[highlighted]:border-2"
20+
key={user}
21+
>
22+
{user}
23+
</SelectOption>
1624
))}
1725
</SelectListbox>
1826
</Select>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Locator, Page } from '@playwright/test';
22

33
export type DriverLocator = Locator | Page;
44

5-
export function selectTestDriver<T extends DriverLocator>(locator: T) {
5+
export function createTestDriver<T extends DriverLocator>(locator: T) {
66
const getRoot = () => {
77
return locator.getByRole('combobox');
88
};
Lines changed: 219 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,239 @@
1-
import { test, expect } from '@playwright/test';
2-
import { selectTestDriver } from './select.driver';
3-
4-
test.beforeEach(async ({ page }) => {
1+
import { test, expect, type Page } from '@playwright/test';
2+
import { createTestDriver } from './select.driver';
3+
async function setup(page: Page, selector: string) {
54
await page.goto('/docs/headless/select');
6-
});
5+
6+
const driver = createTestDriver(page.getByTestId(selector));
7+
8+
const { getListbox, getTrigger, getOptions } = driver;
9+
10+
return {
11+
driver,
12+
getListbox,
13+
getTrigger,
14+
getOptions,
15+
};
16+
}
717

818
test.describe('critical functionality', () => {
9-
test(`GIVEN a basic select
19+
test(`GIVEN the hero select
1020
WHEN clicking on the trigger
11-
THEN open up the listbox
12-
AND aria-expanded should be true`, async ({ page }) => {
13-
const testDriver = selectTestDriver(page.getByTestId('select-hero-test'));
14-
15-
const { getListbox, getTrigger } = testDriver;
21+
THEN open up the listbox AND aria-expanded should be true`, async ({ page }) => {
22+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
1623

1724
await getTrigger().click();
1825

1926
await expect(getListbox()).toBeVisible();
2027
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'true');
2128
});
2229

23-
test(`GIVEN a basic select
30+
test(`GIVEN the hero select
31+
WHEN focusing the trigger and hitting enter
32+
THEN open up the listbox AND aria-expanded should be true`, async ({ page }) => {
33+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
34+
35+
await getTrigger().focus();
36+
await getTrigger().press('Enter');
37+
38+
await expect(getListbox()).toBeVisible();
39+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'true');
40+
});
41+
42+
test(`GIVEN a hero select with an open listbox
43+
WHEN the trigger is clicked
44+
THEN close the listbox AND aria-expanded should be false`, async ({ page }) => {
45+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
46+
47+
await getTrigger().click();
48+
// should be open initially
49+
await expect(getListbox()).toBeVisible();
50+
51+
await getTrigger().click();
52+
await expect(getListbox()).toBeHidden();
53+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
54+
});
55+
56+
test(`GIVEN a hero select with an open listbox
2457
WHEN focusing the trigger and hitting enter
25-
THEN open up the listbox
26-
AND aria-expanded should be true`, async ({ page }) => {
27-
const testDriver = selectTestDriver(page.getByTestId('select-hero-test'));
58+
THEN close the listbox AND aria-expanded should be false`, async ({ page }) => {
59+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
60+
61+
await getTrigger().click();
62+
// should be open initially
63+
await expect(getListbox()).toBeVisible();
2864

29-
const { getListbox, getTrigger } = testDriver;
65+
await getTrigger().focus();
66+
await getTrigger().press('Enter');
67+
await expect(getListbox()).toBeHidden();
68+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
69+
});
70+
});
71+
72+
test.describe('Keyboard Behavior', () => {
73+
test(`GIVEN a hero select
74+
WHEN pressing the space key
75+
THEN open up the listbox AND aria-expanded should be true`, async ({ page }) => {
76+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
3077

3178
await getTrigger().focus();
32-
await page.keyboard.press('Enter');
79+
await getTrigger().press('Space');
3380

3481
await expect(getListbox()).toBeVisible();
3582
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'true');
3683
});
84+
85+
test(`GIVEN a hero select with an open listbox
86+
WHEN pressing the space key
87+
THEN close listbox AND aria-expanded should be false`, async ({ page }) => {
88+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
89+
90+
await getTrigger().focus();
91+
await getTrigger().press('Space');
92+
// should be open initially
93+
await expect(getListbox()).toBeVisible();
94+
95+
await getTrigger().focus();
96+
await getTrigger().press('Space');
97+
await expect(getListbox()).toBeHidden();
98+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
99+
});
100+
101+
test(`GIVEN a hero select
102+
WHEN pressing the down arrow key
103+
THEN open up the listbox AND aria-expanded should be true`, async ({ page }) => {
104+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
105+
106+
await getTrigger().focus();
107+
await getTrigger().press('ArrowDown');
108+
await expect(getListbox()).toBeVisible();
109+
});
110+
111+
test(`GIVEN a hero select with an opened listbox
112+
WHEN pressing the escape key
113+
THEN the listbox should close
114+
AND aria-expanded should be false`, async ({ page }) => {
115+
const { getTrigger, getListbox } = await setup(page, 'select-hero-test');
116+
117+
await getTrigger().click();
118+
// should be open initially
119+
await expect(getListbox()).toBeVisible();
120+
121+
await getTrigger().focus();
122+
await getTrigger().press('Escape');
123+
await expect(getListbox()).toBeHidden();
124+
await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false');
125+
});
126+
127+
test(`GIVEN a hero select
128+
WHEN pressing the down arrow key
129+
THEN open up the listbox
130+
AND the first option should have data-highlighted`, async ({ page }) => {
131+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
132+
133+
await getTrigger().focus();
134+
await getTrigger().press('ArrowDown');
135+
await expect(getListbox()).toBeVisible();
136+
137+
const options = await getOptions();
138+
await expect(options[0]).toHaveAttribute('data-highlighted');
139+
});
140+
141+
test(`GIVEN a hero select
142+
WHEN pressing the enter key
143+
THEN open up the listbox
144+
AND the first option should have data-highlighted`, async ({ page }) => {
145+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
146+
147+
await getTrigger().focus();
148+
await getTrigger().press('Enter');
149+
await expect(getListbox()).toBeVisible();
150+
151+
const options = await getOptions();
152+
await expect(options[0]).toHaveAttribute('data-highlighted');
153+
});
154+
155+
test(`GIVEN a hero select
156+
WHEN pressing the space key
157+
THEN open up the listbox
158+
AND the first option should have data-highlighted`, async ({ page }) => {
159+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
160+
161+
await getTrigger().focus();
162+
await getTrigger().press('Space');
163+
await expect(getListbox()).toBeVisible();
164+
165+
const options = await getOptions();
166+
await expect(options[0]).toHaveAttribute('data-highlighted');
167+
});
168+
169+
test(`GIVEN a hero select
170+
WHEN pressing the up arrow
171+
THEN open up the listbox
172+
AND the first option should have data-highlighted`, async ({ page }) => {
173+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
174+
175+
await getTrigger().focus();
176+
await getTrigger().press('ArrowUp');
177+
await expect(getListbox()).toBeVisible();
178+
179+
const options = await getOptions();
180+
await expect(options[0]).toHaveAttribute('data-highlighted');
181+
});
182+
183+
test(`GIVEN an open hero select
184+
WHEN pressing the end key
185+
THEN the last option should have data-highlighted`, async ({ page }) => {
186+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
187+
188+
await getTrigger().click();
189+
// should be open initially
190+
await expect(getListbox()).toBeVisible();
191+
192+
await getTrigger().focus();
193+
await getTrigger().press('End');
194+
195+
const options = await getOptions();
196+
const lastIndex = options.length - 1;
197+
await expect(options[lastIndex]).toHaveAttribute('data-highlighted');
198+
});
199+
200+
test(`GIVEN an open hero select
201+
WHEN pressing the home key after the end key
202+
THEN the first option should have data-highlighted`, async ({ page }) => {
203+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
204+
205+
await getTrigger().click();
206+
// should be open initially
207+
await expect(getListbox()).toBeVisible();
208+
209+
// to last index
210+
await getTrigger().focus();
211+
await getTrigger().press('End');
212+
const options = await getOptions();
213+
const lastIndex = options.length - 1;
214+
await expect(options[lastIndex]).toHaveAttribute('data-highlighted');
215+
216+
// to first index
217+
await getTrigger().press('Home');
218+
await expect(options[0]).toHaveAttribute('data-highlighted');
219+
});
220+
221+
test(`GIVEN an open hero select
222+
WHEN the first option is highlighted and the down arrow key is pressed
223+
THEN the second option should have data-highlighted`, async ({ page }) => {
224+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
225+
226+
await getTrigger().focus();
227+
await getTrigger().press('Enter');
228+
// should be open initially
229+
await expect(getListbox()).toBeVisible();
230+
231+
// first index highlighted
232+
const options = await getOptions();
233+
await expect(options[0]).toHaveAttribute('data-highlighted');
234+
235+
await getTrigger().focus();
236+
await getTrigger().press('ArrowDown');
237+
await expect(options[1]).toHaveAttribute('data-highlighted');
238+
});
37239
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"changeset": "changeset",
1616
"changeset.version": "changeset version && pnpm install --no-frozen-lockfile && git add --all",
1717
"dev": "nx serve website",
18-
"playwright": "nx e2e website",
18+
"test.docs": "nx e2e website",
1919
"format.fix": "pretty-quick --staged",
2020
"link.dist": "cd dist/packages/kit-headless && pnpm link --global",
2121
"lint": "nx affected:lint",

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ What do they all have in common? How do people use them? What are the most impor
1313
## Anatomy:
1414

1515
<Select>
16-
<SelectTrigger />
16+
<SelectTrigger>
17+
<SelectValue>
18+
</SelectTrigger>
1719
<SelectPopover>
1820
<SelectListbox>
1921
<SelectOption />
@@ -40,6 +42,10 @@ What do they all have in common? How do people use them? What are the most impor
4042

4143
### State
4244

45+
name: value
46+
type: string
47+
description: uncontrolled select value
48+
4349
name: bind:value
4450
type: Signal
4551
description: controlled selected value, manages the selected option.
@@ -82,7 +88,7 @@ What do they all have in common? How do people use them? What are the most impor
8288

8389
## Keyboard Interactions:
8490

85-
key: Space;
91+
key: Space
8692
description: Pressing space opens the select menu and highlights the chosen option. If an option is highlighted, pressing space selects it.
8793

8894
key: Enter

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@builder.io/qwik';
1010
import SelectContextId from './select-context';
1111

12-
type SelectOptionProps = PropsOf<'li'> & {
12+
export type SelectOptionProps = PropsOf<'li'> & {
1313
index?: number;
1414
disabled?: boolean;
1515
};

0 commit comments

Comments
 (0)