refactor menu to use dropdown component

This commit is contained in:
Chris Kruining 2025-01-07 14:04:02 +01:00
parent 096d4c2651
commit 9d943c1182
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
4 changed files with 25 additions and 79 deletions

View file

@ -21,8 +21,6 @@ export function Dropdown(props: DropdownProps) {
const [dialog, setDialog] = createSignal<HTMLDialogElement>(); const [dialog, setDialog] = createSignal<HTMLDialogElement>();
const [open, setOpen] = createSignal<boolean>(props.open ?? false); const [open, setOpen] = createSignal<boolean>(props.open ?? false);
const showCaret = createMemo(() => props.showCaret ?? true);
createEffect(() => { createEffect(() => {
dialog()?.[open() ? 'showPopover' : 'hidePopover'](); dialog()?.[open() ? 'showPopover' : 'hidePopover']();
}); });
@ -42,7 +40,7 @@ export function Dropdown(props: DropdownProps) {
<button id={`${props.id}_button`} popoverTarget={`${props.id}_dialog`} class={css.button}> <button id={`${props.id}_button`} popoverTarget={`${props.id}_dialog`} class={css.button}>
{props.text} {props.text}
<Show when={showCaret()}> <Show when={props.showCaret}>
<FaSolidAngleDown class={css.caret} /> <FaSolidAngleDown class={css.caret} />
</Show> </Show>
</button> </button>

View file

@ -19,6 +19,7 @@ export function Select<T, K extends string>(props: SelectProps<T, K>) {
const [key, setKey] = createSignal<K>(props.value); const [key, setKey] = createSignal<K>(props.value);
const [query, setQuery] = createSignal<string>(''); const [query, setQuery] = createSignal<string>('');
const showCaret = createMemo(() => props.showCaret ?? true);
const values = createMemo(() => { const values = createMemo(() => {
let entries = Object.entries<T>(props.values) as [K, T][]; let entries = Object.entries<T>(props.values) as [K, T][];
const filter = props.filter; const filter = props.filter;
@ -43,7 +44,7 @@ export function Select<T, K extends string>(props: SelectProps<T, K>) {
} }
}</Show> }</Show>
return <Dropdown api={setDropdown} id={props.id} class={`${css.box} ${props.class}`} showCaret={props.showCaret} open={props.open} text={text}> return <Dropdown api={setDropdown} id={props.id} class={`${css.box} ${props.class}`} showCaret={showCaret()} open={props.open} text={text}>
<Show when={props.filter !== undefined}> <Show when={props.filter !== undefined}>
<header> <header>
<input value={query()} onInput={e => setQuery(e.target.value)} /> <input value={query()} onInput={e => setQuery(e.target.value)} />

View file

@ -1,4 +1,6 @@
.root { .root {
display: grid;
grid-auto-flow: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -7,7 +9,7 @@
} }
} }
.item { :is(.item, .child > button) {
padding: var(--padding-m) var(--padding-l); padding: var(--padding-m) var(--padding-l);
background-color: inherit; background-color: inherit;
@ -22,26 +24,12 @@
} }
} }
.child { .child > dialog {
position: fixed;
inset-inline-start: anchor(self-start);
inset-block-start: anchor(end);
grid-template-columns: auto auto; grid-template-columns: auto auto;
place-content: start; place-content: start;
gap: var(--padding-m); gap: var(--padding-m);
padding: var(--padding-m) 0; padding: var(--padding-m) 0;
inline-size: max-content;
background-color: var(--surface-500);
border: 1px solid var(--surface-300);
border-block-start-width: 0;
margin: unset;
&:popover-open {
display: grid;
}
& > .separator { & > .separator {
grid-column: span 2; grid-column: span 2;
@ -62,8 +50,4 @@
background-color: var(--surface-600); background-color: var(--surface-600);
} }
} }
}
:popover-open + .item {
background-color: var(--surface-500);
} }

View file

@ -3,6 +3,7 @@ import { Portal } from "solid-js/web";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { CommandType, Command, useCommands } from "../command"; import { CommandType, Command, useCommands } from "../command";
import css from "./index.module.css"; import css from "./index.module.css";
import { Dropdown, DropdownApi } from "~/components/dropdown";
export interface MenuContextType { export interface MenuContextType {
ref: Accessor<Node | undefined>; ref: Accessor<Node | undefined>;
@ -102,77 +103,39 @@ const Separator: Component = (props) => {
const Root: ParentComponent<{}> = (props) => { const Root: ParentComponent<{}> = (props) => {
const menuContext = useMenu(); const menuContext = useMenu();
const commandContext = useCommands(); const commandContext = useCommands();
const [current, setCurrent] = createSignal<HTMLElement>();
const items = children(() => props.children).toArray() as unknown as (Item | ItemWithChildren)[]; const items = children(() => props.children).toArray() as unknown as (Item | ItemWithChildren)[];
menuContext.addItems(items) menuContext.addItems(items);
const close = () => {
const el = current();
if (el) {
el.hidePopover();
setCurrent(undefined);
}
};
const onExecute = (command?: CommandType) => {
return command
? (e: Event) => {
close();
return commandContext?.execute(command, e);
}
: () => { }
};
const Child: Component<{ command: CommandType }> = (props) => {
return <button class={css.item} type="button" onpointerdown={onExecute(props.command)}>
<Command.Handle command={props.command} />
</button>
};
return <Portal mount={menuContext.ref()}> return <Portal mount={menuContext.ref()}>
<For each={items}>{ <For each={items}>{
item => <Switch> item => <Switch>
<Match when={item.kind === 'node' ? item as ItemWithChildren : undefined}>{ <Match when={item.kind === 'node' ? item as ItemWithChildren : undefined}>{
item => <> item => {
<div const [dropdown, setDropdown] = createSignal<DropdownApi>();
class={css.child}
id={`child-${item().id}`} return <Dropdown api={setDropdown} class={css.child} id={`child-${item().id}`} text={item().label}>
style={`position-anchor: --menu-${item().id};`}
popover
on:toggle={(e: ToggleEvent) => {
if (e.newState === 'open' && e.target !== null) {
return setCurrent(e.target as HTMLElement);
}
}}
>
<For each={item().children}>{ <For each={item().children}>{
child => <Switch> child => <Switch>
<Match when={child.kind === 'leaf' ? child as Item : undefined}>{ <Match when={child.kind === 'leaf' ? child as Item : undefined}>{
item => <Child command={item().command} /> item => <button class={css.item} type="button" onpointerdown={e => {
commandContext?.execute(item().command, e);
dropdown()?.hide();
}}>
<Command.Handle command={item().command} />
</button>
}</Match> }</Match>
<Match when={child.kind === 'separator'}><hr class={css.separator} /></Match> <Match when={child.kind === 'separator'}><hr class={css.separator} /></Match>
</Switch> </Switch>}</For>
}</For> </Dropdown>;
</div> }
<button
class={css.item}
type="button"
popovertarget={`child-${item().id}`}
style={`anchor-name: --menu-${item().id};`}
>
{item().label}
</button>
</>
}</Match> }</Match>
<Match when={item.kind === 'leaf' ? item as Item : undefined}>{ <Match when={item.kind === 'leaf' ? item as Item : undefined}>{
item => <Child command={item().command} /> item => <button class={css.item} type="button" onpointerdown={e => commandContext?.execute(item().command, e)}>
<Command.Handle command={item().command} />
</button>
}</Match> }</Match>
</Switch> </Switch>
}</For> }</For>