fix(electrobun): CustomDropdown groups .by + remove function call syntax

This commit is contained in:
Hibryda 2026-03-23 19:26:21 +01:00
parent bd48a09fd8
commit e8278ef444

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { ChevronDown } from 'lucide-svelte';
import { ChevronDown } from "lucide-svelte";
interface DropdownItem {
value: string;
@ -16,7 +16,14 @@
disabled?: boolean;
}
let { items, selected, placeholder = 'Select...', onSelect, groupBy = false, disabled = false }: Props = $props();
let {
items,
selected,
placeholder = "Select...",
onSelect,
groupBy = false,
disabled = false,
}: Props = $props();
let open = $state(false);
let focusIndex = $state(-1);
@ -24,10 +31,12 @@
let menuRef = $state<HTMLDivElement | null>(null);
let triggerRef = $state<HTMLButtonElement | null>(null);
let selectedLabel = $derived(items.find(i => i.value === selected)?.label ?? placeholder);
let selectedLabel = $derived(
items.find((i) => i.value === selected)?.label ?? placeholder,
);
// Group items by group field
let groups = $derived<string[]>(() => {
let groups = $derived.by<string[]>(() => {
if (!groupBy) return [];
const seen = new Set<string>();
for (const item of items) {
@ -49,7 +58,7 @@
if (disabled) return;
open = !open;
if (open) {
focusIndex = flatItems.findIndex(i => i.value === selected);
focusIndex = flatItems.findIndex((i) => i.value === selected);
computeFlip();
}
}
@ -67,7 +76,7 @@
function handleKeydown(e: KeyboardEvent) {
if (!open) {
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle();
}
@ -75,22 +84,22 @@
}
switch (e.key) {
case 'ArrowDown':
case "ArrowDown":
e.preventDefault();
focusIndex = Math.min(focusIndex + 1, flatItems.length - 1);
break;
case 'ArrowUp':
case "ArrowUp":
e.preventDefault();
focusIndex = Math.max(focusIndex - 1, 0);
break;
case 'Enter':
case ' ':
case "Enter":
case " ":
e.preventDefault();
if (focusIndex >= 0 && focusIndex < flatItems.length) {
select(flatItems[focusIndex].value);
}
break;
case 'Escape':
case "Escape":
e.preventDefault();
close();
triggerRef?.focus();
@ -120,13 +129,24 @@
</button>
<!-- svelte-ignore a11y_no_static_element_interactions a11y-no-static-element-interactions -->
<div class="dd-backdrop" style:display={open ? 'block' : 'none'} onclick={handleBackdropClick} onkeydown={e => e.key === 'Escape' && close()}></div>
<div
class="dd-backdrop"
style:display={open ? "block" : "none"}
onclick={handleBackdropClick}
onkeydown={(e) => e.key === "Escape" && close()}
></div>
<div class="dd-menu" class:flip-up={flipUp} style:display={open ? 'flex' : 'none'} bind:this={menuRef} role="listbox">
{#if groupBy && groups().length > 0}
{#each groups() as group}
<div
class="dd-menu"
class:flip-up={flipUp}
style:display={open ? "flex" : "none"}
bind:this={menuRef}
role="listbox"
>
{#if groupBy && groups.length > 0}
{#each groups as group}
<div class="dd-group-header">{group}</div>
{#each flatItems.filter(i => i.group === group) as item, idx}
{#each flatItems.filter((i) => i.group === group) as item, idx}
{@const globalIdx = flatItems.indexOf(item)}
<button
class="dd-option"
@ -134,12 +154,12 @@
class:focused={globalIdx === focusIndex}
role="option"
aria-selected={item.value === selected}
onclick={() => select(item.value)}
>{item.label}</button>
onclick={() => select(item.value)}>{item.label}</button
>
{/each}
{/each}
<!-- Ungrouped items -->
{#each flatItems.filter(i => !i.group) as item}
{#each flatItems.filter((i) => !i.group) as item}
{@const globalIdx = flatItems.indexOf(item)}
<button
class="dd-option"
@ -147,8 +167,8 @@
class:focused={globalIdx === focusIndex}
role="option"
aria-selected={item.value === selected}
onclick={() => select(item.value)}
>{item.label}</button>
onclick={() => select(item.value)}>{item.label}</button
>
{/each}
{:else}
{#each flatItems as item, idx}
@ -158,48 +178,89 @@
class:focused={idx === focusIndex}
role="option"
aria-selected={item.value === selected}
onclick={() => select(item.value)}
>{item.label}</button>
onclick={() => select(item.value)}>{item.label}</button
>
{/each}
{/if}
</div>
</div>
<style>
.dd-root { position: relative; width: 100%; }
.dd-root {
position: relative;
width: 100%;
}
.dd-trigger {
width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 0.375rem;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family);
cursor: pointer; text-align: left;
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
color: var(--ctp-text);
font-size: 0.8125rem;
font-family: var(--ui-font-family);
cursor: pointer;
text-align: left;
transition: border-color 0.12s;
}
.dd-trigger:hover:not(.disabled) { border-color: var(--ctp-surface2); }
.dd-trigger:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -1px; }
.dd-trigger.open { border-color: var(--ctp-blue); }
.dd-trigger.disabled { opacity: 0.4; cursor: not-allowed; }
.dd-trigger:hover:not(.disabled) {
border-color: var(--ctp-surface2);
}
.dd-trigger:focus-visible {
outline: 2px solid var(--ctp-blue);
outline-offset: -1px;
}
.dd-trigger.open {
border-color: var(--ctp-blue);
}
.dd-trigger.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.dd-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-trigger-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dd-trigger :global(.dd-chev) {
color: var(--ctp-overlay1); flex-shrink: 0;
color: var(--ctp-overlay1);
flex-shrink: 0;
transition: transform 0.15s;
}
.dd-trigger :global(.dd-chev-open) { transform: rotate(180deg); }
.dd-trigger :global(.dd-chev-open) {
transform: rotate(180deg);
}
.dd-backdrop {
position: fixed; inset: 0; z-index: 49; background: transparent;
position: fixed;
inset: 0;
z-index: 49;
background: transparent;
}
.dd-menu {
position: absolute; top: calc(100% + 0.25rem); left: 0; right: 0; z-index: 50;
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
right: 0;
z-index: 50;
flex-direction: column;
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
background: var(--ctp-mantle);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
padding: 0.25rem;
max-height: 14rem; overflow-y: auto;
box-shadow: 0 0.5rem 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
max-height: 14rem;
overflow-y: auto;
box-shadow: 0 0.5rem 1rem
color-mix(in srgb, var(--ctp-crust) 60%, transparent);
}
.dd-menu.flip-up {
top: auto;
@ -208,22 +269,43 @@
}
.dd-group-header {
padding: 0.25rem 0.5rem 0.125rem; font-size: 0.625rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em; color: var(--ctp-overlay0);
padding: 0.25rem 0.5rem 0.125rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ctp-overlay0);
border-top: 1px solid var(--ctp-surface0);
}
.dd-group-header:first-child { border-top: none; }
.dd-group-header:first-child {
border-top: none;
}
.dd-option {
width: 100%; padding: 0.3rem 0.5rem; background: none; border: none;
border-radius: 0.2rem; color: var(--ctp-subtext1); font-size: 0.8125rem;
font-family: var(--ui-font-family); cursor: pointer; text-align: left;
width: 100%;
padding: 0.3rem 0.5rem;
background: none;
border: none;
border-radius: 0.2rem;
color: var(--ctp-subtext1);
font-size: 0.8125rem;
font-family: var(--ui-font-family);
cursor: pointer;
text-align: left;
transition: background 0.08s;
}
.dd-option:hover, .dd-option.focused { background: var(--ctp-surface0); color: var(--ctp-text); }
.dd-option:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -1px; }
.dd-option:hover,
.dd-option.focused {
background: var(--ctp-surface0);
color: var(--ctp-text);
}
.dd-option:focus-visible {
outline: 2px solid var(--ctp-blue);
outline-offset: -1px;
}
.dd-option.selected {
background: color-mix(in srgb, var(--ctp-blue) 15%, transparent);
color: var(--ctp-blue); font-weight: 500;
color: var(--ctp-blue);
font-weight: 500;
}
</style>