fix(dees-input-dropdown): improve dropdown popup lifecycle with window layer overlay and animated visibility transitions

This commit is contained in:
2026-04-04 23:47:13 +00:00
parent 395e0fa3da
commit b1c174a4e2
4 changed files with 53 additions and 21 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-04-04 - 3.56.1 - fix(dees-input-dropdown)
improve dropdown popup lifecycle with window layer overlay and animated visibility transitions
- use a window layer to handle outside-click closing instead of document-level mousedown listeners
- await popup show and search focus to keep popup initialization and overlay setup in sync
- add guarded async hide logic with animated teardown and cleanup of scroll/resize listeners
## 2026-04-04 - 3.56.0 - feat(dees-input-dropdown) ## 2026-04-04 - 3.56.0 - feat(dees-input-dropdown)
extract dropdown popup into a floating overlay component with search, keyboard navigation, and viewport repositioning extract dropdown popup into a floating overlay component with search, keyboard navigation, and viewport repositioning

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '3.56.0', version: '3.56.1',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
} }

View File

@@ -8,9 +8,11 @@ import {
cssManager, cssManager,
DeesElement, DeesElement,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { zIndexRegistry } from '../../00zindex.js'; import { zIndexRegistry } from '../../00zindex.js';
import { cssGeistFontFamily } from '../../00fonts.js'; import { cssGeistFontFamily } from '../../00fonts.js';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -47,6 +49,12 @@ export class DeesInputDropdownPopup extends DeesElement {
@state() @state()
accessor menuZIndex: number = 1000; accessor menuZIndex: number = 1000;
@state()
accessor visible: boolean = false;
private windowLayer: DeesWindowLayer | null = null;
private isDestroying: boolean = false;
public static styles = [ public static styles = [
themeDefaultStyles, themeDefaultStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
@@ -182,7 +190,7 @@ export class DeesInputDropdownPopup extends DeesElement {
return html` return html`
<div <div
class="selectionBox show ${this.opensToTop ? 'top' : 'bottom'}" class="selectionBox ${this.visible ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}"
style="${posStyle}; z-index: ${this.menuZIndex};" style="${posStyle}; z-index: ${this.menuZIndex};"
> >
${this.enableSearch ${this.enableSearch
@@ -233,30 +241,43 @@ export class DeesInputDropdownPopup extends DeesElement {
} }
} }
public show(): void { public async show(): Promise<void> {
this.filteredOptions = this.options; this.filteredOptions = this.options;
this.highlightedIndex = 0; this.highlightedIndex = 0;
this.searchValue = ''; this.searchValue = '';
// Create window layer (transparent, no blur)
this.windowLayer = await DeesWindowLayer.createAndShow();
this.windowLayer.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('close-request'));
});
// Set z-index above the window layer
this.menuZIndex = zIndexRegistry.getNextZIndex(); this.menuZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(this, this.menuZIndex); zIndexRegistry.register(this, this.menuZIndex);
this.style.zIndex = this.menuZIndex.toString(); this.style.zIndex = this.menuZIndex.toString();
document.body.appendChild(this); document.body.appendChild(this);
// Add listeners // Animate in on next frame
await domtools.plugins.smartdelay.delayFor(0);
this.visible = true;
// Add scroll/resize listeners for repositioning
window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true }); window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
window.addEventListener('resize', this.handleScrollOrResize, { passive: true }); window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
setTimeout(() => {
document.addEventListener('mousedown', this.handleOutsideClick);
}, 0);
} }
public hide(): void { public async hide(): Promise<void> {
// Remove listeners // Guard against double-destruction
if (this.isDestroying) {
return;
}
this.isDestroying = true;
// Remove scroll/resize listeners
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions); window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
window.removeEventListener('resize', this.handleScrollOrResize); window.removeEventListener('resize', this.handleScrollOrResize);
document.removeEventListener('mousedown', this.handleOutsideClick);
zIndexRegistry.unregister(this); zIndexRegistry.unregister(this);
@@ -264,9 +285,21 @@ export class DeesInputDropdownPopup extends DeesElement {
this.filteredOptions = this.options; this.filteredOptions = this.options;
this.highlightedIndex = 0; this.highlightedIndex = 0;
// Don't await - let window layer cleanup happen in background for instant visual feedback
if (this.windowLayer) {
this.windowLayer.destroy();
this.windowLayer = null;
}
// Animate out via CSS transition
this.visible = false;
await domtools.plugins.smartdelay.delayFor(150);
if (this.parentElement) { if (this.parentElement) {
this.parentElement.removeChild(this); this.parentElement.removeChild(this);
} }
this.isDestroying = false;
} }
public async focusSearchInput(): Promise<void> { public async focusSearchInput(): Promise<void> {
@@ -328,13 +361,6 @@ export class DeesInputDropdownPopup extends DeesElement {
} }
}; };
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 => { private handleScrollOrResize = (): void => {
this.dispatchEvent(new CustomEvent('reposition-request')); this.dispatchEvent(new CustomEvent('reposition-request'));
}; };
@@ -343,7 +369,6 @@ export class DeesInputDropdownPopup extends DeesElement {
await super.disconnectedCallback(); await super.disconnectedCallback();
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions); window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
window.removeEventListener('resize', this.handleScrollOrResize); window.removeEventListener('resize', this.handleScrollOrResize);
document.removeEventListener('mousedown', this.handleOutsideClick);
zIndexRegistry.unregister(this); zIndexRegistry.unregister(this);
} }
} }

View File

@@ -205,12 +205,12 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
this.popupInstance.addEventListener('close-request', this.handleCloseRequest); this.popupInstance.addEventListener('close-request', this.handleCloseRequest);
this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest); this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest);
// Show popup (appends to document.body) // Show popup (creates window layer, appends to document.body)
this.popupInstance.show(); await this.popupInstance.show();
// Focus search input // Focus search input
if (this.enableSearch) { if (this.enableSearch) {
this.popupInstance.focusSearchInput(); await this.popupInstance.focusSearchInput();
} }
} }