import { customElement, type TemplateResult, property, state, html, css, cssManager, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { demoFunc } from './dees-input-dropdown.demo.js'; import { DeesInputBase } from './dees-input-base.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-dropdown': DeesInputDropdown; } } @customElement('dees-input-dropdown') export class DeesInputDropdown extends DeesInputBase { public static demo = demoFunc; // INSTANCE @property() public options: { option: string; key: string; payload?: any }[] = []; @property() public selectedOption: { option: string; key: string; payload?: any } = null; // Add value property for form compatibility public get value() { return this.selectedOption; } public set value(val: { option: string; key: string; payload?: any }) { this.selectedOption = val; } @property({ type: Boolean, }) public enableSearch: boolean = true; @state() public opensToTop: boolean = false; @state() private filteredOptions: { option: string; key: string; payload?: any }[] = []; @state() private highlightedIndex: number = 0; @state() public isOpened = false; @state() private searchValue: string = ''; public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` * { box-sizing: border-box; } :host { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; position: relative; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; } .maincontainer { display: block; position: relative; } .selectedBox { user-select: none; position: relative; width: 100%; height: 40px; line-height: 38px; padding: 0 40px 0 12px; background: transparent; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 6px; transition: all 0.15s ease; font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .selectedBox:hover:not(.disabled) { border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; } .selectedBox:focus-visible { outline: none; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')}; } .selectedBox.disabled { background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; cursor: not-allowed; opacity: 0.5; } /* Dropdown arrow */ .selectedBox::after { content: ''; position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; transition: transform 0.15s ease; } .selectedBox.open::after { transform: translateY(-50%) rotate(180deg); } .selectionBox { will-change: transform, opacity; pointer-events: none; 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; position: absolute; user-select: none; margin-top: 4px; z-index: 50; left: 0; right: 0; } .selectionBox.top { bottom: calc(100% + 4px); top: auto; margin-top: 0; margin-bottom: 4px; transform: translateY(8px) scale(0.98); } .selectionBox.bottom { top: 100%; } .selectionBox.show { pointer-events: all; transform: translateY(0) scale(1); opacity: 1; } /* Options container */ .options-container { max-height: 250px; overflow-y: auto; padding: 4px; } /* Options */ .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 message */ .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 */ .search { padding: 4px; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; margin-bottom: 4px; } .search.bottom { border-bottom: none; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; margin-bottom: 0; margin-top: 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%)')}; } /* Scrollbar styling */ .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 { return html`
${this.selectedOption?.option || 'Select an option'}
${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}
`; }) }
`; } async connectedCallback() { super.connectedCallback(); this.handleClickOutside = this.handleClickOutside.bind(this); } firstUpdated() { this.selectedOption = this.selectedOption || null; this.filteredOptions = this.options; } updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('options')) { this.filteredOptions = this.options; } } public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) { this.selectedOption = selectedOption; this.isOpened = false; this.searchValue = ''; this.filteredOptions = this.options; this.highlightedIndex = 0; this.dispatchEvent( new CustomEvent('selectedOption', { detail: selectedOption, bubbles: true, }) ); this.changeSubject.next(this); } private handleClickOutside = (event: MouseEvent) => { const path = event.composedPath(); if (!path.includes(this)) { this.isOpened = false; this.searchValue = ''; this.filteredOptions = this.options; document.removeEventListener('click', this.handleClickOutside); } }; public async toggleSelectionBox() { this.isOpened = !this.isOpened; if (this.isOpened) { // Check available space and set position const selectedBox = this.shadowRoot.querySelector('.selectedBox') as HTMLElement; const rect = selectedBox.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; // Determine if we should open upwards this.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow; // Focus search input if present await this.updateComplete; const searchInput = this.shadowRoot.querySelector('.search input') as HTMLInputElement; if (searchInput) { searchInput.focus(); } // Add click outside listener setTimeout(() => { document.addEventListener('click', this.handleClickOutside); }, 0); } else { // Cleanup this.searchValue = ''; this.filteredOptions = this.options; document.removeEventListener('click', this.handleClickOutside); } } 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 handleKeyDown(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.updateSelection(this.filteredOptions[this.highlightedIndex]); } } else if (key === 'Escape') { event.preventDefault(); this.isOpened = false; } } private handleSearchKeydown(event: KeyboardEvent): void { if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') { this.handleKeyDown(event); } } private handleSelectedBoxKeydown(event: KeyboardEvent) { if (this.disabled) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); this.toggleSelectionBox(); } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); if (!this.isOpened) { this.toggleSelectionBox(); } } else if (event.key === 'Escape') { event.preventDefault(); if (this.isOpened) { this.isOpened = false; } } } public getValue(): { option: string; key: string; payload?: any } { return this.selectedOption; } public setValue(value: { option: string; key: string; payload?: any }): void { this.selectedOption = value; } async disconnectedCallback() { await super.disconnectedCallback(); document.removeEventListener('click', this.handleClickOutside); } }