refactor dropdown to select
This commit is contained in:
parent
57bb6ec2d9
commit
6d69566e1a
3 changed files with 179 additions and 3 deletions
95
src/components/select/index.module.css
Normal file
95
src/components/select/index.module.css
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
.box {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
&:has(> :popover-open) > .button {
|
||||||
|
background-color: var(--surface-500);
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: inherit;
|
||||||
|
place-items: center start;
|
||||||
|
|
||||||
|
/* Make sure the height of the button does not collapse when it is empty */
|
||||||
|
block-size: 1em;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
padding: var(--padding-m);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radii-m);
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--surface-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(> .caret) {
|
||||||
|
padding-inline-end: calc(1em + (2 * var(--padding-m)));
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .caret {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: var(--padding-m);
|
||||||
|
inset-block-start: 50%;
|
||||||
|
translate: 0 -50%;
|
||||||
|
inline-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
grid-template-columns: inherit;
|
||||||
|
|
||||||
|
inset-inline-start: anchor(start);
|
||||||
|
inset-block-start: anchor(end);
|
||||||
|
position-try-fallbacks: flip-block, flip-inline;
|
||||||
|
|
||||||
|
/* inline-size: anchor-size(self-inline); */
|
||||||
|
background-color: var(--surface-500);
|
||||||
|
padding: var(--padding-m);
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
|
||||||
|
&:popover-open {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > header {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
|
||||||
|
gap: var(--padding-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
row-gap: var(--padding-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
place-items: center start;
|
||||||
|
|
||||||
|
border-radius: var(--radii-m);
|
||||||
|
padding: var(--padding-s);
|
||||||
|
margin-inline: calc(-1 * var(--padding-s));
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: oklch(from var(--info) l c h / .1);
|
||||||
|
}
|
||||||
|
}
|
81
src/components/select/index.tsx
Normal file
81
src/components/select/index.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { createMemo, createSignal, For, JSX, Setter, createEffect, Show } from "solid-js";
|
||||||
|
import { FaSolidAngleDown } from "solid-icons/fa";
|
||||||
|
import css from './index.module.css';
|
||||||
|
|
||||||
|
interface SelectProps<T, K extends string> {
|
||||||
|
id: string;
|
||||||
|
class?: string;
|
||||||
|
value: K;
|
||||||
|
setValue?: Setter<K>;
|
||||||
|
values: Record<K, T>;
|
||||||
|
open?: boolean;
|
||||||
|
showCaret?: boolean;
|
||||||
|
children: (key: K, value: T) => JSX.Element;
|
||||||
|
filter?: (query: string, key: K, value: T) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select<T, K extends string>(props: SelectProps<T, K>) {
|
||||||
|
const [dialog, setDialog] = createSignal<HTMLDialogElement>();
|
||||||
|
const [key, setKey] = createSignal<K>(props.value);
|
||||||
|
const [open, setOpen] = createSignal<boolean>(props.open ?? false);
|
||||||
|
const [query, setQuery] = createSignal<string>('');
|
||||||
|
|
||||||
|
const values = createMemo(() => {
|
||||||
|
let entries = Object.entries<T>(props.values) as [K, T][];
|
||||||
|
const filter = props.filter;
|
||||||
|
const q = query();
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
entries = entries.filter(([k, v]) => filter(q, k, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showCaret = createMemo(() => props.showCaret ?? true);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
props.setValue?.(() => key());
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
dialog()?.[open() ? 'showPopover' : 'hidePopover']();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <section class={`${css.box} ${props.class}`}>
|
||||||
|
<button id={`${props.id}_button`} popoverTarget={`${props.id}_dialog`} class={css.button}>
|
||||||
|
<Show when={key()}>{
|
||||||
|
key => {
|
||||||
|
const value = createMemo(() => props.values[key()]);
|
||||||
|
|
||||||
|
return <>{props.children(key(), value())}</>;
|
||||||
|
}
|
||||||
|
}</Show>
|
||||||
|
|
||||||
|
<Show when={showCaret()}>
|
||||||
|
<FaSolidAngleDown class={css.caret} />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog ref={setDialog} id={`${props.id}_dialog`} anchor={`${props.id}_button`} popover class={css.dialog} onToggle={e => setOpen(e.newState === 'open')}>
|
||||||
|
<Show when={props.filter !== undefined}>
|
||||||
|
<header>
|
||||||
|
<input value={query()} onInput={e => setQuery(e.target.value)} />
|
||||||
|
</header>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<For each={values()}>{
|
||||||
|
([k, v]) => {
|
||||||
|
const selected = createMemo(() => key() === k);
|
||||||
|
|
||||||
|
return <span class={`${css.option} ${selected() ? css.selected : ''}`} onpointerdown={() => {
|
||||||
|
setKey(() => k);
|
||||||
|
dialog()?.hidePopover();
|
||||||
|
}}>{props.children(k, v)}</span>;
|
||||||
|
}
|
||||||
|
}</For>
|
||||||
|
</main>
|
||||||
|
</dialog>
|
||||||
|
</section>;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
import { internal_useI18n } from "./context";
|
import { internal_useI18n } from "./context";
|
||||||
import { locales } from "./constants";
|
import { locales } from "./constants";
|
||||||
import { Dropdown } from "~/components/dropdown";
|
import { Select } from "~/components/select";
|
||||||
import { Dynamic } from "solid-js/web";
|
import { Dynamic } from "solid-js/web";
|
||||||
import css from './picker.module.css';
|
import css from './picker.module.css';
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ interface LocalePickerProps { }
|
||||||
export const LocalePicker: Component<LocalePickerProps> = (props) => {
|
export const LocalePicker: Component<LocalePickerProps> = (props) => {
|
||||||
const { locale, setLocale } = internal_useI18n();
|
const { locale, setLocale } = internal_useI18n();
|
||||||
|
|
||||||
return <Dropdown
|
return <Select
|
||||||
id="locale-picker"
|
id="locale-picker"
|
||||||
class={css.box}
|
class={css.box}
|
||||||
value={locale()}
|
value={locale()}
|
||||||
|
@ -19,5 +19,5 @@ export const LocalePicker: Component<LocalePickerProps> = (props) => {
|
||||||
showCaret={false}
|
showCaret={false}
|
||||||
>
|
>
|
||||||
{(locale, { flag, label }) => <Dynamic component={flag} lang={locale} aria-label={label} class={css.flag} />}
|
{(locale, { flag, label }) => <Dynamic component={flag} lang={locale} aria-label={label} class={css.flag} />}
|
||||||
</Dropdown>
|
</Select>
|
||||||
};
|
};
|
Loading…
Add table
Add a link
Reference in a new issue