Skip to content

Commit 6b435e8

Browse files
thejacksheltonwmertens
authored andcommitted
refactor(combobox): refactoring types / deriving types
1 parent 15d5924 commit 6b435e8

File tree

8 files changed

+97
-46
lines changed

8 files changed

+97
-46
lines changed

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ const trainers = [
2626
];
2727

2828
interface Trainer {
29-
value: string;
30-
label: string;
29+
testValue: string;
30+
testLabel: string;
3131
disabled: boolean;
3232
}
3333

3434
const ALL_OPTIONS: Array<Trainer> = [
35-
{ value: 'alice', label: 'Alice', disabled: false },
36-
{ value: 'joana', label: 'Joana', disabled: false },
37-
{ value: 'malcolm', label: 'Malcolm', disabled: false },
38-
{ value: 'zack', label: 'Zack', disabled: true },
39-
{ value: 'brian', label: 'Brian', disabled: false }
35+
{ testValue: 'alice', testLabel: 'Alice', disabled: false },
36+
{ testValue: 'joana', testLabel: 'Joana', disabled: false },
37+
{ testValue: 'malcolm', testLabel: 'Malcolm', disabled: false },
38+
{ testValue: 'zack', testLabel: 'Zack', disabled: true },
39+
{ testValue: 'brian', testLabel: 'Brian', disabled: false }
4040
];
4141

4242
export const Example01 = component$(() => {
@@ -46,7 +46,7 @@ export const Example01 = component$(() => {
4646

4747
const onInputChange$ = $((value: string) => {
4848
optionsSig.value = ALL_OPTIONS.filter((option) => {
49-
return option.label.toLowerCase().includes(value.toLowerCase());
49+
return option.testLabel.toLowerCase().includes(value.toLowerCase());
5050
});
5151

5252
console.log(optionsSig.value);
@@ -66,13 +66,15 @@ export const Example01 = component$(() => {
6666
<Combobox
6767
options={optionsSig}
6868
onInputChange$={onInputChange$}
69+
optionValueKey="testValue"
70+
optionLabelKey="testLabel"
6971
optionComponent$={$((option: string | Trainer, index: number) => (
7072
<ComboboxOption
7173
index={index}
7274
option={option}
7375
class="rounded-sm px-2 hover:bg-[#496080] focus:bg-[#496080]"
7476
>
75-
{typeof option === 'string' ? option : option.label}
77+
{typeof option === 'string' ? option : option.testLabel}
7678
</ComboboxOption>
7779
))}
7880
class="relative"

packages/kit-headless/src/components/combobox/combobox-context.type.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export interface ComboboxContext {
1010
triggerRef: Signal<HTMLButtonElement | undefined>;
1111
optionComponent$?: QRL<(option: any, index: number) => JSXNode>;
1212
onInputChange$?: QRL<(value: string) => void>;
13+
optionValueKey?: string;
14+
optionLabelKey?: string;
15+
optionDisabledKey?: string;
1316
options: Signal<Array<string | Record<string, any>>>;
1417
highlightedIndexSig: Signal<number>;
1518
}
19+
20+
export type Option = ComboboxContext['options']['value'][number];

packages/kit-headless/src/components/combobox/combobox-control.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const ComboboxControl = component$((props: ComboboxControlProps) => {
3131

3232
useOnWindow('click', closeULOnOutsideClick$);
3333

34-
useVisibleTask$(({ cleanup }) => {
34+
useVisibleTask$(function preventFocusChangeTask({ cleanup }) {
3535
if (controlRef.value) {
3636
const handleMousedown = (e: MouseEvent): void => {
3737
const isTrigger = e.target === context.triggerRef.value;

packages/kit-headless/src/components/combobox/combobox-input.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@builder.io/qwik';
99
import { KeyCode } from '../../utils';
1010
import ComboboxContextId from './combobox-context-id';
11+
import { getOptionLabel } from './utils';
1112

1213
const preventedKeys = [KeyCode.Home, KeyCode.End, KeyCode.PageDown, KeyCode.ArrowUp];
1314

@@ -18,10 +19,10 @@ export const ComboboxInput = component$((props: ComboboxInputProps) => {
1819
const context = useContext(ComboboxContextId);
1920

2021
const onKeydownBehavior$ = $((e: QwikKeyboardEvent) => {
21-
const highlightedOption = context.options.value[context.highlightedIndexSig.value];
22-
const highlightedOptionLabel = highlightedOption
23-
? (highlightedOption as any).label
24-
: undefined;
22+
const highlightedOptionLabel = getOptionLabel(
23+
context.options.value[context.highlightedIndexSig.value],
24+
context
25+
);
2526

2627
if (e.key === 'ArrowDown') {
2728
// If the listbox is already open, move down

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

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,25 @@ import {
88
useVisibleTask$,
99
} from '@builder.io/qwik';
1010
import ComboboxContextId from './combobox-context-id';
11+
import { getOptionLabel } from './utils';
12+
13+
import { Option } from './combobox-context.type';
1114

1215
export type ComboboxOptionProps = {
1316
index: number;
14-
option: string | Record<string, any>;
17+
option: Option;
1518
} & QwikIntrinsicElements['li'];
1619

1720
export const ComboboxOption = component$(
1821
({ index, option, ...props }: ComboboxOptionProps) => {
1922
// const index = (props as ComboboxOptionProps & { _index: number })._index;
23+
option;
2024
const context = useContext(ComboboxContextId);
21-
const isHighlightedSig = useSignal(false);
25+
const isHighlightedSig = useComputed$(
26+
() => context.highlightedIndexSig.value === index
27+
);
28+
29+
useSignal(false);
2230
const optionRef = useSignal<HTMLLIElement>();
2331
// const selectedOptionIndexSig = context.selectedOptionIndexSig;
2432

@@ -55,23 +63,32 @@ export const ComboboxOption = component$(
5563
}
5664
});
5765

58-
useVisibleTask$(function setHighlightedOptionTask({ track }) {
59-
track(() => context.highlightedIndexSig.value);
60-
61-
const highlightedOption = context.options.value[context.highlightedIndexSig.value];
62-
63-
// NOT a signal. The value property on the options array of objects.
64-
const optionValue = (option as any).value;
65-
const highlightedOptionValue = highlightedOption
66-
? (highlightedOption as any).value
67-
: undefined;
68-
69-
if (highlightedOption && highlightedOptionValue === optionValue) {
70-
isHighlightedSig.value = true;
71-
} else {
72-
isHighlightedSig.value = false;
73-
}
74-
});
66+
// // NOT a signal. The value property on the options array of objects.
67+
// let optionValue, optionLabel, isOptionDisabled;
68+
// if (typeof option === 'string') {
69+
// optionValue = option;
70+
// optionLabel = option;
71+
// isOptionDisabled = false;
72+
// } else {
73+
// const valueKey = context.optionValueKey ?? 'value';
74+
// const labelKey = context.optionLabelKey ?? 'label';
75+
// const disabledKey = context.optionDisabledKey ?? 'disabled';
76+
// if (option[valueKey] === undefined) {
77+
// throw new Error(
78+
// 'Qwik UI: Combobox optionValueKey was not provided, and the option was not a string. Please provide a value for optionValueKey, or ensure that the option is a string.'
79+
// );
80+
// }
81+
82+
// if (option[labelKey] === undefined) {
83+
// throw new Error(
84+
// 'Qwik UI: Combobox optionLabelKey was not provided, and the option was not a string. Please provide a value for optionLabelKey, or ensure that the option is a string.'
85+
// );
86+
// }
87+
88+
// optionValue = option[valueKey];
89+
// optionLabel = option[labelKey];
90+
// isOptionDisabled = option[disabledKey];
91+
// }
7592

7693
return (
7794
<li
@@ -85,12 +102,10 @@ export const ComboboxOption = component$(
85102
return;
86103
}
87104

88-
context.inputRef.value.value = (
89-
context.options.value[context.highlightedIndexSig.value] as Record<
90-
string,
91-
any
92-
>
93-
).label;
105+
context.inputRef.value.value = getOptionLabel(
106+
context.options.value[context.highlightedIndexSig.value],
107+
context
108+
);
94109

95110
context.isListboxOpenSig.value = false;
96111
// context.inputRef.value.focus();

packages/kit-headless/src/components/combobox/combobox-portal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ import {
1212
useVisibleTask$,
1313
} from '@builder.io/qwik';
1414

15+
import { isServer } from '@builder.io/qwik/build';
1516
import { ContextPair, openPortalContextId } from '../qwik-ui-provider';
1617
import ComboboxContextId from './combobox-context-id';
1718

18-
import { isServer } from '@builder.io/qwik/build';
19-
2019
export const ComboboxPortal: FunctionComponent = ({ children }) => {
2120
return <ComboboxPortalImpl elementToTeleport={children} />;
2221
};

packages/kit-headless/src/components/combobox/combobox.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ import {
99
useSignal,
1010
} from '@builder.io/qwik';
1111

12-
export type ComboboxImplProps = {
12+
import { Option } from './combobox-context.type';
13+
14+
export type ComboboxProps = {
1315
defaultValue?: string;
1416
placeholder?: string;
1517
// filter: boolean | ((value: string) => boolean);
16-
optionComponent$?: QRL<(option: any, index: number) => JSXNode>;
18+
optionComponent$?: QRL<(option: Option, index: number) => JSXNode>;
1719
onInputChange$?: QRL<(value: string) => void>;
1820
optionValue?: string;
1921
optionTextValue?: string;
2022
optionLabel?: string;
21-
options: Signal<Array<string | Record<string, any>>>;
23+
optionValueKey?: string;
24+
optionLabelKey?: string;
25+
optionDisabledKey?: string;
26+
options: Signal<Array<Option>>;
2227
'bind:isListboxOpenSig'?: Signal<boolean | undefined>;
2328
'bind:isInputFocusedSig'?: Signal<boolean | undefined>;
2429
'bind:isTriggerFocusedSig'?: Signal<boolean | undefined>;
@@ -29,6 +34,7 @@ export type OptionInfo = {
2934
index: number;
3035
};
3136

37+
// DO NOT REMOVE THIS: IT'S HOW YOU GET INDEXES WITHOUT MAPPING
3238
// export const Combobox: FunctionComponent<ComboboxImplProps> = (props) => {
3339
// const { children: myChildren, ...rest } = props;
3440

@@ -60,7 +66,6 @@ export type OptionInfo = {
6066
// childrenToProcess.unshift(...portalChildren);
6167
// break;
6268
// }
63-
6469
// case ComboboxListbox: {
6570
// const listboxChildren = Array.isArray(child.props.children)
6671
// ? [...child.props.children]
@@ -80,14 +85,17 @@ export type OptionInfo = {
8085
import ComboboxContextId from './combobox-context-id';
8186
import { ComboboxContext } from './combobox-context.type';
8287

83-
export const Combobox = component$((props: ComboboxImplProps) => {
88+
export const Combobox = component$((props: ComboboxProps) => {
8489
const {
8590
'bind:isListboxOpenSig': givenListboxOpenSig,
8691
'bind:isInputFocusedSig': givenInputFocusedSig,
8792
'bind:isTriggerFocusedSig': givenTriggerFocusedSig,
8893
optionComponent$,
8994
onInputChange$,
9095
options,
96+
optionValueKey,
97+
optionLabelKey,
98+
optionDisabledKey,
9199
...rest
92100
} = props;
93101
const listboxRef = useSignal<HTMLUListElement>();
@@ -119,6 +127,9 @@ export const Combobox = component$((props: ComboboxImplProps) => {
119127
listboxRef,
120128
optionComponent$,
121129
onInputChange$,
130+
optionValueKey,
131+
optionLabelKey,
132+
optionDisabledKey,
122133
options,
123134
highlightedIndexSig,
124135
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ComboboxContext, Option } from './combobox-context.type';
2+
3+
export function getOptionLabel(option: undefined | Option, context: ComboboxContext) {
4+
if (option === undefined) {
5+
return undefined;
6+
}
7+
if (typeof option === 'string') {
8+
return option;
9+
}
10+
11+
const labelKey = context.optionLabelKey ?? 'label';
12+
if (option[labelKey] === undefined) {
13+
throw new Error(
14+
'Qwik UI: Combobox optionLabelKey was not provided, and the option was not a string. Please provide a value for optionLabelKey, use the property name "label", or ensure that the option is a string.'
15+
);
16+
}
17+
return option[labelKey];
18+
}

0 commit comments

Comments
 (0)