import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS, state, } from '@design.estate/dees-element'; // Import design tokens import { colors, bdTheme } from './00colors.js'; import { spacing, radius, shadows, transitions } from './00tokens.js'; import { fontFamilies } from './00fonts.js'; export interface IDropdownMenuItem { id: string; label: string; icon?: string; divider?: boolean; destructive?: boolean; disabled?: boolean; } declare global { interface HTMLElementTagNameMap { 'sio-dropdown-menu': SioDropdownMenu; } } @customElement('sio-dropdown-menu') export class SioDropdownMenu extends DeesElement { public static demo = () => html`
`; @property({ type: Array }) public items: IDropdownMenuItem[] = []; @property({ type: String }) public align: 'left' | 'right' = 'right'; @state() private isOpen: boolean = false; private documentClickHandler: (e: MouseEvent) => void; private scrollHandler: () => void; private resizeHandler: () => void; public static styles = [ cssManager.defaultStyles, css` :host { position: relative; display: inline-block; font-family: ${unsafeCSS(fontFamilies.sans)}; } .trigger { cursor: pointer; } .dropdown { position: absolute; top: calc(100% + 10px); right: 0; min-width: 200px; background: ${bdTheme('background')}; border: 1px solid ${bdTheme('border')}; border-radius: ${unsafeCSS(radius.lg)}; box-shadow: ${unsafeCSS(shadows.lg)}; overflow: hidden; z-index: 100000; opacity: 0; transform: translateY(-10px) scale(0.95); pointer-events: none; transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1); transform-origin: top right; } .dropdown.align-left { right: auto; left: 0; transform-origin: top left; } .dropdown.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: all; } .menu-item { display: flex; align-items: center; gap: ${unsafeCSS(spacing["3"])}; padding: ${unsafeCSS(spacing["2.5"])} ${unsafeCSS(spacing["3"])}; font-size: 0.875rem; line-height: 1.5; color: ${bdTheme('foreground')}; cursor: pointer; transition: ${unsafeCSS(transitions.all)}; user-select: none; border: none; background: none; width: 100%; text-align: left; } .menu-item:hover:not(.disabled) { background: ${bdTheme('accent')}; } .menu-item:active:not(.disabled) { transform: scale(0.98); } .menu-item.disabled { opacity: 0.5; cursor: not-allowed; } .menu-item.destructive { color: ${bdTheme('destructive')}; } .menu-item.destructive:hover:not(.disabled) { background: ${bdTheme('destructive')}10; } .menu-icon { flex-shrink: 0; color: ${bdTheme('mutedForeground')}; } .menu-item.destructive .menu-icon { color: ${bdTheme('destructive')}; } .menu-label { flex: 1; } .divider { height: 1px; background: ${bdTheme('border')}; margin: ${unsafeCSS(spacing["1"])} 0; } /* Mobile adjustments */ @media (max-width: 600px) { .dropdown { position: fixed; top: auto !important; left: ${unsafeCSS(spacing["4"])} !important; right: ${unsafeCSS(spacing["4"])}; bottom: ${unsafeCSS(spacing["4"])}; width: auto; transform-origin: bottom center; } .dropdown.open { transform: translateY(0) scale(1); } .dropdown:not(.open) { transform: translateY(10px) scale(0.95); } } `, ]; public render(): TemplateResult { return html`
`; } private toggleDropdown = (e: Event) => { console.log('[Dropdown] Toggle called, current state:', this.isOpen); e.preventDefault(); e.stopPropagation(); this.isOpen = !this.isOpen; console.log('[Dropdown] New state:', this.isOpen); if (this.isOpen) { this.addDocumentListener(); } else { this.removeDocumentListener(); } } private updateDropdownPosition() { // For absolute positioning, we don't need to calculate position dynamically // The CSS handles it with top: calc(100% + 10px) and right: 0 console.log('[Dropdown] Position is handled by CSS (absolute positioning)'); } private handleItemClick(item: IDropdownMenuItem) { if (item.disabled || item.divider) return; this.isOpen = false; this.removeDocumentListener(); // Dispatch custom event with the selected item this.dispatchEvent(new CustomEvent('item-selected', { detail: { item }, bubbles: true, composed: true })); } private addDocumentListener() { // Close dropdown when clicking outside this.documentClickHandler = (e: MouseEvent) => { const path = e.composedPath(); if (!path.includes(this)) { this.isOpen = false; this.removeDocumentListener(); } }; // Update position on scroll/resize this.scrollHandler = () => this.updateDropdownPosition(); this.resizeHandler = () => { this.updateDropdownPosition(); if (window.innerWidth <= 600) { // Close on mobile resize to prevent positioning issues this.isOpen = false; this.removeDocumentListener(); } }; // Delay to avoid immediate closing setTimeout(() => { document.addEventListener('click', this.documentClickHandler); window.addEventListener('scroll', this.scrollHandler, true); window.addEventListener('resize', this.resizeHandler); }, 0); } private removeDocumentListener() { if (this.documentClickHandler) { document.removeEventListener('click', this.documentClickHandler); } if (this.scrollHandler) { window.removeEventListener('scroll', this.scrollHandler, true); } if (this.resizeHandler) { window.removeEventListener('resize', this.resizeHandler); } } public async disconnectedCallback() { await super.disconnectedCallback(); this.removeDocumentListener(); } public close() { this.isOpen = false; this.removeDocumentListener(); } }