diff --git a/packages/web-components/fast-foundation/src/close-watcher.d.ts b/packages/web-components/fast-foundation/src/close-watcher.d.ts new file mode 100644 index 00000000000..28ada3dfe73 --- /dev/null +++ b/packages/web-components/fast-foundation/src/close-watcher.d.ts @@ -0,0 +1,19 @@ +interface CloseWatcher extends EventTarget { + new (options?: CloseWatcherOptions): CloseWatcher; + requestClose(): void; + close(): void; + destroy(): void; + + oncancel: (event: Event) => any | null; + onclose: (event: Event) => any | null; +} + +declare const CloseWatcher: CloseWatcher; + +interface CloseWatcherOptions { + signal: AbortSignal; +} + +declare interface Window { + CloseWatcher?: CloseWatcher; +} diff --git a/packages/web-components/fast-foundation/src/dialog/dialog.ts b/packages/web-components/fast-foundation/src/dialog/dialog.ts index 26bc77c1b3b..405b99ee3e1 100644 --- a/packages/web-components/fast-foundation/src/dialog/dialog.ts +++ b/packages/web-components/fast-foundation/src/dialog/dialog.ts @@ -103,6 +103,11 @@ export class FASTDialog extends FASTElement { */ private notifier: Notifier; + /** + * @internal + */ + private closeWatcher: CloseWatcher | null = null; + /** * @internal */ @@ -119,6 +124,11 @@ export class FASTDialog extends FASTElement { */ public show(): void { this.hidden = false; + if ("CloseWatcher" in window) { + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.hide(); + } } /** @@ -128,6 +138,7 @@ export class FASTDialog extends FASTElement { */ public hide(): void { this.hidden = true; + this.closeWatcher?.destroy(); // implement `` interface this.$emit("close"); } @@ -152,6 +163,7 @@ export class FASTDialog extends FASTElement { // remove keydown event listener document.removeEventListener("keydown", this.handleDocumentKeydown); + this.closeWatcher?.destroy(); // if we are trapping focus remove the focusin listener this.updateTrapFocus(false); @@ -176,8 +188,10 @@ export class FASTDialog extends FASTElement { if (!e.defaultPrevented && !this.hidden) { switch (e.key) { case keyEscape: - this.dismiss(); - e.preventDefault(); + if (!this.closeWatcher) { + this.dismiss(); + e.preventDefault(); + } break; case keyTab: diff --git a/packages/web-components/fast-foundation/src/picker/picker.ts b/packages/web-components/fast-foundation/src/picker/picker.ts index 769f2ef9465..6f0cc454c56 100644 --- a/packages/web-components/fast-foundation/src/picker/picker.ts +++ b/packages/web-components/fast-foundation/src/picker/picker.ts @@ -445,6 +445,8 @@ export class FASTPicker extends FormAssociatedPicker { private inputElementView: HTMLView | null = null; private behaviorOrchestrator: ViewBehaviorOrchestrator | null = null; + private closeWatcher: CloseWatcher | null = null; + /** * @internal */ @@ -558,6 +560,9 @@ export class FASTPicker extends FormAssociatedPicker { if (open && this.getRootActiveElement() === this.inputElement) { this.flyoutOpen = open; + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.toggleFlyout(false); Updates.enqueue(() => { if (this.menuElement !== undefined) { this.setFocusedOption(0); @@ -570,6 +575,7 @@ export class FASTPicker extends FormAssociatedPicker { this.flyoutOpen = false; this.disableMenu(); + this.closeWatcher?.destroy(); return; } @@ -660,6 +666,9 @@ export class FASTPicker extends FormAssociatedPicker { } case keyEscape: { + if (this.closeWatcher) { + return true; + } this.toggleFlyout(false); return false; } diff --git a/packages/web-components/fast-foundation/src/select/select.ts b/packages/web-components/fast-foundation/src/select/select.ts index f03240c6efd..64207208c97 100644 --- a/packages/web-components/fast-foundation/src/select/select.ts +++ b/packages/web-components/fast-foundation/src/select/select.ts @@ -57,6 +57,8 @@ export class FASTSelect extends FormAssociatedSelect { @attr({ attribute: "open", mode: "boolean" }) public open: boolean = false; + private closeWatcher: CloseWatcher | null = null; + /** * Sets focus and synchronizes ARIA attributes when the open property changes. * @@ -78,10 +80,16 @@ export class FASTSelect extends FormAssociatedSelect { this.focusAndScrollOptionIntoView(); this.indexWhenOpened = this.selectedIndex; + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => (this.open = false); + // focus is directed to the element when `open` is changed programmatically Updates.enqueue(() => this.focus()); return; + } else { + this.closeWatcher?.destroy(); } this.cleanup?.(); @@ -523,7 +531,7 @@ export class FASTSelect extends FormAssociatedSelect { } case keyEscape: { - if (this.collapsible && this.open) { + if (this.collapsible && this.open && !this.closeWatcher) { e.preventDefault(); this.open = false; } diff --git a/packages/web-components/fast-foundation/src/tooltip/tooltip.ts b/packages/web-components/fast-foundation/src/tooltip/tooltip.ts index 4951f7bda1f..b6224de9d6d 100644 --- a/packages/web-components/fast-foundation/src/tooltip/tooltip.ts +++ b/packages/web-components/fast-foundation/src/tooltip/tooltip.ts @@ -89,6 +89,11 @@ export class FASTTooltip extends FASTElement { @observable private controlledVisibility: boolean = false; + /** + * @internal + */ + private closeWatcher: CloseWatcher | null = null; + /** * Switches between controlled and uncontrolled visibility. * @@ -311,7 +316,9 @@ export class FASTTooltip extends FASTElement { this.addEventListener("mouseout", this.mouseoutAnchorHandler); this.addEventListener("mouseover", this.mouseoverAnchorHandler); - document.addEventListener("keydown", this.keydownDocumentHandler); + if (!("CloseWatcher" in window)) { + document.addEventListener("keydown", this.keydownDocumentHandler); + } } public connectedCallback(): void { @@ -337,6 +344,7 @@ export class FASTTooltip extends FASTElement { public hideTooltip(): void { this._visible = false; this.cleanup?.(); + this.closeWatcher?.destroy(); } /** @@ -446,5 +454,8 @@ export class FASTTooltip extends FASTElement { private showTooltip(): void { this._visible = true; Updates.enqueue(() => this.setPositioning()); + this.closeWatcher?.destroy(); + this.closeWatcher = new CloseWatcher(); + this.closeWatcher.onclose = () => this.dismiss(); } }