fix(dees-input-dropdown): improve dropdown popup lifecycle with window layer overlay and animated visibility transitions
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user