jedmund-svelte/src/lib/components/admin/DropdownMenu.svelte

129 lines
2.6 KiB
Svelte

<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { browser } from '$app/environment'
interface Props {
isOpen: boolean
triggerElement?: HTMLElement
items: DropdownItem[]
onClose?: () => void
}
interface DropdownItem {
id: string
label: string
action: () => void
variant?: 'default' | 'danger'
divider?: boolean
}
let { isOpen = $bindable(), triggerElement, items, onClose }: Props = $props()
let dropdownElement: HTMLDivElement
const dispatch = createEventDispatcher()
// Calculate position dynamically when needed
const position = $derived(() => {
if (!isOpen || !triggerElement || !browser) {
return { top: 0, left: 0 }
}
const rect = triggerElement.getBoundingClientRect()
const dropdownWidth = 180
return {
top: rect.bottom + 4,
left: rect.right - dropdownWidth
}
})
function handleItemClick(item: DropdownItem, event: MouseEvent) {
event.stopPropagation()
item.action()
isOpen = false
onClose?.()
}
function handleOutsideClick(event: MouseEvent) {
if (!dropdownElement || !isOpen) return
const target = event.target as HTMLElement
if (!dropdownElement.contains(target) && !triggerElement?.contains(target)) {
isOpen = false
onClose?.()
}
}
$effect(() => {
if (browser && isOpen) {
document.addEventListener('click', handleOutsideClick)
return () => {
document.removeEventListener('click', handleOutsideClick)
}
}
})
</script>
{#if isOpen && browser}
<div
bind:this={dropdownElement}
class="dropdown-menu"
style="top: {position().top}px; left: {position().left}px"
>
{#each items as item}
{#if item.divider}
<div class="dropdown-divider"></div>
{:else}
<button
class="dropdown-item"
class:danger={item.variant === 'danger'}
onclick={(e) => handleItemClick(item, e)}
>
{item.label}
</button>
{/if}
{/each}
</div>
{/if}
<style lang="scss">
@import '$styles/variables.scss';
.dropdown-menu {
position: fixed;
background: white;
border: 1px solid $grey-85;
border-radius: $unit;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
min-width: 180px;
z-index: 1050;
}
.dropdown-item {
width: 100%;
padding: $unit-2x $unit-3x;
background: none;
border: none;
text-align: left;
font-size: 0.875rem;
color: $grey-20;
cursor: pointer;
transition: background-color 0.2s ease;
display: block;
&:hover {
background-color: $grey-95;
}
&.danger {
color: $red-60;
}
}
.dropdown-divider {
height: 1px;
background-color: $grey-80;
margin: $unit-half 0;
}
</style>