diff --git a/src/components/dialog/dialog.component.ts b/src/components/dialog/dialog.component.ts index 1884c6b3..d55a07c3 100644 --- a/src/components/dialog/dialog.component.ts +++ b/src/components/dialog/dialog.component.ts @@ -75,6 +75,7 @@ export default class SlDialog extends ShoelaceElement { private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; public modal = new Modal(this); + private closeWatcher: CloseWatcher | null; @query('.dialog') dialog: HTMLElement; @query('.dialog__panel') panel: HTMLElement; @@ -112,6 +113,7 @@ export default class SlDialog extends ShoelaceElement { super.disconnectedCallback(); this.modal.deactivate(); unlockBodyScrolling(this); + this.closeWatcher?.destroy(); } private requestClose(source: 'close-button' | 'keyboard' | 'overlay') { @@ -130,10 +132,17 @@ export default class SlDialog extends ShoelaceElement { } private addOpenListeners() { - document.addEventListener('keydown', this.handleDocumentKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.requestClose('keyboard'); + } else { + document.addEventListener('keydown', this.handleDocumentKeyDown); + } } private removeOpenListeners() { + this.closeWatcher?.destroy(); document.removeEventListener('keydown', this.handleDocumentKeyDown); } diff --git a/src/components/drawer/drawer.component.ts b/src/components/drawer/drawer.component.ts index d3a84171..fa08c472 100644 --- a/src/components/drawer/drawer.component.ts +++ b/src/components/drawer/drawer.component.ts @@ -81,6 +81,7 @@ export default class SlDrawer extends ShoelaceElement { private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; public modal = new Modal(this); + private closeWatcher: CloseWatcher | null; @query('.drawer') drawer: HTMLElement; @query('.drawer__panel') panel: HTMLElement; @@ -129,6 +130,7 @@ export default class SlDrawer extends ShoelaceElement { disconnectedCallback() { super.disconnectedCallback(); unlockBodyScrolling(this); + this.closeWatcher?.destroy(); } private requestClose(source: 'close-button' | 'keyboard' | 'overlay') { @@ -147,11 +149,20 @@ export default class SlDrawer extends ShoelaceElement { } private addOpenListeners() { - document.addEventListener('keydown', this.handleDocumentKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + if (!this.contained) { + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.requestClose('keyboard'); + } + } else { + document.addEventListener('keydown', this.handleDocumentKeyDown); + } } private removeOpenListeners() { document.removeEventListener('keydown', this.handleDocumentKeyDown); + this.closeWatcher?.destroy(); } private handleDocumentKeyDown = (event: KeyboardEvent) => { diff --git a/src/components/dropdown/dropdown.component.ts b/src/components/dropdown/dropdown.component.ts index dc44ab83..2d8f778a 100644 --- a/src/components/dropdown/dropdown.component.ts +++ b/src/components/dropdown/dropdown.component.ts @@ -48,6 +48,7 @@ export default class SlDropdown extends ShoelaceElement { @query('.dropdown__panel') panel: HTMLSlotElement; private readonly localize = new LocalizeController(this); + private closeWatcher: CloseWatcher | null; /** * Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you @@ -149,7 +150,7 @@ export default class SlDropdown extends ShoelaceElement { private handleDocumentKeyDown = (event: KeyboardEvent) => { // Close when escape or tab is pressed - if (event.key === 'Escape' && this.open) { + if (event.key === 'Escape' && this.open && !this.closeWatcher) { event.stopPropagation(); this.focusOnTrigger(); this.hide(); @@ -334,7 +335,16 @@ export default class SlDropdown extends ShoelaceElement { addOpenListeners() { this.panel.addEventListener('sl-select', this.handlePanelSelect); - this.panel.addEventListener('keydown', this.handleKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => { + this.hide(); + this.focusOnTrigger(); + }; + } else { + this.panel.addEventListener('keydown', this.handleKeyDown); + } document.addEventListener('keydown', this.handleDocumentKeyDown); document.addEventListener('mousedown', this.handleDocumentMouseDown); } @@ -346,6 +356,7 @@ export default class SlDropdown extends ShoelaceElement { } document.removeEventListener('keydown', this.handleDocumentKeyDown); document.removeEventListener('mousedown', this.handleDocumentMouseDown); + this.closeWatcher?.destroy(); } @watch('open', { waitUntilFirstUpdate: true }) diff --git a/src/components/select/select.component.ts b/src/components/select/select.component.ts index 979f0ca8..5c42d2ae 100644 --- a/src/components/select/select.component.ts +++ b/src/components/select/select.component.ts @@ -81,6 +81,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon private readonly localize = new LocalizeController(this); private typeToSelectString = ''; private typeToSelectTimeout: number; + private closeWatcher: CloseWatcher | null; @query('.select') popup: SlPopup; @query('.select__combobox') combobox: HTMLSlotElement; @@ -222,6 +223,16 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon // https://github.com/shoelace-style/shoelace/issues/1763 // const root = this.getRootNode(); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => { + if (this.open) { + this.hide(); + this.displayInput.focus({ preventScroll: true }); + } + } + } root.addEventListener('focusin', this.handleDocumentFocusIn); root.addEventListener('keydown', this.handleDocumentKeyDown); root.addEventListener('mousedown', this.handleDocumentMouseDown); @@ -232,6 +243,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon root.removeEventListener('focusin', this.handleDocumentFocusIn); root.removeEventListener('keydown', this.handleDocumentKeyDown); root.removeEventListener('mousedown', this.handleDocumentMouseDown); + this.closeWatcher?.destroy(); } private handleFocus() { @@ -264,7 +276,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon } // Close when pressing escape - if (event.key === 'Escape' && this.open) { + if (event.key === 'Escape' && this.open && !this.closeWatcher) { event.preventDefault(); event.stopPropagation(); this.hide(); diff --git a/src/components/tooltip/tooltip.component.ts b/src/components/tooltip/tooltip.component.ts index 91e11f89..8b271985 100644 --- a/src/components/tooltip/tooltip.component.ts +++ b/src/components/tooltip/tooltip.component.ts @@ -45,6 +45,7 @@ export default class SlTooltip extends ShoelaceElement { private hoverTimeout: number; private readonly localize = new LocalizeController(this); + private closeWatcher: CloseWatcher | null; @query('slot:not([name])') defaultSlot: HTMLSlotElement; @query('.tooltip__body') body: HTMLElement; @@ -108,6 +109,7 @@ export default class SlTooltip extends ShoelaceElement { disconnectedCallback() { // Cleanup this event in case the tooltip is removed while open + this.closeWatcher?.destroy(); document.removeEventListener('keydown', this.handleDocumentKeyDown); } @@ -181,7 +183,13 @@ export default class SlTooltip extends ShoelaceElement { // Show this.emit('sl-show'); - document.addEventListener('keydown', this.handleDocumentKeyDown); + if ('CloseWatcher' in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => { this.hide() }; + } else { + document.addEventListener('keydown', this.handleDocumentKeyDown); + } await stopAnimations(this.body); this.body.hidden = false; @@ -194,6 +202,7 @@ export default class SlTooltip extends ShoelaceElement { } else { // Hide this.emit('sl-hide'); + this.closeWatcher?.destroy(); document.removeEventListener('keydown', this.handleDocumentKeyDown); await stopAnimations(this.body); diff --git a/src/declaration.d.ts b/src/declaration.d.ts index 2b906527..1bb4255b 100644 --- a/src/declaration.d.ts +++ b/src/declaration.d.ts @@ -14,3 +14,23 @@ declare namespace Chai { interface HTMLInputElement { showPicker: () => void; } + +interface CloseWatcher extends EventTarget { + new (options?: CloseWatcherOptions): CloseWatcher; + requestClose(): void; + close(): void; + destroy(): void; + + oncancel: ((event: Event) => void | null); + onclose: ((event: Event) => void | null); +} + +declare const CloseWatcher: CloseWatcher; + +interface CloseWatcherOptions { + signal: AbortSignal; +} + +declare interface Window { + CloseWatcher?: CloseWatcher; +}