diff --git a/.changeset/breezy-crabs-film.md b/.changeset/breezy-crabs-film.md
new file mode 100644
index 000000000..a92f96aab
--- /dev/null
+++ b/.changeset/breezy-crabs-film.md
@@ -0,0 +1,5 @@
+---
+'@qwik-ui/headless': patch
+---
+
+feat: Adding the bind:open signal prop to the select component can now reactively control the select listbox open state
diff --git a/apps/website/src/routes/docs/headless/select/examples/bind-open.tsx b/apps/website/src/routes/docs/headless/select/examples/bind-open.tsx
new file mode 100644
index 000000000..e2af01c91
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/select/examples/bind-open.tsx
@@ -0,0 +1,34 @@
+import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
+import {
+ Select,
+ SelectListbox,
+ SelectOption,
+ SelectPopover,
+ SelectTrigger,
+ SelectValue,
+} from '@qwik-ui/headless';
+import styles from '../snippets/select.css?inline';
+
+export default component$(() => {
+ useStyles$(styles);
+ const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby'];
+ const isOpen = useSignal(false);
+
+ return (
+ <>
+
+
+ >
+ );
+});
diff --git a/apps/website/src/routes/docs/headless/select/index.mdx b/apps/website/src/routes/docs/headless/select/index.mdx
index 5ce69a7ec..603d96b67 100644
--- a/apps/website/src/routes/docs/headless/select/index.mdx
+++ b/apps/website/src/routes/docs/headless/select/index.mdx
@@ -173,10 +173,14 @@ We can pass reactive state by using the `bind:value` prop to the `` ro
-`bind:value` is a signal prop, it allows us to programmatically control the selected value of the select component.
+`bind:value` is a signal prop, it allows us to reactively control the selected value of the select component.
> Signal props enables two-way data binding efficiently without the common issues of change detection in other frameworks.
+We can also reactively control the open state of the select component by using the `bind:open` signal prop.
+
+
+
### Programmatic changes
To combine some of our previous knowledge, let's use the `onChange$` handler and `bind:value` prop in tandem.
diff --git a/packages/kit-headless/src/components/select/select.test.ts b/packages/kit-headless/src/components/select/select.test.ts
index 403ad63ad..772747006 100644
--- a/packages/kit-headless/src/components/select/select.test.ts
+++ b/packages/kit-headless/src/components/select/select.test.ts
@@ -972,6 +972,18 @@ test.describe('Props', () => {
await expect(getHiddenOptionAt(1)).toHaveAttribute('data-highlighted');
await expect(getHiddenOptionAt(1)).toHaveAttribute('aria-selected', 'true');
});
+
+ test(`GIVEN a controlled closed select with a bind:open prop on the root component
+ WHEN the bind:open signal changes to true
+ THEN the listbox should open to reflect the new signal value`, async ({ page }) => {
+ const { driver: d } = await setup(page, 'bind-open');
+
+ await expect(d.getListbox()).toBeHidden();
+
+ page.getByRole('button', { name: 'Toggle open state' }).click();
+
+ await expect(d.getListbox()).toBeVisible();
+ });
});
test.describe('option value', () => {
diff --git a/packages/kit-headless/src/components/select/select.tsx b/packages/kit-headless/src/components/select/select.tsx
index 888841f08..f79cff01d 100644
--- a/packages/kit-headless/src/components/select/select.tsx
+++ b/packages/kit-headless/src/components/select/select.tsx
@@ -30,9 +30,12 @@ export type SelectProps = PropsOf<'div'> & {
/** The initial selected value (uncontrolled). */
value?: string;
- /** A signal that contains the current selected value (controlled). */
+ /** A signal that controls the current selected value (controlled). */
'bind:value'?: Signal;
+ /** A signal that controls the current open state (controlled). */
+ 'bind:open'?: Signal;
+
/**
* QRL handler that runs when a select value changes.
* @param value The new value as a string.
@@ -136,6 +139,12 @@ export const SelectImpl = component$(
}
});
+ useTask$(function reactiveOpenTask({ track }) {
+ const signalValue = track(() => props['bind:open']?.value);
+
+ isListboxOpenSig.value = signalValue ?? isListboxOpenSig.value;
+ });
+
useTask$(async function onChangeTask({ track }) {
track(() => selectedIndexSig.value);
if (isBrowser && selectedIndexSig.value !== null) {