Skip to content

Commit

Permalink
Merge pull request #707 from thejackshelton/feat/select
Browse files Browse the repository at this point in the history
feat: bind:open prop on select
  • Loading branch information
thejackshelton authored Apr 18, 2024
2 parents 11e0590 + ffd16ff commit 6afa7fe
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-crabs-film.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button onClick$={() => (isOpen.value = true)}>Toggle open state</button>
<Select bind:open={isOpen} class="select" aria-label="hero">
<SelectTrigger class="select-trigger">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectPopover class="select-popover">
<SelectListbox class="select-listbox">
{users.map((user) => (
<SelectOption key={user}>{user}</SelectOption>
))}
</SelectListbox>
</SelectPopover>
</Select>
</>
);
});
6 changes: 5 additions & 1 deletion apps/website/src/routes/docs/headless/select/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,14 @@ We can pass reactive state by using the `bind:value` prop to the `<Select />` ro

<Showcase name="controlled" />

`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.

<Showcase name="bind-open" />

### Programmatic changes

To combine some of our previous knowledge, let's use the `onChange$` handler and `bind:value` prop in tandem.
Expand Down
12 changes: 12 additions & 0 deletions packages/kit-headless/src/components/select/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/kit-headless/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

/** A signal that controls the current open state (controlled). */
'bind:open'?: Signal<boolean>;

/**
* QRL handler that runs when a select value changes.
* @param value The new value as a string.
Expand Down Expand Up @@ -136,6 +139,12 @@ export const SelectImpl = component$<SelectProps & InternalSelectProps>(
}
});

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) {
Expand Down

0 comments on commit 6afa7fe

Please # to comment.