toggle group: add arrow key navigation
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
|
import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
import type { IconComponentProps } from 'phosphor-svelte';
|
|
||||||
import { defaultIconProps, type IconDef } from './util';
|
import { defaultIconProps, type IconDef } from './util';
|
||||||
|
|
||||||
interface Props extends Omit<HTMLButtonAttributes, 'class'> {
|
interface Props extends Omit<HTMLButtonAttributes, 'class'> {
|
||||||
|
|||||||
@@ -27,16 +27,23 @@
|
|||||||
|
|
||||||
const id = $derived(generateIdentifier('toggle-group', name));
|
const id = $derived(generateIdentifier('toggle-group', name));
|
||||||
|
|
||||||
|
const extended: { detail: Option; ref?: ToggleSelect }[] = $derived.by(() => {
|
||||||
|
return options.map((opt) => ({
|
||||||
|
detail: opt
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
let inputElement: HTMLInputElement | undefined = $state<HTMLInputElement>();
|
let inputElement: HTMLInputElement | undefined = $state<HTMLInputElement>();
|
||||||
let valid: boolean = $state(true);
|
let valid: boolean = $state(true);
|
||||||
|
|
||||||
const makeSelectedHandler = (name: string) => {
|
const makeSelectedHandler = (name: string) => {
|
||||||
return (toggled: boolean) => {
|
return (toggled: boolean) => {
|
||||||
if (!inputElement) return;
|
if (inputElement) {
|
||||||
if (toggled) selected = [...selected, name];
|
if (toggled) selected = [...selected, name];
|
||||||
else selected = selected.filter((item) => item !== name);
|
else selected = selected.filter((item) => item !== name);
|
||||||
inputElement.value = JSON.stringify(selected);
|
inputElement.value = JSON.stringify(selected);
|
||||||
validateInput(inputElement); // trigger validation on hidden input
|
validateInput(inputElement); // trigger validation on hidden input
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -61,12 +68,26 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each options as opt}
|
{#each extended as opt, i (getValue(opt.detail))}
|
||||||
<ToggleSelect
|
<ToggleSelect
|
||||||
name={name ? undefined : `toggle_${getValue(opt)}`}
|
bind:this={extended[i].ref}
|
||||||
ontoggle={makeSelectedHandler(getValue(opt))}
|
ontoggle={makeSelectedHandler(getValue(opt.detail))}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
if (i < extended.length - 1) {
|
||||||
|
extended[i + 1].ref?.focus();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
if (i > 0) {
|
||||||
|
extended[i - 1].ref?.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span class={[typeof opt === 'string' && 'capitalize']}>{getLabel(opt)}</span>
|
<span class={[typeof opt === 'string' && 'capitalize']}>{getLabel(opt.detail)}</span>
|
||||||
</ToggleSelect>
|
</ToggleSelect>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
import type { HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Omit<HTMLButtonAttributes, 'type' | 'ontoggle'> {
|
||||||
|
ref?: HTMLButtonElement | null;
|
||||||
name?: string;
|
name?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
class?: ClassValue | null | undefined;
|
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
ontoggle?: (selected: boolean) => void;
|
ontoggle?: (selected: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { name, selected = false, class: classValue, children, onclick, ontoggle }: Props = $props();
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
name,
|
||||||
|
selected = false,
|
||||||
|
class: classValue,
|
||||||
|
children,
|
||||||
|
onclick,
|
||||||
|
ontoggle,
|
||||||
|
...others
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
const handleToggleSelectClick: MouseEventHandler<HTMLButtonElement> = (event) => {
|
const handleToggleSelectClick: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||||
selected = !selected; // update state
|
selected = !selected; // update state
|
||||||
ontoggle?.(selected); // send event to parent
|
ontoggle?.(selected); // send event to parent
|
||||||
onclick?.(event); // pass onclick handler through
|
onclick?.(event); // pass onclick handler through
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggle = () => {
|
||||||
|
selected = !selected; // update state
|
||||||
|
ontoggle?.(selected); // send event to parent
|
||||||
|
};
|
||||||
|
|
||||||
|
export const focus = () => {
|
||||||
|
ref?.focus();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if name}
|
{#if name}
|
||||||
@@ -25,6 +43,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
{...others}
|
||||||
class={[
|
class={[
|
||||||
'rounded-3xl border px-6 py-2.5 font-medium transition-colors',
|
'rounded-3xl border px-6 py-2.5 font-medium transition-colors',
|
||||||
selected
|
selected
|
||||||
|
|||||||
@@ -204,21 +204,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="component">
|
<div class="component">
|
||||||
<p class="title">Toggle Group</p>
|
<p class="title">Toggle</p>
|
||||||
|
|
||||||
|
<ToggleSelect name="example-toggle-select" class="mb-4">Toggle Me!</ToggleSelect>
|
||||||
|
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
name="example-toggle-group"
|
label="Toggle Group"
|
||||||
label="Toggler"
|
|
||||||
options={['item one', 'item two', { value: 'complex', label: 'Complex item' }]}
|
options={['item one', 'item two', { value: 'complex', label: 'Complex item' }]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="component">
|
|
||||||
<p class="title">Toggle Select</p>
|
|
||||||
|
|
||||||
<ToggleSelect name="example-toggle-select">Toggle Me!</ToggleSelect>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="component">
|
<div class="component">
|
||||||
<p class="title">Toolbar</p>
|
<p class="title">Toolbar</p>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user