component Dropdown { outputs { toggle(payload: { open: boolean; sourceEvent: Event; reason?: object }) open(payload: { sourceEvent: Event }) close(payload: { reason: string; sourceEvent: Event }) select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event }) action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event }) } props { items: unknown[] = [] open: boolean = false defaultOpen: boolean = false label: string = "Open menu" icon: string = "" showChevron: boolean = true menuLabel: string = "Dropdown menu" placement: string = "bottom-start" width: string = "md" size: string = "default" color: string = "primary" variant: string = "raised" closeOnSelect: boolean = true closeOnOutside: boolean = true disabled: boolean = false emptyLabel: string = "No menu items" class: string = "" } state visible = defaultOpen functions { shared function isOpen() { return open || visible } client function showMenu(sourceEvent) { if (disabled) { return } visible = true output.open({ sourceEvent: sourceEvent }) output.toggle({ open: true, sourceEvent: sourceEvent }) } client function hideMenu(reason, sourceEvent) { visible = false output.close({ reason: reason, sourceEvent: sourceEvent }) output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent }) } client function toggleMenu(sourceEvent) { if (isOpen()) { hideMenu("toggle", sourceEvent) } else { showMenu(sourceEvent) } } client function chooseItem(item, itemIndex, sourceEvent) { if (disabled || item.disabled || item.type === "divider" || item.type === "header") { sourceEvent.preventDefault() return } output.select({ item: item, itemIndex: itemIndex, value: item.value || "", sourceEvent: sourceEvent }) if (item.action) { output.action({ item: item, itemIndex: itemIndex, value: item.value || "", sourceEvent: sourceEvent }) } if (closeOnSelect) { hideMenu("select", sourceEvent) } } client function moveFocus(sourceEvent, direction) { const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")] if (!options.length) { return } const activeIndex = options.indexOf(document.activeElement) const nextIndex = (activeIndex + direction + options.length) % options.length options[nextIndex].focus() } client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Escape") { sourceEvent.preventDefault() hideMenu("escape", sourceEvent) } else if (sourceEvent.key === "ArrowDown") { sourceEvent.preventDefault() if (!isOpen()) { showMenu(sourceEvent) } moveFocus(sourceEvent, 1) } else if (sourceEvent.key === "ArrowUp") { sourceEvent.preventDefault() if (!isOpen()) { showMenu(sourceEvent) } moveFocus(sourceEvent, -1) } else if (sourceEvent.key === "Home" && isOpen()) { sourceEvent.preventDefault() const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget const first = root.querySelector("[data-dropdown-item]:not([disabled])") if (first) { first.focus() } } else if (sourceEvent.key === "End" && isOpen()) { sourceEvent.preventDefault() const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")] if (options.length) { options[options.length - 1].focus() } } } } view {
{#if closeOnOutside} {/if}
} style { .wire-dropdown { --dropdown-accent: var(--wire-color-primary); --dropdown-soft: var(--wire-color-primary-soft); position: relative; display: inline-flex; min-width: 0; } .wire-dropdown[data-color="secondary"] { --dropdown-accent: var(--wire-color-secondary); --dropdown-soft: var(--wire-color-secondary-soft); } .wire-dropdown[data-color="info"] { --dropdown-accent: var(--wire-color-info); --dropdown-soft: var(--wire-color-info-soft); } .wire-dropdown[data-color="success"] { --dropdown-accent: var(--wire-color-success); --dropdown-soft: var(--wire-color-success-soft); } .wire-dropdown[data-color="warning"] { --dropdown-accent: var(--wire-color-warning); --dropdown-soft: var(--wire-color-warning-soft); } .wire-dropdown[data-color="danger"] { --dropdown-accent: var(--wire-color-danger); --dropdown-soft: var(--wire-color-danger-soft); } .wire-dropdown__trigger { appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 0.55rem; min-height: 2.55rem; padding: 0.65rem 0.9rem; color: var(--wire-color-text); background: var(--wire-color-surface-raised); border: 1px solid var(--wire-color-border); border-radius: 0.78rem; font: inherit; font-size: 0.84rem; font-weight: 600; cursor: pointer; transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease; } .wire-dropdown__trigger:hover, .wire-dropdown__trigger:focus-visible, .wire-dropdown[data-open="true"] .wire-dropdown__trigger { color: var(--dropdown-accent); background: var(--dropdown-soft); border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border)); outline: none; } .wire-dropdown__trigger:disabled { opacity: 0.5; cursor: not-allowed; } .wire-dropdown__trigger-icon, .wire-dropdown__chevron { width: 1rem; height: 1rem; } .wire-dropdown__chevron { transition: transform 160ms ease; } .wire-dropdown[data-open="true"] .wire-dropdown__chevron { transform: rotate(180deg); } .wire-dropdown[data-size="sm"] .wire-dropdown__trigger { min-height: 2.2rem; padding: 0.5rem 0.72rem; font-size: 0.78rem; } .wire-dropdown[data-size="lg"] .wire-dropdown__trigger { min-height: 2.9rem; padding: 0.78rem 1.05rem; font-size: 0.9rem; } .wire-dropdown__dismiss-layer { position: fixed; inset: 0; z-index: 1100; appearance: none; padding: 0; background: transparent; border: 0; } .wire-dropdown__panel { position: absolute; z-index: 1101; top: calc(100% + 0.55rem); left: 0; width: max-content; min-width: 12rem; max-width: min(22rem, calc(100vw - 1.5rem)); padding: 0.45rem; color: var(--wire-color-text); background: linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%), color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent); border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wire-color-border)); border-radius: 1rem; box-shadow: 0 1px 0 color-mix(in srgb, white 5%, transparent) inset, 0 24px 64px color-mix(in srgb, black 22%, transparent); backdrop-filter: blur(18px); } .wire-dropdown[data-width="trigger"] .wire-dropdown__panel { width: 100%; min-width: 100%; } .wire-dropdown[data-width="sm"] .wire-dropdown__panel { width: 12rem; } .wire-dropdown[data-width="md"] .wire-dropdown__panel { width: 17rem; } .wire-dropdown[data-width="lg"] .wire-dropdown__panel { width: 22rem; } .wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel { right: 0; left: auto; } .wire-dropdown[data-placement="top-start"] .wire-dropdown__panel { top: auto; bottom: calc(100% + 0.55rem); } .wire-dropdown[data-placement="top-end"] .wire-dropdown__panel { top: auto; right: 0; bottom: calc(100% + 0.55rem); left: auto; } .wire-dropdown[data-variant="soft"] .wire-dropdown__panel { background: linear-gradient(145deg, var(--dropdown-soft), transparent 70%), var(--wire-color-surface-raised); box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent); } .wire-dropdown[data-variant="outline"] .wire-dropdown__panel { background: var(--wire-color-surface-raised); box-shadow: none; } .wire-dropdown__items { display: grid; gap: 0.15rem; } .wire-dropdown__section-label { padding: 0.55rem 0.72rem 0.3rem; color: var(--wire-color-text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; } .wire-dropdown__divider { height: 1px; margin: 0.3rem 0.4rem; background: var(--wire-color-border); } .wire-dropdown__item { appearance: none; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.7rem; width: 100%; min-width: 0; padding: 0.68rem 0.72rem; color: var(--wire-color-text); background: transparent; border: 0; border-radius: 0.72rem; font: inherit; text-align: left; text-decoration: none; cursor: pointer; transition: background-color 150ms ease, color 150ms ease, transform 150ms ease; } .wire-dropdown__item:hover, .wire-dropdown__item:focus-visible, .wire-dropdown__item[data-selected="true"] { color: var(--dropdown-accent); background: var(--dropdown-soft); outline: none; } .wire-dropdown__item:active { transform: scale(0.985); } .wire-dropdown__item[data-danger="true"] { color: var(--wire-color-danger); } .wire-dropdown__item[data-danger="true"]:hover, .wire-dropdown__item[data-danger="true"]:focus-visible { background: var(--wire-color-danger-soft); } .wire-dropdown__item[disabled], .wire-dropdown__item[aria-disabled="true"] { opacity: 0.48; pointer-events: none; } .wire-dropdown__item-icon, .wire-dropdown__status { width: 1rem; height: 1rem; } .wire-dropdown__copy { display: grid; gap: 0.12rem; min-width: 0; } .wire-dropdown__copy strong { overflow: hidden; font-size: 0.82rem; font-weight: 600; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; } .wire-dropdown__copy small { overflow: hidden; color: var(--wire-color-text-muted); font-size: 0.7rem; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; } .wire-dropdown__badge { padding: 0.18rem 0.45rem; color: var(--dropdown-accent); background: var(--dropdown-soft); border-radius: 999px; font-size: 0.64rem; font-weight: 700; } .wire-dropdown kbd { padding: 0.16rem 0.38rem; color: var(--wire-color-text-muted); background: var(--wire-color-surface-soft); border: 1px solid var(--wire-color-border); border-radius: 0.36rem; font-size: 0.64rem; font-family: inherit; } .wire-dropdown__empty { padding: 1rem; color: var(--wire-color-text-muted); font-size: 0.8rem; text-align: center; } @media (max-width: 639px) { .wire-dropdown__panel { position: fixed; right: 0.75rem; bottom: 0.75rem; left: 0.75rem; top: auto; width: auto; min-width: 0; max-width: none; } } @media (prefers-reduced-motion: reduce) { .wire-dropdown__trigger, .wire-dropdown__chevron, .wire-dropdown__item { transition: none; } } } }