fix(electrobun): CustomDropdown groups .by + remove function call syntax
This commit is contained in:
parent
bd48a09fd8
commit
e8278ef444
1 changed files with 131 additions and 49 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue