Skip to content

Commit 875d197

Browse files
feat(select): added value prop
1 parent 2440215 commit 875d197

File tree

9 files changed

+141
-7
lines changed

9 files changed

+141
-7
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@qwik-ui/headless';
9+
import styles from './select.css?inline';
10+
export default component$(() => {
11+
useStyles$(styles);
12+
const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby'];
13+
const selected = useSignal<string>('Ryan');
14+
15+
return (
16+
<>
17+
<Select
18+
onChange$={$((value: string) => {
19+
selected.value = value;
20+
})}
21+
bind:value={selected}
22+
class="select"
23+
>
24+
<SelectTrigger class="select-trigger">
25+
<SelectValue placeholder="Select an option" />
26+
</SelectTrigger>
27+
<SelectListbox class="select-listbox">
28+
{users.map((user, index) => (
29+
<SelectOption value={index.toString()} class="select-option" key={user}>
30+
{user}
31+
</SelectOption>
32+
))}
33+
</SelectListbox>
34+
</Select>
35+
<button onClick$={$(() => (selected.value = '4'))}>Change to Abby</button>
36+
</>
37+
);
38+
});

apps/website/src/routes/docs/headless/select/examples/controlled.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default component$(() => {
1616
<>
1717
<Select
1818
onChange$={$((value: string) => {
19+
console.log('value: ', value);
1920
selected.value = value;
2021
})}
2122
bind:value={selected}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@qwik-ui/headless';
9+
import styles from './select.css?inline';
10+
export default component$(() => {
11+
useStyles$(styles);
12+
const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby'];
13+
const selected = useSignal<string | null>(null);
14+
15+
const handleChange$ = $((value: string) => {
16+
selected.value = value;
17+
});
18+
19+
return (
20+
<>
21+
<Select onChange$={handleChange$} class="select">
22+
<SelectTrigger class="select-trigger">
23+
<SelectValue placeholder="Select an option" />
24+
</SelectTrigger>
25+
<SelectListbox class="select-listbox">
26+
{users.map((user, index) => (
27+
<SelectOption value={index.toString()} class="select-option" key={user}>
28+
{user}
29+
</SelectOption>
30+
))}
31+
</SelectListbox>
32+
</Select>
33+
<p>The selected value is: {selected.value ?? 'null'}</p>
34+
</>
35+
);
36+
});

apps/website/src/routes/docs/headless/select/index.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ Reveals a list of options to choose from, often triggered by a button.
4040
<Showcase name="controlled" />
4141
</div>
4242

43+
## Passing a value
44+
45+
<div data-testid="select-option-value-test">
46+
<Showcase name="option-value" />
47+
</div>
48+
49+
## Controlled + The value prop
50+
51+
<div data-testid="select-controlled-value-test">
52+
<Showcase name="controlled-value" />
53+
</div>
54+
4355
## Adding Users
4456

4557
<div data-testid="select-add-users-test">

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,40 @@ test.describe('Props', () => {
10721072
await expect(options[1]).toHaveAttribute('aria-selected', 'true');
10731073
});
10741074
});
1075+
1076+
test.describe('option value', () => {
1077+
test(`GIVEN a select with distinct display and option values
1078+
WHEN the 2nd option is selected
1079+
THEN the selected value matches the 2nd option's value`, async ({ page }) => {
1080+
const { openListbox, getTrigger, getRoot } = await setup(
1081+
page,
1082+
'select-option-value-test',
1083+
);
1084+
1085+
await openListbox('Enter');
1086+
const changeStr = getRoot().locator('+ p');
1087+
await expect(changeStr).toContainText('The selected value is: null');
1088+
await getTrigger().focus();
1089+
await getTrigger().press('ArrowDown');
1090+
await getTrigger().press('Enter');
1091+
1092+
await expect(changeStr).toContainText('The selected value is: 1');
1093+
});
1094+
1095+
test(`GIVEN a select with distinct display and option values
1096+
WHEN a controlled value is set to the 5th option
1097+
THEN the selected value matches the 5th option's value`, async ({ page }) => {
1098+
const { getTrigger, getRoot, getOptions } = await setup(
1099+
page,
1100+
'select-controlled-value-test',
1101+
);
1102+
1103+
await expect(getTrigger()).toHaveText('Select an option');
1104+
await getRoot().locator('+ button').click();
1105+
const options = await getOptions();
1106+
await expect(getTrigger()).toHaveText(`${await options[4].textContent()}`);
1107+
});
1108+
});
10751109
});
10761110

