Skip to content

Commit ffd16ff

Browse files
feat: bind:open prop on select
1 parent 11e0590 commit ffd16ff

File tree

5 files changed

+66
-2
lines changed

5 files changed

+66
-2
lines changed

.changeset/breezy-crabs-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik-ui/headless': patch
3+
---
4+
5+
feat: Adding the bind:open signal prop to the select component can now reactively control the select listbox open state
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
2+
import {
3+
Select,
4+
SelectListbox,
5+
SelectOption,
6+
SelectPopover,
7+
SelectTrigger,
8+
SelectValue,
9+
} from '@qwik-ui/headless';
10+
import styles from '../snippets/select.css?inline';
11+
12+
export default component$(() => {
13+
useStyles$(styles);
14+
const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby'];
15+
const isOpen = useSignal(false);
16+
17+
return (
18+
<>
19+
<button onClick$={() => (isOpen.value = true)}>Toggle open state</button>
20+
<Select bind:open={isOpen} class="select" aria-label="hero">
21+
<SelectTrigger class="select-trigger">
22+
<SelectValue placeholder="Select an option" />
23+
</SelectTrigger>
24+
<SelectPopover class="select-popover">
25+
<SelectListbox class="select-listbox">
26+
{users.map((user) => (
27+
<SelectOption key={user}>{user}</SelectOption>
28+
))}
29+
</SelectListbox>
30+
</SelectPopover>
31+
</Select>
32+
</>
33+
);
34+
});

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,14 @@ We can pass reactive state by using the `bind:value` prop to the `<Select />` ro
173173

174174
<Showcase name="controlled" />
175175

176-
`bind:value` is a signal prop, it allows us to programmatically control the selected value of the select component.
176+
`bind:value` is a signal prop, it allows us to reactively control the selected value of the select component.
177177

178178
> Signal props enables two-way data binding efficiently without the common issues of change detection in other frameworks.
179179
180+
We can also reactively control the open state of the select component by using the `bind:open` signal prop.
181+
182+
<Showcase name="bind-open" />
183+
180184
### Programmatic changes
181185

182186
To combine some of our previous knowledge, let's use the `onChange$` handler and `bind:value` prop in tandem.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,18 @@ test.describe('Props', () => {
972972
await expect(getHiddenOptionAt(1)).toHaveAttribute('data-highlighted');
973973
await expect(getHiddenOptionAt(1)).toHaveAttribute('aria-selected', 'true');
974974
});
975+
976+
test(`GIVEN a controlled closed select with a bind:open prop on the root component
977+
WHEN the bind:open signal changes to true
978+
THEN the listbox should open to reflect the new signal value`, async ({ page }) => {
979+
const { driver: d } = await setup(page, 'bind-open');
980+
981+
await expect(d.getListbox()).toBeHidden();
982+
983+
page.getByRole('button', { name: 'Toggle open state' }).click();
984+
985+
await expect(d.getListbox()).toBeVisible();
986+
});
975987
});
976988

977989
test.describe('option value', () => {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ export type SelectProps = PropsOf<'div'> & {
3030
/** The initial selected value (uncontrolled). */
3131
value?: string;
3232

33-
/** A signal that contains the current selected value (controlled). */
33+
/** A signal that controls the current selected value (controlled). */
3434
'bind:value'?: Signal<string>;
3535

36+
/** A signal that controls the current open state (controlled). */
37+
'bind:open'?: Signal<boolean>;
38+
3639
/**
3740
* QRL handler that runs when a select value changes.
3841
* @param value The new value as a string.
@@ -136,6 +139,12 @@ export const SelectImpl = component$<SelectProps & InternalSelectProps>(
136139
}
137140
});
138141

142+
useTask$(function reactiveOpenTask({ track }) {
143+
const signalValue = track(() => props['bind:open']?.value);
144+
145+
isListboxOpenSig.value = signalValue ?? isListboxOpenSig.value;
146+
});
147+
139148
useTask$(async function onChangeTask({ track }) {
140149
track(() => selectedIndexSig.value);
141150
if (isBrowser && selectedIndexSig.value !== null) {

0 commit comments

Comments
 (0)