import { customElement, type TemplateResult, property, state, html, css, cssManager, DeesElement, } from '@design.estate/dees-element'; import { zIndexRegistry } from '../../00zindex.js'; import { cssGeistFontFamily } from '../../00fonts.js'; import { themeDefaultStyles } from '../../00theme.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-dropdown-popup': DeesInputDropdownPopup; } } @customElement('dees-input-dropdown-popup') export class DeesInputDropdownPopup extends DeesElement { @property({ type: Array }) accessor options: { option: string; key: string; payload?: any }[] = []; @property({ type: Boolean }) accessor enableSearch: boolean = true; @property({ type: Boolean }) accessor opensToTop: boolean = false; @property({ attribute: false }) accessor triggerRect: DOMRect | null = null; @property({ attribute: false }) accessor ownerComponent: HTMLElement | null = null; @state() accessor filteredOptions: { option: string; key: string; payload?: any }[] = []; @state() accessor highlightedIndex: number = 0; @state() accessor searchValue: string = ''; @state() accessor menuZIndex: number = 1000; public static styles = [ themeDefaultStyles, cssManager.defaultStyles, css` :host { position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none; font-family: ${cssGeistFontFamily}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; } * { box-sizing: border-box; } .selectionBox { position: fixed; pointer-events: auto; will-change: transform, opacity; transition: all 0.15s ease; opacity: 0; transform: translateY(-8px) scale(0.98); background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1); min-height: 40px; max-height: 300px; overflow: hidden; border-radius: 6px; user-select: none; } .selectionBox.top { transform: translateY(8px) scale(0.98); } .selectionBox.show { pointer-events: auto; transform: translateY(0) scale(1); opacity: 1; } .options-container { max-height: 250px; overflow-y: auto; padding: 4px; } .option { transition: all 0.15s ease; line-height: 32px; padding: 0 8px; border-radius: 4px; margin: 2px 0; cursor: pointer; font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; } .option.highlighted { background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; } .option:hover { background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; } .no-options { padding: 8px; text-align: center; font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; font-style: italic; } .search { padding: 4px; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; margin-bottom: 4px; } .search input { display: block; width: 100%; height: 32px; padding: 0 8px; background: transparent; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 4px; color: inherit; font-size: 14px; font-family: inherit; outline: none; transition: border-color 0.15s ease; } .search input::placeholder { color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; } .search input:focus { border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; } .options-container::-webkit-scrollbar { width: 8px; } .options-container::-webkit-scrollbar-track { background: transparent; } .options-container::-webkit-scrollbar-thumb { background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 4px; } .options-container::-webkit-scrollbar-thumb:hover { background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; } `, ]; public render(): TemplateResult { if (!this.triggerRect) return html``; const posStyle = this.computePositionStyle(); return html`
${this.enableSearch ? html` ` : null}
${this.filteredOptions.length === 0 ? html`
No options found
` : this.filteredOptions.map((option, index) => { const isHighlighted = this.highlightedIndex === index; return html`
${option.option}
`; })}
`; } private computePositionStyle(): string { const rect = this.triggerRect!; const left = rect.left; const width = rect.width; if (this.opensToTop) { const bottom = window.innerHeight - rect.top + 4; return `left: ${left}px; width: ${width}px; bottom: ${bottom}px; top: auto`; } else { const top = rect.bottom + 4; return `left: ${left}px; width: ${width}px; top: ${top}px`; } } public show(): void { this.filteredOptions = this.options; this.highlightedIndex = 0; this.searchValue = ''; this.menuZIndex = zIndexRegistry.getNextZIndex(); zIndexRegistry.register(this, this.menuZIndex); this.style.zIndex = this.menuZIndex.toString(); document.body.appendChild(this); // Add listeners window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true }); window.addEventListener('resize', this.handleScrollOrResize, { passive: true }); setTimeout(() => { document.addEventListener('mousedown', this.handleOutsideClick); }, 0); } public hide(): void { // Remove listeners window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions); window.removeEventListener('resize', this.handleScrollOrResize); document.removeEventListener('mousedown', this.handleOutsideClick); zIndexRegistry.unregister(this); this.searchValue = ''; this.filteredOptions = this.options; this.highlightedIndex = 0; if (this.parentElement) { this.parentElement.removeChild(this); } } public async focusSearchInput(): Promise { await this.updateComplete; const input = this.shadowRoot!.querySelector('.search input') as HTMLInputElement; if (input) input.focus(); } public updateOptions(options: { option: string; key: string; payload?: any }[]): void { this.options = options; // Re-filter with current search value if (this.searchValue) { const searchLower = this.searchValue.toLowerCase(); this.filteredOptions = this.options.filter((opt) => opt.option.toLowerCase().includes(searchLower) ); } else { this.filteredOptions = this.options; } this.highlightedIndex = 0; } private selectOption(option: { option: string; key: string; payload?: any }): void { this.dispatchEvent( new CustomEvent('option-selected', { detail: option, }) ); } private handleSearch = (event: Event): void => { const searchTerm = (event.target as HTMLInputElement).value; this.searchValue = searchTerm; const searchLower = searchTerm.toLowerCase(); this.filteredOptions = this.options.filter((option) => option.option.toLowerCase().includes(searchLower) ); this.highlightedIndex = 0; }; private handleSearchKeydown = (event: KeyboardEvent): void => { const key = event.key; const maxIndex = this.filteredOptions.length - 1; if (key === 'ArrowDown') { event.preventDefault(); this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1; } else if (key === 'ArrowUp') { event.preventDefault(); this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1; } else if (key === 'Enter') { event.preventDefault(); if (this.filteredOptions[this.highlightedIndex]) { this.selectOption(this.filteredOptions[this.highlightedIndex]); } } else if (key === 'Escape') { event.preventDefault(); this.dispatchEvent(new CustomEvent('close-request')); } }; private handleOutsideClick = (event: MouseEvent): void => { const path = event.composedPath(); if (!path.includes(this) && (!this.ownerComponent || !path.includes(this.ownerComponent))) { this.dispatchEvent(new CustomEvent('close-request')); } }; private handleScrollOrResize = (): void => { this.dispatchEvent(new CustomEvent('reposition-request')); }; async disconnectedCallback() { await super.disconnectedCallback(); window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions); window.removeEventListener('resize', this.handleScrollOrResize); document.removeEventListener('mousedown', this.handleOutsideClick); zIndexRegistry.unregister(this); } }