10771111
test.describe('A11y', () => {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SelectGroup } from './select-group';
77
export type Opt = {
88
isDisabled: boolean;
99
value: string;
10+
displayValue?: string;
1011
};
1112

1213
/*
@@ -65,15 +66,22 @@ export const Select: Component<SelectProps> = (props: SelectProps) => {
6566
.props.children}.`,
6667
);
6768
}
69+
6870
child.props.index = currentIndex;
71+
const isDisabled = child.props.disabled === true;
72+
const value = (
73+
child.props.value ? child.props.value : child.props.children
74+
) as string;
75+
6976
const opt: Opt = {
70-
isDisabled: child.props.disabled === true,
71-
value: child.props.children as string,
77+
isDisabled,
78+
value,
79+
displayValue: child.props.children as string,
7280
};
7381

7482
opts.push(opt);
7583

76-
if (child.props.children === props.value) {
84+
if (value === props.value) {
7785
valuePropIndex = currentIndex;
7886
}
7987

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import SelectContextId from './select-context';
1414
export type SelectOptionProps = PropsOf<'li'> & {
1515
index?: number;
1616
disabled?: boolean;
17+
value?: string;
1718
};
1819

1920
export const SelectOption = component$<SelectOptionProps>((props) => {
@@ -24,6 +25,8 @@ export const SelectOption = component$<SelectOptionProps>((props) => {
2425
const localIndexSig = useSignal<number | null>(null);
2526
const optionId = `${context.localId}-${index}`;
2627

28+
console.log('value: ', props.value);
29+
2730
const isSelectedSig = useComputed$(() => {
2831
return !disabled && context.selectedIndexSig.value === index;
2932
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const SelectValue = component$((props: SelectValueProps) => {
1212

1313
const displayStrSig = useComputed$(() => {
1414
if (context.selectedIndexSig.value !== null) {
15-
return context.optionsSig.value[context.selectedIndexSig.value].value;
15+
return context.optionsSig.value[context.selectedIndexSig.value].displayValue;
1616
} else {
1717
return props.placeholder;
1818
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ export function useTypeahead() {
88
const prevTimeoutSig = useSignal<undefined | NodeJS.Timeout>(undefined);
99

1010
const firstCharOptionsSig = useComputed$(() => {
11-
return context.optionsSig.value.map((opt) => opt.value.slice(0, 1).toLowerCase());
11+
return context.optionsSig.value.map((opt) =>
12+
opt.displayValue?.slice(0, 1).toLowerCase(),
13+
);
1214
});
1315
const fullStrOptionsSig = useComputed$(() => {
14-
return context.optionsSig.value.map((opt) => opt.value.toLowerCase());
16+
return context.optionsSig.value.map((opt) => opt.displayValue?.toLowerCase());
1517
});
1618

1719
const typeahead$ = $((key: string): void => {
@@ -75,7 +77,7 @@ export function useTypeahead() {
7577

7678
const firstPossibleOpt = fullStrOptionsSig.value.findIndex((str) => {
7779
const size = inputStrSig.value.length;
78-
return str.substring(0, size) === inputStrSig.value;
80+
return str?.substring(0, size) === inputStrSig.value;
7981
});
8082
if (firstPossibleOpt !== -1) {
8183
context.highlightedIndexSig.value = firstPossibleOpt;

0 commit comments

Comments
 (0)