Skip to content

Commit 86ea5b9

Browse files
feat(select): get proper indexes, better test suite
1 parent 6a4eb32 commit 86ea5b9

File tree

4 files changed

+110
-8
lines changed

4 files changed

+110
-8
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
@@ -13,7 +13,7 @@ export default component$(() => {
1313
<SelectOption
1414
class="border-dashed border-blue-400 data-[highlighted]:border-2"
1515
key={user}
16-
disabled={index === 0 ? true : false}
16+
disabled={index === 0 || index === usersSig.value.length - 1 ? true : false}
1717
>
1818
{user}
1919
</SelectOption>

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

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,56 @@ test.describe('Keyboard Behavior', () => {
236236
await getTrigger().press('ArrowDown');
237237
await expect(options[1]).toHaveAttribute('data-highlighted');
238238
});
239+
240+
test(`GIVEN an open hero select
241+
WHEN the third option is highlighted and the up arrow key is pressed
242+
THEN the second option should have data-highlighted`, async ({ page }) => {
243+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
244+
245+
await getTrigger().focus();
246+
await getTrigger().press('Enter');
247+
// should be open initially
248+
await expect(getListbox()).toBeVisible();
249+
250+
// third option highlighted
251+
const options = await getOptions();
252+
await expect(options[0]).toHaveAttribute('data-highlighted');
253+
await getTrigger().press('ArrowDown');
254+
await getTrigger().press('ArrowDown');
255+
256+
await getTrigger().press('ArrowUp');
257+
await expect(options[1]).toHaveAttribute('data-highlighted');
258+
});
259+
260+
test(`GIVEN an open hero select
261+
WHEN the listbox is closed with a chosen option
262+
AND the down arrow key is pressed
263+
THEN the data-highlighted option should not change on re-open`, async ({
264+
page,
265+
}) => {
266+
const { getTrigger, getListbox, getOptions } = await setup(page, 'select-hero-test');
267+
268+
await getTrigger().focus();
269+
await getTrigger().press('Enter');
270+
// should be open initially
271+
await expect(getListbox()).toBeVisible();
272+
273+
// second option highlighted
274+
const options = await getOptions();
275+
await getTrigger().press('ArrowDown');
276+
await expect(options[1]).toHaveAttribute('data-highlighted');
277+
await getTrigger().press('Enter');
278+
await expect(getListbox()).toBeHidden();
279+
280+
await getTrigger().press('ArrowDown');
281+
await expect(options[1]).toHaveAttribute('data-highlighted');
282+
});
239283
});
240284

241285
test.describe('disabled', () => {
242286
test(`GIVEN an open hero select with the first option disabled
243287
WHEN clicking the disabled option
244-
It should be disabled`, async ({ page }) => {
288+
It should have aria-disabled`, async ({ page }) => {
245289
const { getTrigger, getListbox, getOptions } = await setup(
246290
page,
247291
'select-disabled-test',
@@ -258,7 +302,26 @@ test.describe('disabled', () => {
258302
await expect(options[0]).toBeDisabled();
259303
});
260304

261-
test(`GIVEN an open hero select by the enter key
305+
test(`GIVEN an open disable select with the first option disabled
306+
WHEN clicking the disabled option
307+
THEN the listbox should stay open`, async ({ page }) => {
308+
const { getTrigger, getListbox, getOptions } = await setup(
309+
page,
310+
'select-disabled-test',
311+
);
312+
313+
await getTrigger().focus();
314+
await getTrigger().press('Enter');
315+
// should be open initially
316+
await expect(getListbox()).toBeVisible();
317+
318+
const options = await getOptions();
319+
// eslint-disable-next-line playwright/no-force-option
320+
await options[0].click({ force: true });
321+
await expect(getListbox()).toBeVisible();
322+
});
323+
324+
test(`GIVEN an open disabled select
262325
WHEN first option is disabled
263326
THEN the second option should have data-highlighted`, async ({ page }) => {
264327
const { getTrigger, getListbox, getOptions } = await setup(
@@ -273,4 +336,21 @@ test.describe('disabled', () => {
273336
const options = await getOptions();
274337
await expect(options[1]).toHaveAttribute('data-highlighted');
275338
});
339+
340+
test(`GIVEN an open disabled select
341+
WHEN the last option is disabled and the end key is pressed
342+
THEN the second last index should have data-highlighted`, async ({ page }) => {
343+
const { getTrigger, getListbox, getOptions } = await setup(
344+
page,
345+
'select-disabled-test',
346+
);
347+
348+
await getTrigger().focus();
349+
await getTrigger().press('ArrowDown');
350+
// should be open initially
351+
await expect(getListbox()).toBeVisible();
352+
await getTrigger().press('End');
353+
const options = await getOptions();
354+
await expect(options[options.length - 2]).toHaveAttribute('data-highlighted');
355+
});
276356
});

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

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

55
export type OptionsType = {
66
element: HTMLLIElement;
@@ -49,27 +49,34 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
4949
}
5050

5151
if (e.key === 'Home') {
52-
context.highlightedIndexSig.value = 0;
52+
context.highlightedIndexSig.value = getNextEnabledOptionIndex(-1, options);
5353
}
5454

5555
if (e.key === 'End') {
56-
context.highlightedIndexSig.value = context.optionRefsArray.value.length - 1;
56+
const lastEnabledOptionIndex = getPrevEnabledOptionIndex(options.length, options);
57+
context.highlightedIndexSig.value = lastEnabledOptionIndex;
5758
}
5859

5960
/** When initially opening the listbox, we want to grab the first enabled option index */
6061
if (context.highlightedIndexSig.value === null) {
6162
context.highlightedIndexSig.value = getNextEnabledOptionIndex(-1, options);
62-
console.log(context.highlightedIndexSig.value);
6363
return;
6464
}
6565

66-
if (context.isListboxOpenSig.value) {
66+
if (context.isListboxOpenSig.value && !shouldOpen) {
6767
if (e.key === 'ArrowDown') {
6868
context.highlightedIndexSig.value = getNextEnabledOptionIndex(
6969
context.highlightedIndexSig.value,
7070
options,
7171
);
7272
}
73+
74+
if (e.key === 'ArrowUp') {
75+
context.highlightedIndexSig.value = getPrevEnabledOptionIndex(
76+
context.highlightedIndexSig.value,
77+
options,
78+
);
79+
}
7380
}
7481
});
7582

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,18 @@ export const getNextEnabledOptionIndex = (index: number, options: OptionsType) =
2020
}
2121
return (currentIndex + offset) % len;
2222
};
23+
24+
export const getPrevEnabledOptionIndex = (index: number, options: OptionsType) => {
25+
let offset = 1;
26+
let currentIndex = index;
27+
const opts = options;
28+
const len = opts.length;
29+
while (opts[(currentIndex - offset + len) % len]?.isDisabled) {
30+
offset++;
31+
if (currentIndex - offset < 0) {
32+
currentIndex = len - 1;
33+
offset = 0;
34+
}
35+
}
36+
return (currentIndex - offset + len) % len;
37+
};

0 commit comments

Comments
 (0)