Skip to content

Commit 7e0db1a

Browse files
refactor: move helper functions inside of useSelect hook
1 parent 1ba2e6b commit 7e0db1a

File tree

4 files changed

+79
-77
lines changed

4 files changed

+79
-77
lines changed

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

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

65
type SelectTriggerProps = PropsOf<'button'>;
76
export const SelectTrigger = component$<SelectTriggerProps>((props) => {
87
const context = useContext(SelectContextId);
8+
const { getNextEnabledOptionIndex, getPrevEnabledOptionIndex } = useSelect();
99
const openKeys = ['ArrowUp', 'ArrowDown'];
1010
const closedKeys = [`Escape`];
1111

@@ -32,7 +32,7 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
3232
}
3333
});
3434

35-
const handleKeyDown$ = $((e: KeyboardEvent) => {
35+
const handleKeyDown$ = $(async (e: KeyboardEvent) => {
3636
typeahead$(e.key);
3737
const shouldOpen = !context.isListboxOpenSig.value && openKeys.includes(e.key);
3838
const shouldClose = context.isListboxOpenSig.value && closedKeys.includes(e.key);
@@ -47,15 +47,15 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
4747
}
4848

4949
if (e.key === 'Home') {
50-
context.highlightedIndexSig.value = getNextEnabledOptionIndex(
50+
context.highlightedIndexSig.value = await getNextEnabledOptionIndex(
5151
-1,
5252
context.optionsSig.value,
5353
context.loop,
5454
);
5555
}
5656

5757
if (e.key === 'End') {
58-
const lastEnabledOptionIndex = getPrevEnabledOptionIndex(
58+
const lastEnabledOptionIndex = await getPrevEnabledOptionIndex(
5959
context.optionsSig.value.length,
6060
context.optionsSig.value,
6161
context.loop,
@@ -65,7 +65,7 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
6565

6666
if (!context.isListboxOpenSig.value) {
6767
if (e.key === 'ArrowRight' && context.highlightedIndexSig.value === null) {
68-
context.selectedIndexSig.value = getNextEnabledOptionIndex(
68+
context.selectedIndexSig.value = await getNextEnabledOptionIndex(
6969
-1,
7070
context.optionsSig.value,
7171
context.loop,
@@ -76,17 +76,19 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
7676
}
7777

7878
if (e.key === 'ArrowRight' && context.highlightedIndexSig.value !== null) {
79-
context.selectedIndexSig.value = getNextEnabledOptionIndex(
79+
context.selectedIndexSig.value = await getNextEnabledOptionIndex(
8080
context.selectedIndexSig.value!,
8181
context.optionsSig.value,
8282
context.loop,
8383
);
8484

85+
console.log('selectedIndex', context.selectedIndexSig.value);
86+
8587
context.highlightedIndexSig.value = context.selectedIndexSig.value;
8688
}
8789

8890
if (e.key === 'ArrowLeft' && context.highlightedIndexSig.value === null) {
89-
context.selectedIndexSig.value = getPrevEnabledOptionIndex(
91+
context.selectedIndexSig.value = await getPrevEnabledOptionIndex(
9092
context.optionsSig.value.length,
9193
context.optionsSig.value,
9294
context.loop,
@@ -97,7 +99,7 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
9799
}
98100

99101
if (e.key === 'ArrowLeft' && context.highlightedIndexSig.value !== null) {
100-
context.selectedIndexSig.value = getPrevEnabledOptionIndex(
102+
context.selectedIndexSig.value = await getPrevEnabledOptionIndex(
101103
context.highlightedIndexSig.value,
102104
context.optionsSig.value,
103105
context.loop,
@@ -109,7 +111,7 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
109111

110112
/** When initially opening the listbox, we want to grab the first enabled option index */
111113
if (context.highlightedIndexSig.value === null) {
112-
context.highlightedIndexSig.value = getNextEnabledOptionIndex(
114+
context.highlightedIndexSig.value = await getNextEnabledOptionIndex(
113115
-1,
114116
context.optionsSig.value,
115117
context.loop,
@@ -128,15 +130,15 @@ export const SelectTrigger = component$<SelectTriggerProps>((props) => {
128130
}
129131

130132
if (e.key === 'ArrowDown') {
131-
context.highlightedIndexSig.value = getNextEnabledOptionIndex(
133+
context.highlightedIndexSig.value = await getNextEnabledOptionIndex(
132134
context.highlightedIndexSig.value,
133135
context.optionsSig.value,
134136
context.loop,
135137
);
136138
}
137139

138140
if (e.key === 'ArrowUp') {
139-
context.highlightedIndexSig.value = getPrevEnabledOptionIndex(
141+
context.highlightedIndexSig.value = await getPrevEnabledOptionIndex(
140142
context.highlightedIndexSig.value,
141143
context.optionsSig.value,
142144
context.loop,

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ test.describe('Keyboard Behavior', () => {
444444
await expect(getValueElement()).toHaveText(expectedValue!);
445445
});
446446

447-
test(`GIVEN an already selected item
447+
test(`GIVEN no selected item and a placeholder
448448
WHEN pressing the right arrow key once
449449
THEN the first enabled option should be selected and have aria-selected`, async ({
450450
page,
@@ -463,24 +463,23 @@ test.describe('Keyboard Behavior', () => {
463463
await expect(getHiddenOptionAt(0)).toHaveAttribute('data-highlighted');
464464
});
465465

466-
test(`GIVEN an already selected item
466+
test(`GIVEN no selected item and a placeholder
467467
WHEN pressing the right arrow key twice
468468
THEN the first enabled option should be selected and have aria-selected`, async ({
469469
page,
470470
}) => {
471-
const { getTrigger, getHiddenOptionAt, getValueElement } = await setup(
472-
page,
473-
'hero',
474-
);
471+
const { driver: d } = await setup(page, 'hero');
475472

476-
const secondItemValue = await getHiddenOptionAt(1).textContent();
477-
await getTrigger().focus();
478-
await getTrigger().press('ArrowRight');
479-
await getTrigger().press('ArrowRight');
473+
const firstItemValue = await d.getHiddenOptionAt(0).textContent();
474+
const secondItemValue = await d.getHiddenOptionAt(1).textContent();
480475

481-
expect(getValueElement()).toHaveText(secondItemValue!);
482-
await expect(getHiddenOptionAt(1)).toHaveAttribute('aria-selected', 'true');
483-
await expect(getHiddenOptionAt(1)).toHaveAttribute('data-highlighted');
476+
await d.getTrigger().press('ArrowRight');
477+
await expect(d.getValueElement()).toHaveText(firstItemValue!);
478+
await d.getTrigger().press('ArrowRight');
479+
480+
await expect(d.getValueElement()).toHaveText(secondItemValue!);
481+
await expect(d.getHiddenOptionAt(1)).toHaveAttribute('aria-selected', 'true');
482+
await expect(d.getHiddenOptionAt(1)).toHaveAttribute('data-highlighted');
484483
});
485484

486485
test(`GIVEN the second item is selected

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
import { useContext, useSignal, $, useComputed$ } from '@builder.io/qwik';
22
import SelectContextId from './select-context';
3+
import { Opt } from './select-inline';
4+
5+
/**
6+
* Helper functions go inside of hooks.
7+
* This is because outside of the component$ boundary Qwik core wakes up.
8+
*/
9+
export function useSelect() {
10+
const getNextEnabledOptionIndex = $((index: number, options: Opt[], loop: boolean) => {
11+
let offset = 1;
12+
const len = options.length;
13+
14+
if (!loop && index + 1 >= len) {
15+
return index;
16+
}
17+
18+
while (offset < len) {
19+
const nextIndex = (index + offset) % len;
20+
if (!options[nextIndex].isDisabled) {
21+
return nextIndex;
22+
}
23+
offset++;
24+
if (!loop && index + offset >= len) {
25+
break;
26+
}
27+
}
28+
29+
return index;
30+
});
31+
32+
const getPrevEnabledOptionIndex = $((index: number, options: Opt[], loop: boolean) => {
33+
let offset = 1;
34+
const len = options.length;
35+
36+
if (!loop && index - 1 < 0) {
37+
return index;
38+
}
39+
40+
while (offset <= len) {
41+
const prevIndex = (index - offset + len) % len;
42+
if (!options[prevIndex].isDisabled) {
43+
return prevIndex;
44+
}
45+
offset++;
46+
if (!loop && index - offset < 0) {
47+
break;
48+
}
49+
}
50+
51+
return index;
52+
});
53+
54+
return { getNextEnabledOptionIndex, getPrevEnabledOptionIndex };
55+
}
356

457
export function useTypeahead() {
558
const context = useContext(SelectContextId);

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

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,5 @@
11
import { type Opt } from './select-inline';
22

3-
export const getNextEnabledOptionIndex = (
4-
index: number,
5-
options: Opt[],
6-
loop: boolean,
7-
) => {
8-
let offset = 1;
9-
const len = options.length;
10-
11-
if (!loop && index + 1 >= len) {
12-
return index;
13-
}
14-
15-
while (offset < len) {
16-
const nextIndex = (index + offset) % len;
17-
if (!options[nextIndex].isDisabled) {
18-
return nextIndex;
19-
}
20-
offset++;
21-
if (!loop && index + offset >= len) {
22-
break;
23-
}
24-
}
25-
26-
return index;
27-
};
28-
29-
export const getPrevEnabledOptionIndex = (
30-
index: number,
31-
options: Opt[],
32-
loop: boolean,
33-
) => {
34-
let offset = 1;
35-
const len = options.length;
36-
37-
if (!loop && index - 1 < 0) {
38-
return index;
39-
}
40-
41-
while (offset <= len) {
42-
const prevIndex = (index - offset + len) % len;
43-
if (!options[prevIndex].isDisabled) {
44-
return prevIndex;
45-
}
46-
offset++;
47-
if (!loop && index - offset < 0) {
48-
break;
49-
}
50-
}
51-
52-
return index;
53-
};
54-
553
export const getActiveDescendant = (index: number, options: Opt[], localId: string) => {
564
const option = options[index];
575

0 commit comments

Comments
 (0)