import { customElement, type TemplateResult, property, state, html, css, cssManager, } from '@design.estate/dees-element'; import { demoFunc } from './dees-input-dropdown.demo.js'; import { DeesInputBase } from '../dees-input-base/dees-input-base.js'; import { cssGeistFontFamily } from '../../00fonts.js'; import { themeDefaultStyles } from '../../00theme.js'; import { DeesInputDropdownPopup } from './dees-input-dropdown-popup.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-dropdown': DeesInputDropdown; } } @customElement('dees-input-dropdown') export class DeesInputDropdown extends DeesInputBase { public static demo = demoFunc; public static demoGroups = ['Input']; // INSTANCE @property() accessor options: { option: string; key: string; payload?: any }[] = []; @property() accessor selectedOption: { option: string; key: string; payload?: any } | null = null; // Add value property for form compatibility public get value() { return this.selectedOption; } public set value(val: { option: string; key: string; payload?: any } | null) { this.selectedOption = val; } @property({ type: Boolean, }) accessor enableSearch: boolean = true; @state() accessor isOpened = false; private popupInstance: DeesInputDropdownPopup | null = null; public static styles = [ themeDefaultStyles, ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` * { box-sizing: border-box; } :host { font-family: ${cssGeistFontFamily}; 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); } `, ]; public render(): TemplateResult { return html`
${this.selectedOption?.option || 'Select an option'}
`; } firstUpdated() { this.selectedOption = this.selectedOption || null; } updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('options') && this.popupInstance && this.isOpened) { this.popupInstance.updateOptions(this.options); } } public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) { this.selectedOption = selectedOption; this.closePopup(); this.dispatchEvent( new CustomEvent('selectedOption', { detail: selectedOption, bubbles: true, }) ); this.changeSubject.next(this); } public async toggleSelectionBox() { if (this.isOpened) { this.closePopup(); return; } this.isOpened = true; // Get trigger position const selectedBox = this.shadowRoot!.querySelector('.selectedBox') as HTMLElement; const rect = selectedBox.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow; // Create popup if needed if (!this.popupInstance) { this.popupInstance = new DeesInputDropdownPopup(); } // Configure popup this.popupInstance.options = this.options; this.popupInstance.enableSearch = this.enableSearch; this.popupInstance.opensToTop = opensToTop; this.popupInstance.triggerRect = rect; this.popupInstance.ownerComponent = this; // Listen for popup events this.popupInstance.addEventListener('option-selected', this.handleOptionSelected); this.popupInstance.addEventListener('close-request', this.handleCloseRequest); this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest); // Show popup (creates window layer, appends to document.body) await this.popupInstance.show(); // Focus search input if (this.enableSearch) { await this.popupInstance.focusSearchInput(); } } private closePopup(): void { this.isOpened = false; if (this.popupInstance) { this.popupInstance.removeEventListener('option-selected', this.handleOptionSelected); this.popupInstance.removeEventListener('close-request', this.handleCloseRequest); this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest); this.popupInstance.hide(); } } private handleOptionSelected = (event: Event): void => { const detail = (event as CustomEvent).detail; this.updateSelection(detail); }; private handleCloseRequest = (): void => { this.closePopup(); }; private handleRepositionRequest = (): void => { if (!this.popupInstance || !this.isOpened) return; const selectedBox = this.shadowRoot!.querySelector('.selectedBox') as HTMLElement; if (!selectedBox) return; const rect = selectedBox.getBoundingClientRect(); // Close if trigger scrolled off-screen if (rect.bottom < 0 || rect.top > window.innerHeight) { this.closePopup(); return; } // Update position const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; this.popupInstance.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow; this.popupInstance.triggerRect = rect; }; 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.closePopup(); } } } public getValue(): { option: string; key: string; payload?: any } | null { return this.selectedOption; } public setValue(value: { option: string; key: string; payload?: any }): void { this.selectedOption = value; } async disconnectedCallback() { await super.disconnectedCallback(); if (this.popupInstance) { this.popupInstance.removeEventListener('option-selected', this.handleOptionSelected); this.popupInstance.removeEventListener('close-request', this.handleCloseRequest); this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest); this.popupInstance.hide(); this.popupInstance = null; } } }