Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: bind:open prop on select #707

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading