Skip to content

Commit 427b453

Browse files
authored
Merge pull request #372 from shiroinegai/refactor-select
refactor(select): component restructure & update event handlers
2 parents debdbfe + 98f26d9 commit 427b453

21 files changed

+744
-469
lines changed

apps/website/src/routes/docs/headless/(components)/select/examples.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ export const Example01 = component$(() => {
3737
</SelectTrigger>
3838
<SelectListBox class="bg-[#1f2532] border-[#7d95b3] mt-2 border-[1px] rounded-md text-white">
3939
<SelectOption
40-
value="🚀 Qwik"
40+
optionValue="🚀 Qwik"
4141
class="p-4 hover:bg-[#496080] focus:bg-[#496080]"
42-
/>
42+
>
43+
🚀 Qwik
44+
</SelectOption>
4345
<SelectGroup class="p-4">
4446
<SelectLabel class="p-4">Fruits</SelectLabel>
4547
{[
@@ -51,10 +53,12 @@ export const Example01 = component$(() => {
5153
return (
5254
<SelectOption
5355
key={option.value}
54-
value={option.value}
56+
optionValue={option.value}
5557
disabled={option.disabled}
5658
class="hover:bg-[#496080] focus:bg-[#496080] aria-disabled:text-red-500 aria-disabled:cursor-not-allowed rounded-sm p-4"
57-
/>
59+
>
60+
{option.value}
61+
</SelectOption>
5862
);
5963
})}
6064
</SelectGroup>
@@ -91,9 +95,15 @@ export const Example02 = component$(() => {
9195
</SelectMarker>
9296
</SelectTrigger>
9397
<SelectListBox class="bg-slate-100 dark:bg-gray-700 border-slate-200 dark:border-gray-600 border-[1px]">
94-
<SelectOption value="Orders" class="p-4" />
95-
<SelectOption value="Settings" class="p-4" />
96-
<SelectOption value="Contact us" class="p-4" />
98+
<SelectOption optionValue="Orders" class="p-4">
99+
Orders
100+
</SelectOption>
101+
<SelectOption optionValue="Settings" class="p-4">
102+
Settings
103+
</SelectOption>
104+
<SelectOption optionValue="Contact us" class="p-4">
105+
Contact us
106+
</SelectOption>
97107
</SelectListBox>
98108
</SelectRoot>
99109
</div>

apps/website/src/routes/docs/headless/(components)/select/index.mdx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
4040
</SelectMarker>
4141
</SelectTrigger>
4242
<SelectListBox class="bg-slate-100 dark:bg-gray-700 border-slate-200 dark:border-gray-600 mt-2 border-[1px] rounded-md">
43-
<SelectOption value="🚀 Qwik" class="p-4" />
43+
<SelectOption value="🚀 Qwik" class="p-4">🚀 Qwik</SelectOption>
4444
<SelectGroup class="p-4 ">
4545
<SelectLabel class="p-4">Fruits</SelectLabel>
4646
{[
@@ -55,7 +55,7 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
5555
value={option.value}
5656
disabled={option.disabled}
5757
class="aria-disabled:text-red-500 aria-disabled:cursor-not-allowed hover:bg-slate-200 rounded-sm dark:hover:bg-gray-600 p-4"
58-
/>
58+
>{option.value}</SelectOption>
5959
);
6060
})}
6161
</SelectGroup>
@@ -81,9 +81,9 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
8181
<SelectListBox>
8282
<SelectGroup>
8383
<SelectLabel>Options</SelectLabel>
84-
<SelectOption/>
85-
<SelectOption/>
86-
<SelectOption/>
84+
<SelectOption>Value</SelectOption>
85+
<SelectOption>Value</SelectOption>
86+
<SelectOption>Value</SelectOption>
8787
</SelectGroup>
8888
</SelectListBox>
8989
</SelectRoot>
@@ -116,9 +116,15 @@ import { AlphaBanner } from '../../../_components/alpha-banner/alpha-banner';
116116
</SelectMarker>
117117
</SelectTrigger>
118118
<SelectListBox class="bg-slate-100 dark:bg-gray-700 border-slate-200 dark:border-gray-600 border-[1px]">
119-
<SelectOption value="Orders" class="p-4" />
120-
<SelectOption value="Settings" class="p-4" />
121-
<SelectOption value="Contact us" class="p-4" />
119+
<SelectOption optionValue="Orders" class="p-4">
120+
Orders
121+
</SelectOption>
122+
<SelectOption optionValue="Settings" class="p-4">
123+
Settings
124+
</SelectOption>
125+
<SelectOption optionValue="Contact us" class="p-4">
126+
Contact us
127+
</SelectOption>
122128
</SelectListBox>
123129
</SelectRoot>
124130
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export * from './select-context-id';
2+
export * from './select-context.type';
3+
export * from './select-root';
4+
export * from './select-trigger';
5+
export * from './select-value';
6+
export * from './select-marker';
7+
export * from './select-listbox';
8+
export * from './select-label';
9+
export * from './select-group';
10+
export * from './select-option';
11+
export * from './select-native-select';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createContextId } from '@builder.io/qwik';
2+
import { SelectContext } from './select-context.type';
3+
4+
const SelectContextId = createContextId<SelectContext>('select-root');
5+
6+
export default SelectContextId;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Signal } from '@builder.io/qwik';
2+
3+
export type SelectContext = {
4+
optionsStore: HTMLElement[];
5+
selectedOptionSig: Signal<string | undefined>;
6+
isOpenSig: Signal<boolean>;
7+
triggerRefSig: Signal<HTMLElement | undefined>;
8+
listBoxRefSig: Signal<HTMLElement | undefined>;
9+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { component$, QwikIntrinsicElements, Slot } from '@builder.io/qwik';
2+
3+
export type SelectGroupProps = {
4+
disabled?: boolean;
5+
} & QwikIntrinsicElements['div'];
6+
7+
export const SelectGroup = component$(({ disabled, ...props }: SelectGroupProps) => {
8+
return (
9+
<div role="group" aria-disabled={disabled} {...props}>
10+
<Slot />
11+
</div>
12+
);
13+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { component$, QwikIntrinsicElements, Slot } from '@builder.io/qwik';
2+
3+
export type SelectLabelProps = QwikIntrinsicElements['label'];
4+
5+
export const SelectLabel = component$((props: SelectLabelProps) => {
6+
return (
7+
<label {...props}>
8+
<Slot />
9+
</label>
10+
);
11+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
component$,
3+
QwikIntrinsicElements,
4+
Slot,
5+
useContext,
6+
useSignal,
7+
useVisibleTask$
8+
} from '@builder.io/qwik';
9+
import SelectContextId from './select-context-id';
10+
11+
export type SelectListBoxProps = QwikIntrinsicElements['ul'];
12+
13+
export const SelectListBox = component$((props: SelectListBoxProps) => {
14+
const listBoxRef = useSignal<HTMLElement>();
15+
const selectContext = useContext(SelectContextId);
16+
selectContext.listBoxRefSig = listBoxRef;
17+
18+
useVisibleTask$(function setKeyHandler({ cleanup }) {
19+
function keyHandler(e: KeyboardEvent) {
20+
const availableOptions = selectContext.optionsStore.filter(
21+
(option) => !(option?.getAttribute('aria-disabled') === 'true')
22+
);
23+
const target = e.target as HTMLElement;
24+
const currentIndex = availableOptions.indexOf(target);
25+
26+
if (
27+
e.key === 'ArrowDown' ||
28+
e.key === 'ArrowUp' ||
29+
e.key === 'Home' ||
30+
e.key === 'End' ||
31+
e.key === ' '
32+
) {
33+
e.preventDefault();
34+
}
35+
36+
if (e.key === 'ArrowDown') {
37+
if (currentIndex === availableOptions.length - 1) {
38+
availableOptions[0]?.focus();
39+
} else {
40+
availableOptions[currentIndex + 1]?.focus();
41+
}
42+
}
43+
44+
if (e.key === 'ArrowUp') {
45+
if (currentIndex <= 0) {
46+
availableOptions[availableOptions.length - 1]?.focus();
47+
} else {
48+
availableOptions[currentIndex - 1]?.focus();
49+
}
50+
}
51+
}
52+
listBoxRef.value?.addEventListener('keydown', keyHandler);
53+
cleanup(() => {
54+
listBoxRef.value?.removeEventListener('keydown', keyHandler);
55+
});
56+
});
57+
58+
return (
59+
<ul
60+
ref={listBoxRef}
61+
role="listbox"
62+
tabIndex={0}
63+
style={`
64+
display: ${selectContext.isOpenSig.value ? 'block' : 'none'};
65+
position: absolute;
66+
z-index: 1;
67+
${props.style}
68+
`}
69+
class={props.class}
70+
>
71+
<Slot />
72+
</ul>
73+
);
74+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { component$, QwikIntrinsicElements, Slot } from '@builder.io/qwik';
2+
3+
export type SelectMarkerProps = QwikIntrinsicElements['span'];
4+
5+
export const SelectMarker = component$((props: SelectMarkerProps) => {
6+
return (
7+
<span aria-hidden="true" {...props}>
8+
<Slot />
9+
</span>
10+
);
11+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { $, component$, QwikIntrinsicElements, useSignal } from '@builder.io/qwik';
2+
import SelectContextId from './select-context-id';
3+
import { useContext } from '@builder.io/qwik';
4+
import { useOn } from '@builder.io/qwik';
5+
import { useVisibleTask$ } from '@builder.io/qwik';
6+
7+
export const NativeSelect = component$(
8+
({ ...props }: QwikIntrinsicElements['select']) => {
9+
const selectContext = useContext(SelectContextId);
10+
const ref = useSignal<HTMLElement>();
11+
12+
useVisibleTask$(function populateNativeSelect({ track }) {
13+
const options = track(() => selectContext.optionsStore);
14+
15+
options.length > 0 &&
16+
options.map((option) => {
17+
const optionElement = document.createElement('option');
18+
const optionValue = option.dataset.optionValue;
19+
optionElement.setAttribute('value', optionValue!);
20+
ref.value?.append(optionElement);
21+
});
22+
});
23+
24+
useOn(
25+
'change',
26+
$((e) => {
27+
const target = e.target as HTMLSelectElement;
28+
target.value = selectContext.selectedOptionSig.value!;
29+
})
30+
);
31+
32+
return (
33+
<select
34+
ref={ref}
35+
required
36+
aria-hidden
37+
tabIndex={-1}
38+
bind:value={selectContext.selectedOptionSig}
39+
{...props}
40+
>
41+
<option value="" />
42+
</select>
43+
);
44+
}
45+
);

0 commit comments

Comments
 (0)