Skip to content

Commit

Permalink
fix(overlays): no hiding of nested overlays having hideOnEsc config…
Browse files Browse the repository at this point in the history
…ured
  • Loading branch information
tlouisse committed Nov 4, 2024
1 parent a5b2c2d commit ade4034
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/rude-flies-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ui': patch
---

[overlays] no hiding of nested overlays having `hideOnEsc` configured
53 changes: 40 additions & 13 deletions packages/ui/components/overlays/src/OverlayController.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { overlays } from './singleton.js';
import { containFocus } from './utils/contain-focus.js';
import { deepContains } from './utils/deep-contains.js';
import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
import { _adoptStyleUtils } from './utils/adopt-styles.js';

/**
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
* @typedef {import('@lion/ui/types/overlays.js').ViewportConfig} ViewportConfig
* @typedef {import('@popperjs/core').createPopper} Popper
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
* @typedef {import('@popperjs/core').Options} PopperOptions
* @typedef {import('@popperjs/core').Placement} Placement
* @typedef {import('@popperjs/core').createPopper} Popper
* @typedef {{ createPopper: Popper }} PopperModule
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
*/

/**
Expand Down Expand Up @@ -103,6 +104,8 @@ async function preloadPopper() {
return /** @type {* & Promise<PopperModule>} */ (import('@popperjs/core/dist/esm/popper.js'));
}

const childDialogsClosedInEventLoopWeakmap = new WeakMap();

/**
* OverlayController is the fundament for every single type of overlay. With the right
* configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
Expand Down Expand Up @@ -498,9 +501,6 @@ export class OverlayController extends EventTarget {
'[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
);
}
// if (newConfig.popperConfig.modifiers.arrow && !newConfig.contentWrapperNode) {
// throw new Error('You need to provide a .contentWrapperNode when Popper arrow is enabled');
// }
}

/**
Expand Down Expand Up @@ -1146,11 +1146,40 @@ export class OverlayController extends EventTarget {
ev.preventDefault();
}

/** @private */
__escKeyHandler(/** @type {KeyboardEvent} */ ev) {
return ev.key === 'Escape' && this.hide();
/**
* @param {KeyboardEvent} event
* @returns {void}
*/
__escKeyHandler(event) {
if (event.key !== 'Escape' || childDialogsClosedInEventLoopWeakmap.has(event)) return;

const hasPressedInside =
event.composedPath().includes(this.contentNode) ||
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
if (hasPressedInside) {
this.hide();
// We could do event.stopPropagation() here, but we don't want to hide info for
// the outside world about user interactions. Instead, we store the event in a WeakMap
// that will be garbage collected after the event loop.
childDialogsClosedInEventLoopWeakmap.set(event, this);
}
}

/**
* @param {KeyboardEvent} event
* @returns {void}
*/
#outsideEscKeyHandler = event => {
if (event.key !== 'Escape') return;

const hasPressedInside =
event.composedPath().includes(this.contentNode) ||
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
if (!hasPressedInside) {
this.hide();
}
};

/**
* @param {{ phase: OverlayPhase }} config
* @protected
Expand All @@ -1175,11 +1204,9 @@ export class OverlayController extends EventTarget {
*/
_handleHidesOnOutsideEsc({ phase }) {
if (phase === 'show') {
this.__escKeyHandler = (/** @type {KeyboardEvent} */ ev) =>
ev.key === 'Escape' && this.hide();
document.addEventListener('keyup', this.__escKeyHandler);
document.addEventListener('keyup', this.#outsideEscKeyHandler);
} else if (phase === 'hide') {
document.removeEventListener('keyup', this.__escKeyHandler);
document.removeEventListener('keyup', this.#outsideEscKeyHandler);
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/components/overlays/test-helpers/mimicClick.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ async function sleep(t = 0) {

/**
* @param {HTMLElement} el
* @param {{isAsync?:boolean, releaseElement?: HTMLElement}} config
* @param {{isAsync?:boolean, releaseElement?: HTMLElement}} config releaseElement can be different when the mouse is dragged before release
*/
export async function mimicClick(el, { isAsync, releaseElement } = { isAsync: false }) {
export async function mimicClick(el, { isAsync = false, releaseElement } = {}) {
const releaseEl = releaseElement || el;
el.dispatchEvent(new MouseEvent('mousedown'));
if (isAsync) {
Expand Down
Loading

0 comments on commit ade4034

Please sign in to comment.