Skip to content

Commit d1b70de

Browse files
authored
avoid use of useClientEffect in Headless Select (#165)
1 parent 60a0849 commit d1b70de

File tree

1 file changed

+27
-56
lines changed

1 file changed

+27
-56
lines changed

packages/headless/src/components/select/select.tsx

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ import {
44
useContext,
55
useContextProvider,
66
Slot,
7-
useClientEffect$,
87
useSignal,
98
Signal,
109
$,
11-
QRL,
1210
useOnWindow,
1311
useStore,
12+
useTask$,
1413
} from '@builder.io/qwik';
1514
import { computePosition, flip } from '@floating-ui/dom';
1615

1716
interface SelectRootContextService {
18-
options: HTMLElement[];
17+
options: Signal<HTMLElement | undefined>[];
1918
selectedOption: Signal<string>;
2019
isExpanded: Signal<boolean>;
21-
setTriggerRef$: QRL<(ref: Signal<HTMLElement | undefined>) => void>;
22-
setListBoxRef$: QRL<(ref: Signal<HTMLElement | undefined>) => void>;
20+
triggerRef: Signal<HTMLElement | undefined>;
21+
listBoxRef: Signal<HTMLElement | undefined>;
2322
}
2423

25-
export const selectContext = createContext<SelectRootContextService>('select-root');
24+
export const selectContext =
25+
createContext<SelectRootContextService>('select-root');
2626

2727
interface StyleProps {
2828
class?: string;
@@ -35,30 +35,17 @@ interface RootProps extends StyleProps {
3535

3636
const Root = component$(({ defaultValue, ...props }: RootProps) => {
3737
const options = useStore([]);
38-
3938
const selectedOption = useSignal(defaultValue ? defaultValue : '');
4039
const isExpanded = useSignal(false);
41-
4240
const triggerRef = useSignal<HTMLElement>();
43-
const setTriggerRef$ = $((ref: Signal<HTMLElement | undefined>) => {
44-
if (ref) {
45-
triggerRef.value = ref.value;
46-
}
47-
});
48-
4941
const listBoxRef = useSignal<HTMLElement>();
50-
const setListBoxRef$ = $((ref: Signal<HTMLElement | undefined>) => {
51-
if (ref) {
52-
listBoxRef.value = ref.value;
53-
}
54-
});
5542

5643
const contextService: SelectRootContextService = {
5744
options,
5845
selectedOption,
5946
isExpanded,
60-
setTriggerRef$,
61-
setListBoxRef$,
47+
triggerRef,
48+
listBoxRef,
6249
};
6350

6451
useContextProvider(selectContext, contextService);
@@ -77,9 +64,9 @@ const Root = component$(({ defaultValue, ...props }: RootProps) => {
7764
}
7865
);
7966

80-
useClientEffect$(async ({ track }) => {
81-
const trigger = track(() => triggerRef.value);
82-
const listBox = track(() => listBoxRef.value);
67+
useTask$(async ({ track }) => {
68+
const trigger = track(() => contextService.triggerRef.value);
69+
const listBox = track(() => contextService.listBoxRef.value);
8370
const expanded = track(() => isExpanded.value);
8471

8572
if (expanded && trigger && listBox) {
@@ -101,7 +88,7 @@ const Root = component$(({ defaultValue, ...props }: RootProps) => {
10188
const target = e.target as HTMLElement;
10289
if (
10390
contextService.isExpanded.value === true &&
104-
e.target !== triggerRef.value &&
91+
e.target !== contextService.triggerRef.value &&
10592
target.getAttribute('role') !== 'option' &&
10693
target.nodeName !== 'LABEL'
10794
) {
@@ -131,10 +118,7 @@ interface TriggerProps extends StyleProps {
131118
const Trigger = component$(({ disabled, ...props }: TriggerProps) => {
132119
const ref = useSignal<HTMLElement>();
133120
const contextService = useContext(selectContext);
134-
135-
useClientEffect$(() => {
136-
contextService.setTriggerRef$(ref);
137-
});
121+
contextService.triggerRef = ref;
138122

139123
return (
140124
<button
@@ -177,15 +161,7 @@ const Marker = component$(({ ...props }: StyleProps) => {
177161
const ListBox = component$(({ ...props }: StyleProps) => {
178162
const ref = useSignal<HTMLElement>();
179163
const contextService = useContext(selectContext);
180-
181-
useClientEffect$(() => {
182-
contextService.setListBoxRef$(ref);
183-
const options = ref.value?.querySelectorAll<HTMLElement>('[role="option"]');
184-
if (options?.length) {
185-
options.forEach((option) => contextService.options.push(option));
186-
}
187-
});
188-
164+
contextService.listBoxRef = ref;
189165
return (
190166
<ul
191167
ref={ref}
@@ -199,25 +175,27 @@ const ListBox = component$(({ ...props }: StyleProps) => {
199175
`}
200176
class={props.class}
201177
onKeyDown$={(e) => {
202-
const availableOptions = contextService.options.filter(
203-
(option) => !(option.getAttribute('aria-disabled') === 'true')
204-
);
178+
const availableOptions = contextService.options
179+
.map((option) => option.value)
180+
.filter(
181+
(option) => !(option?.getAttribute('aria-disabled') === 'true')
182+
);
205183
const target = e.target as HTMLElement;
206184
const currentIndex = availableOptions.indexOf(target);
207185

208186
if (e.key === 'ArrowDown') {
209187
if (currentIndex === availableOptions.length - 1) {
210-
availableOptions[0].focus();
188+
availableOptions[0]?.focus();
211189
} else {
212-
availableOptions[currentIndex + 1].focus();
190+
availableOptions[currentIndex + 1]?.focus();
213191
}
214192
}
215193

216194
if (e.key === 'ArrowUp') {
217195
if (currentIndex <= 0) {
218-
availableOptions[availableOptions.length - 1].focus();
196+
availableOptions[availableOptions.length - 1]?.focus();
219197
} else {
220-
availableOptions[currentIndex - 1].focus();
198+
availableOptions[currentIndex - 1]?.focus();
221199
}
222200
}
223201
}}
@@ -254,9 +232,11 @@ interface OptionProps extends StyleProps {
254232

255233
const Option = component$(({ disabled, value, ...props }: OptionProps) => {
256234
const contextService = useContext(selectContext);
257-
235+
const thisOptionSignal = useSignal<HTMLElement>();
236+
contextService.options = [...contextService.options, thisOptionSignal];
258237
return (
259238
<li
239+
ref={thisOptionSignal}
260240
role="option"
261241
tabIndex={disabled ? -1 : 0}
262242
aria-disabled={disabled}
@@ -298,13 +278,4 @@ const Option = component$(({ disabled, value, ...props }: OptionProps) => {
298278
);
299279
});
300280

301-
export {
302-
Root,
303-
Trigger,
304-
Value,
305-
Marker,
306-
ListBox,
307-
Group,
308-
Label,
309-
Option,
310-
};
281+
export { Root, Trigger, Value, Marker, ListBox, Group, Label, Option };

0 commit comments

Comments
 (0)