Skip to content

Commit

Permalink
Merge pull request #722 from thejackshelton/multi-select
Browse files Browse the repository at this point in the history
Initial multi-select capabilities
  • Loading branch information
thejackshelton authored Apr 25, 2024
2 parents 2a34656 + 447ebe5 commit 3659c02
Show file tree
Hide file tree
Showing 24 changed files with 328 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default component$(() => {
return (
<>
<button onClick$={() => (isOpen.value = true)}>Toggle open state</button>
<Select bind:open={isOpen} class="select" aria-label="hero">
<Select bind:open={isOpen} class="select">
<SelectTrigger class="select-trigger">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@ export default component$(() => {

return (
<>
<Select
onChange$={$((value: string) => {
selected.value = value;
})}
bind:value={selected}
class="select"
>
<Select bind:value={selected} class="select">
<SelectTrigger class="select-trigger">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default component$(() => {

return (
<form preventdefault:submit>
<Select required class="select" aria-label="hero">
<Select required class="select">
<SelectTrigger class="select-trigger">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Select,
SelectPopover,
SelectGroup,
SelectLabel,
SelectGroupLabel,
SelectListbox,
SelectOption,
SelectTrigger,
Expand All @@ -24,13 +24,13 @@ export default component$(() => {
<SelectPopover class="select-popover">
<SelectListbox class="select-listbox">
<SelectGroup>
<SelectLabel class="select-label">People</SelectLabel>
<SelectGroupLabel class="select-label">People</SelectGroupLabel>
{users.map((user) => (
<SelectOption key={user}>{user}</SelectOption>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel class="select-label">Animals</SelectLabel>
<SelectGroupLabel class="select-group-label">Animals</SelectGroupLabel>
{animals.map((animal) => (
<SelectOption class="select-option" key={animal}>
{animal}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { component$, useStyles$ } from '@builder.io/qwik';
import {
Select,
SelectLabel,
SelectListbox,
SelectOption,
SelectPopover,
Expand All @@ -14,7 +15,8 @@ export default component$(() => {
const users = ['Tim', 'Ryan', 'Jim', 'Jessie', 'Abby'];

return (
<Select class="select" aria-label="hero">
<Select class="select">
<SelectLabel>Logged in users</SelectLabel>
<SelectTrigger class="select-trigger">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
Expand Down
31 changes: 31 additions & 0 deletions apps/website/src/routes/docs/headless/select/examples/multiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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 selected = useSignal<string[]>([]);

return (
<Select multiple bind:value={selected} class="select">
<SelectTrigger class="select-trigger">
<SelectValue>{selected.value}</SelectValue>
</SelectTrigger>
<SelectPopover class="select-popover">
<SelectListbox class="select-listbox">
{users.map((user) => (
<SelectOption key={user}>{user}</SelectOption>
))}
</SelectListbox>
</SelectPopover>
</Select>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Select,
SelectPopover,
SelectGroup,
SelectLabel,
SelectGroupLabel,
SelectListbox,
SelectOption,
SelectTrigger,
Expand All @@ -24,15 +24,15 @@ export default component$(() => {
<SelectPopover class="select-popover">
<SelectListbox class="select-listbox select-max-height">
<SelectGroup>
<SelectLabel class="select-label">People</SelectLabel>
<SelectGroupLabel class="select-label">People</SelectGroupLabel>
{users.map((user) => (
<SelectOption class="select-option" key={user}>
{user}
</SelectOption>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel class="select-label">Animals</SelectLabel>
<SelectGroupLabel class="select-label">Animals</SelectGroupLabel>
{animals.map((animal) => (
<SelectOption key={animal}>{animal}</SelectOption>
))}
Expand Down
6 changes: 6 additions & 0 deletions apps/website/src/routes/docs/headless/select/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ A common use case is the addition of options dynamically. For example, an infini

Clicking the `Add Users` button adds a couple new users mapped to the list. Taking this further, we could grab more data from the server and add it to the list, or even hitting a database to get more users.

### Multiple selections

<Showcase name="multiple" />

The `multiple` prop allows the user to select multiple options.

## Menu behavior

### Typeahead
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
SelectPopover,
SelectTrigger,
SelectGroup,
SelectLabel,
SelectGroupLabel,
SelectValue,
SelectListbox,
SelectOption,
Expand All @@ -22,7 +22,7 @@ export default component$(() => (

{/* optional */}
<SelectGroup>
<SelectLabel>group label</SelectLabel>
<SelectGroupLabel>group label</SelectGroupLabel>
<SelectOption>group option</SelectOption>
</SelectGroup>
</SelectListbox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
color: hsl(var(--foreground));
}

.select-label {
.select-group-label {
font-size: 0.875rem;
line-height: 1.25rem;
color: hsla(var(--foreground) / 0.8);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const HiddenSelect = component$(
{options?.map((opt: Opt) => (
<option
value={opt.value}
selected={context.selectedIndexSig.value === opt.index}
selected={context.selectedIndexesSig.value.includes(opt.index)}
key={opt.value}
>
{opt.displayValue}
Expand Down
3 changes: 2 additions & 1 deletion packages/kit-headless/src/components/select/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export * from './select';
export * from './select-label';
export * from './select-inline';
export * from './select-listbox';
export * from './select-option';
export * from './select-popover';
export * from './select-trigger';
export * from './select-value';
export * from './select-group';
export * from './select-label';
export * from './select-group-label';
export * from './hidden-select';
6 changes: 4 additions & 2 deletions packages/kit-headless/src/components/select/select-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ export type SelectContext = {
popoverRef: Signal<HTMLElement | undefined>;
listboxRef: Signal<HTMLUListElement | undefined>;
groupRef: Signal<HTMLDivElement | undefined>;
labelRef: Signal<HTMLDivElement | undefined>;

// core state
localId: string;
optionsSig: Signal<Opt[]>;
highlightedIndexSig: Signal<number | null>;
isListboxOpenSig: Signal<boolean>;
selectedIndexSig: Signal<number | null>;
selectedIndexesSig: Signal<Array<number | null>>;

// user configurable
scrollOptions?: ScrollIntoViewOptions;
loop: boolean;
multiple: boolean | undefined;
};

export const groupContextId = createContextId<GroupContext>('Select-Group');

export type GroupContext = {
labelId: string;
groupLabelId: string;
};
14 changes: 14 additions & 0 deletions packages/kit-headless/src/components/select/select-group-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PropsOf, Slot, component$, useContext } from '@builder.io/qwik';
import { groupContextId } from './select-context';

type SelectLabelProps = PropsOf<'li'>;

export const SelectGroupLabel = component$<SelectLabelProps>((props) => {
const groupContext = useContext(groupContextId);

return (
<li id={groupContext.groupLabelId} {...props}>
<Slot />
</li>
);
});
7 changes: 3 additions & 4 deletions packages/kit-headless/src/components/select/select-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
component$,
useContext,
useContextProvider,
useId,
} from '@builder.io/qwik';

import SelectContextId, { groupContextId } from './select-context';
Expand All @@ -13,16 +12,16 @@ type SelectGroupProps = PropsOf<'div'>;

export const SelectGroup = component$<SelectGroupProps>((props) => {
const context = useContext(SelectContextId);
const labelId = useId();
const groupLabelId = `${context.localId}-group-label`;

const groupContext = {
labelId,
groupLabelId,
};

useContextProvider(groupContextId, groupContext);

return (
<div aria-labelledby={labelId} role="group" {...props} ref={context.groupRef}>
<div aria-labelledby={groupLabelId} role="group" {...props} ref={context.groupRef}>
<Slot />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type JSXNode, Component } from '@builder.io/qwik';
import { SelectImpl, type SelectProps } from './select';
import { SelectOption } from './select-option';
import { SelectLabel } from './select-label';

export type Opt = {
isDisabled: boolean;
Expand All @@ -18,6 +19,7 @@ export const Select: Component<SelectProps> = (props: SelectProps) => {
const opts: Opt[] = [];
let currentIndex = 0;
let valuePropIndex = null;
let label = false;

const childrenToProcess = (
Array.isArray(myChildren) ? [...myChildren] : [myChildren]
Expand Down Expand Up @@ -70,6 +72,11 @@ export const Select: Component<SelectProps> = (props: SelectProps) => {
break;
}

case SelectLabel: {
label = true;
break;
}

default: {
if (child) {
const anyChildren = Array.isArray(child.children)
Expand All @@ -95,7 +102,7 @@ export const Select: Component<SelectProps> = (props: SelectProps) => {
}

return (
<SelectImpl {...rest} _valuePropIndex={valuePropIndex} _options={opts}>
<SelectImpl {...rest} _label={label} _valuePropIndex={valuePropIndex} _options={opts}>
{props.children}
</SelectImpl>
);
Expand Down
13 changes: 6 additions & 7 deletions packages/kit-headless/src/components/select/select-label.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { PropsOf, Slot, component$, useContext } from '@builder.io/qwik';
import { groupContextId } from './select-context';
import SelectContextId from './select-context';

type SelectLabelProps = PropsOf<'li'>;

export const SelectLabel = component$<SelectLabelProps>((props) => {
const groupContext = useContext(groupContextId);
export const SelectLabel = component$((props: PropsOf<'div'>) => {
const context = useContext(SelectContextId);
const labelId = `${context.localId}-label`;

return (
<li id={groupContext.labelId} {...props}>
<div ref={context.labelRef} id={labelId} {...props}>
<Slot />
</li>
</div>
);
});
14 changes: 11 additions & 3 deletions packages/kit-headless/src/components/select/select-option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@builder.io/qwik';
import { isServer, isBrowser } from '@builder.io/qwik/build';
import SelectContextId from './select-context';
import { useSelect } from './use-select';

export type SelectOptionProps = PropsOf<'li'> & {
/** Internal index we get from the inline component. Please see select-inline.tsx */
Expand All @@ -30,8 +31,11 @@ export const SelectOption = component$<SelectOptionProps>((props) => {
const localIndexSig = useSignal<number | null>(null);
const optionId = `${context.localId}-${_index}`;

const { addUniqueIndex } = useSelect();

const isSelectedSig = useComputed$(() => {
return !disabled && context.selectedIndexSig.value === _index;
const index = _index ?? null;
return !disabled && context.selectedIndexesSig.value.includes(index);
});

const isHighlightedSig = useComputed$(() => {
Expand Down Expand Up @@ -78,8 +82,12 @@ export const SelectOption = component$<SelectOptionProps>((props) => {
const handleClick$ = $(() => {
if (disabled) return;

context.selectedIndexSig.value = localIndexSig.value;
context.isListboxOpenSig.value = false;
if (context.multiple) {
addUniqueIndex(context.selectedIndexesSig, localIndexSig.value);
} else {
context.selectedIndexesSig.value = [localIndexSig.value];
context.isListboxOpenSig.value = false;
}
});

const handlePointerOver$ = $(() => {
Expand Down
Loading

0 comments on commit 3659c02

Please sign in to comment.