New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@lion/overlays

Package Overview
Dependencies
Maintainers
1
Versions
128
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lion/overlays - npm Package Compare versions

Comparing version 0.18.0 to 0.19.0

docs/applyDemoOverlayStyles.d.ts

11

CHANGELOG.md
# Change Log
## 0.19.0
### Minor Changes
- e42071d8: Types for overlays, tooltip and button
### Patch Changes
- Updated dependencies [75107a4b]
- @lion/core@0.12.0
## 0.18.0

@@ -4,0 +15,0 @@

19

docs/demo-overlay-system.js
import { html, LitElement } from '@lion/core';
import { OverlayMixin } from '../src/OverlayMixin.js';
/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
*/
class DemoOverlaySystem extends OverlayMixin(LitElement) {
constructor() {
super();
this.__toggle = this.__toggle.bind(this);
}
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return {
return /** @type {OverlayConfig} */ ({
placementMode: 'global',
};
});
}
__toggle() {
this.opened = !this.opened;
}
_setupOpenCloseListeners() {
super._setupOpenCloseListeners();
this.__toggle = () => {
this.opened = !this.opened;
};

@@ -18,0 +27,0 @@ if (this._overlayInvokerNode) {

import { directive } from '@lion/core';
const cache = new WeakMap();
/**
* @typedef {import('lit-html').PropertyPart} PropertyPart
*/
/** @type {WeakSet<Element>} */
const cache = new WeakSet();
/**

@@ -24,7 +29,7 @@ * @desc Allows to have references to different parts of your lit template.

*/
export const ref = directive(refObj => part => {
export const ref = directive(refObj => (/** @type {PropertyPart} */ part) => {
if (cache.has(part.committer.element)) {
return;
}
cache.set(part.committer.element);
cache.add(part.committer.element);
const attrName = part.committer.name;

@@ -31,0 +36,0 @@ const key = attrName.replace(/^#/, '');

{
"name": "@lion/overlays",
"version": "0.18.0",
"version": "0.19.0",
"description": "Overlays System using lit-html for rendering",

@@ -35,3 +35,3 @@ "license": "MIT",

"dependencies": {
"@lion/core": "0.11.0",
"@lion/core": "0.12.0",
"popper.js": "^1.15.0",

@@ -38,0 +38,0 @@ "singleton-manager": "1.1.2"

@@ -1,11 +0,16 @@

export const withBottomSheetConfig = () => ({
hasBackdrop: true,
preventsScroll: true,
trapsKeyboardFocus: true,
hidesOnEsc: true,
placementMode: 'global',
viewportConfig: {
placement: 'bottom',
},
handlesAccessibility: true,
});
/**
* @typedef {import('../../types/OverlayConfig').OverlayConfig} OverlayConfig
*/
export const withBottomSheetConfig = () =>
/** @type {OverlayConfig} */ ({
hasBackdrop: true,
preventsScroll: true,
trapsKeyboardFocus: true,
hidesOnEsc: true,
placementMode: 'global',
viewportConfig: {
placement: 'bottom',
},
handlesAccessibility: true,
});

@@ -1,15 +0,19 @@

export const withDropdownConfig = () => ({
placementMode: 'local',
/**
* @typedef {import('../../types/OverlayConfig').OverlayConfig} OverlayConfig
*/
inheritsReferenceWidth: 'min',
hidesOnOutsideClick: true,
popperConfig: {
placement: 'bottom-start',
modifiers: {
offset: {
enabled: false,
export const withDropdownConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'local',
inheritsReferenceWidth: 'min',
hidesOnOutsideClick: true,
popperConfig: {
placement: 'bottom-start',
modifiers: {
offset: {
enabled: false,
},
},
},
},
handlesAccessibility: true,
});
handlesAccessibility: true,
});

@@ -1,12 +0,16 @@

export const withModalDialogConfig = () => ({
placementMode: 'global',
viewportConfig: {
placement: 'center',
},
/**
* @typedef {import('../../types/OverlayConfig').OverlayConfig} OverlayConfig
*/
hasBackdrop: true,
preventsScroll: true,
trapsKeyboardFocus: true,
hidesOnEsc: true,
handlesAccessibility: true,
});
export const withModalDialogConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'global',
viewportConfig: {
placement: 'center',
},
hasBackdrop: true,
preventsScroll: true,
trapsKeyboardFocus: true,
hidesOnEsc: true,
handlesAccessibility: true,
});
import '@lion/core/src/differentKeyEventNamesShimIE.js';
import { EventTargetShim } from '@lion/core';
// eslint-disable-next-line import/no-cycle
import { overlays } from './overlays.js';
import { containFocus } from './utils/contain-focus.js';
import './utils/typedef.js';
/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
* @typedef {import('../types/OverlayConfig').ViewportConfig} ViewportConfig
* @typedef {import('popper.js').default} Popper
* @typedef {import('popper.js').PopperOptions} PopperOptions
* @typedef {{ default: Popper }} PopperModule
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
*/
/**
* @returns {Promise<PopperModule>}
*/
async function preloadPopper() {
return import('popper.js/dist/esm/popper.min.js');
// @ts-ignore
return /** @type {Promise<PopperModule>} */ (import('popper.js/dist/esm/popper.min.js'));
}

@@ -12,2 +26,3 @@

const GLOBAL_OVERLAYS_CLASS = 'global-overlays__overlay';
// @ts-expect-error CSS not yet typed
const supportsCSSTypedObject = window.CSS && CSS.number;

@@ -73,4 +88,3 @@

*/
export class OverlayController {
export class OverlayController extends EventTargetShim {
/**

@@ -82,3 +96,3 @@ * @constructor

constructor(config = {}, manager = overlays) {
this.__fakeExtendsEventTarget();
super();
this.manager = manager;

@@ -89,3 +103,3 @@ this.__sharedConfig = config;

this._defaultConfig = {
placementMode: null,
placementMode: undefined,
contentNode: config.contentNode,

@@ -95,3 +109,3 @@ contentWrapperNode: config.contentWrapperNode,

backdropNode: config.backdropNode,
referenceNode: null,
referenceNode: undefined,
elementToFocusAfterHide: config.invokerNode,

@@ -108,3 +122,3 @@ inheritsReferenceWidth: 'none',

invokerRelation: 'description',
handlesUserInteraction: false,
// handlesUserInteraction: false,
handlesAccessibility: false,

@@ -155,4 +169,10 @@ popperConfig: {

this.__hasActiveBackdrop = true;
this.__escKeyHandler = this.__escKeyHandler.bind(this);
}
/**
* The invokerNode
* @type {HTMLElement | undefined}
*/
get invoker() {

@@ -162,8 +182,193 @@ return this.invokerNode;

/**
* The contentWrapperNode
* @type {HTMLElement}
*/
get content() {
return this._contentWrapperNode;
return /** @type {HTMLElement} */ (this.contentWrapperNode);
}
/**
* @desc Usually the parent node of contentWrapperNode that either exists locally or globally.
* Determines the connection point in DOM (body vs next to invoker).
* @type {'global' | 'local' | undefined}
*/
get placementMode() {
return this.config?.placementMode;
}
/**
* The interactive element (usually a button) invoking the dialog or tooltip
* @type {HTMLElement | undefined}
*/
get invokerNode() {
return this.config?.invokerNode;
}
/**
* The element that is used to position the overlay content relative to. Usually,
* this is the same element as invokerNode. Should only be provided when invokerNode should not
* be positioned against.
* @type {HTMLElement}
*/
get referenceNode() {
return /** @type {HTMLElement} */ (this.config?.referenceNode);
}
/**
* The most important element: the overlay itself
* @type {HTMLElement}
*/
get contentNode() {
return /** @type {HTMLElement} */ (this.config?.contentNode);
}
/**
* The wrapper element of contentNode, used to supply inline positioning styles. When a Popper
* arrow is needed, it acts as parent of the arrow node. Will be automatically created for global
* and non projected contentNodes. Required when used in shadow dom mode or when Popper arrow is
* supplied. Essential for allowing webcomponents to style their projected contentNodes
* @type {HTMLElement}
*/
get contentWrapperNode() {
return /** @type {HTMLElement} */ (this.__contentWrapperNode ||
this.config?.contentWrapperNode);
}
/**
* The element that is placed behin the contentNode. When not provided and `hasBackdrop` is true,
* a backdropNode will be automatically created
* @type {HTMLElement}
*/
get backdropNode() {
return /** @type {HTMLElement} */ (this.__backdropNode || this.config?.backdropNode);
}
/**
* The element that should be called `.focus()` on after dialog closes
* @type {HTMLElement}
*/
get elementToFocusAfterHide() {
return /** @type {HTMLElement} */ (this.__elementToFocusAfterHide ||
this.config?.elementToFocusAfterHide);
}
/**
* Whether it should have a backdrop (currently exclusive to globalOverlayController)
* @type {boolean}
*/
get hasBackdrop() {
return /** @type {boolean} */ (this.config?.hasBackdrop);
}
/**
* Hides other overlays when mutiple are opened (currently exclusive to globalOverlayController)
* @type {boolean}
*/
get isBlocking() {
return /** @type {boolean} */ (this.config?.isBlocking);
}
/**
* Hides other overlays when mutiple are opened (currently exclusive to globalOverlayController)
* @type {boolean}
*/
get preventsScroll() {
return /** @type {boolean} */ (this.config?.preventsScroll);
}
/**
* Rotates tab, implicitly set when 'isModal'
* @type {boolean}
*/
get trapsKeyboardFocus() {
return /** @type {boolean} */ (this.config?.trapsKeyboardFocus);
}
/**
* Hides the overlay when pressing [ esc ]
* @type {boolean}
*/
get hidesOnEsc() {
return /** @type {boolean} */ (this.config?.hidesOnEsc);
}
/**
* Hides the overlay when clicking next to it, exluding invoker
* @type {boolean}
*/
get hidesOnOutsideClick() {
return /** @type {boolean} */ (this.config?.hidesOnOutsideClick);
}
/**
* Hides the overlay when pressing esc, even when contentNode has no focus
* @type {boolean}
*/
get hidesOnOutsideEsc() {
return /** @type {boolean} */ (this.config?.hidesOnOutsideEsc);
}
/**
* Will align contentNode with referenceNode (invokerNode by default) for local overlays.
* Usually needed for dropdowns. 'max' will prevent contentNode from exceeding width of
* referenceNode, 'min' guarantees that contentNode will be at least as wide as referenceNode.
* 'full' will make sure that the invoker width always is the same.
* @type {'max' | 'full' | 'min' | 'none' | undefined }
*/
get inheritsReferenceWidth() {
return this.config?.inheritsReferenceWidth;
}
/**
* For non `isTooltip`:
* - sets aria-expanded="true/false" and aria-haspopup="true" on invokerNode
* - sets aria-controls on invokerNode
* - returns focus to invokerNode on hide
* - sets focus to overlay content(?)
*
* For `isTooltip`:
* - sets role="tooltip" and aria-labelledby/aria-describedby on the content
*
* @type {boolean}
*/
get handlesAccessibility() {
return /** @type {boolean} */ (this.config?.handlesAccessibility);
}
/**
* Has a totally different interaction- and accessibility pattern from all other overlays.
* Will behave as role="tooltip" element instead of a role="dialog" element
* @type {boolean}
*/
get isTooltip() {
return /** @type {boolean} */ (this.config?.isTooltip);
}
/**
* By default, the tooltip content is a 'description' for the invoker (uses aria-describedby).
* Setting this property to 'label' makes the content function as a label (via aria-labelledby)
* @type {'label' | 'description'| undefined}
*/
get invokerRelation() {
return this.config?.invokerRelation;
}
/**
* Popper configuration. Will be used when placementMode is 'local'
* @type {PopperOptions}
*/
get popperConfig() {
return /** @type {PopperOptions} */ (this.config?.popperConfig);
}
/**
* Viewport configuration. Will be used when placementMode is 'global'
* @type {ViewportConfig}
*/
get viewportConfig() {
return /** @type {ViewportConfig} */ (this.config?.viewportConfig);
}
/**
* Usually the parent node of contentWrapperNode that either exists locally or globally.
* When a responsive scenario is created (in which we switch from global to local or vice versa)

@@ -181,6 +386,7 @@ * we need to know where we should reappend contentWrapperNode (or contentNode in case it's

if (this.__isContentNodeProjected) {
return this.__originalContentParent.getRootNode().host;
// @ts-expect-error
return this.__originalContentParent?.getRootNode().host;
}
/** config [l1] or [l3] */
return this.__originalContentParent;
return /** @type {HTMLElement} */ (this.__originalContentParent);
}

@@ -190,3 +396,3 @@

* @desc The element our local overlay will be positioned relative to.
* @type {HTMLElement}
* @type {HTMLElement | undefined}
*/

@@ -197,5 +403,8 @@ get _referenceNode() {

/**
* @param {string} value
*/
set elevation(value) {
if (this._contentWrapperNode) {
this._contentWrapperNode.style.zIndex = value;
if (this.contentWrapperNode) {
this.contentWrapperNode.style.zIndex = value;
}

@@ -207,8 +416,11 @@ if (this.backdropNode) {

/**
* @type {number}
*/
get elevation() {
return this._contentWrapperNode.zIndex;
return Number(this.contentWrapperNode?.style.zIndex);
}
/**
* @desc Allows to dynamically change the overlay configuration. Needed in case the
* Allows to dynamically change the overlay configuration. Needed in case the
* presentation of the overlay changes depending on screen size.

@@ -223,2 +435,3 @@ * Note that this method is the only allowed way to update a configuration of an

/** @type {OverlayConfig} */
this.__prevConfig = this.config || {};

@@ -245,6 +458,11 @@

this.__validateConfiguration(this.config);
Object.assign(this, this.config);
// TODO: remove this, so we only have the getters (no setters)
// Object.assign(this, this.config);
this._init({ cfgToAdd });
this.__elementToFocusAfterHide = undefined;
}
/**
* @param {OverlayConfig} newConfig
*/
// eslint-disable-next-line class-methods-use-this

@@ -285,2 +503,5 @@ __validateConfiguration(newConfig) {

/**
* @param {{ cfgToAdd: OverlayConfig }} options
*/
_init({ cfgToAdd }) {

@@ -292,4 +513,5 @@ this.__initContentWrapperNode({ cfgToAdd });

// Lazily load Popper if not done yet
if (!this.constructor.popperModule) {
this.constructor.popperModule = preloadPopper();
if (!OverlayController.popperModule) {
// @ts-expect-error
OverlayController.popperModule = preloadPopper();
}

@@ -302,5 +524,6 @@ }

// Now, add our node to the right place in dom (renderTarget)
if (this._contentWrapperNode !== this.__prevConfig._contentWrapperNode) {
if (this.config.placementMode === 'global' || !this.__isContentNodeProjected) {
this._contentWrapperNode.appendChild(this.contentNode);
if (this.contentWrapperNode !== this.__prevConfig?.contentWrapperNode) {
if (this.config?.placementMode === 'global' || !this.__isContentNodeProjected) {
/** @type {HTMLElement} */
(this.contentWrapperNode).appendChild(this.contentNode);
}

@@ -317,7 +540,7 @@ }

} else {
const isInsideRenderTarget = this._renderTarget === this._contentWrapperNode.parentNode;
const nodeContainsTarget = this._contentWrapperNode.contains(this._renderTarget);
const isInsideRenderTarget = this._renderTarget === this.contentWrapperNode.parentNode;
const nodeContainsTarget = this.contentWrapperNode.contains(this._renderTarget);
if (!isInsideRenderTarget && !nodeContainsTarget) {
// contentWrapperNode becomes the direct (non projected) parent of contentNode
this._renderTarget.appendChild(this._contentWrapperNode);
this._renderTarget.appendChild(this.contentWrapperNode);
}

@@ -328,16 +551,17 @@ }

/**
* @desc Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
* Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
* can lead to problems with event listeners...
* @param {{ cfgToAdd: OverlayConfig }} options
*/
__initContentWrapperNode({ cfgToAdd }) {
if (this.config.contentWrapperNode && this.placementMode === 'local') {
if (this.config?.contentWrapperNode && this.placementMode === 'local') {
/** config [l2],[l3],[l4] */
this._contentWrapperNode = this.config.contentWrapperNode;
this.__contentWrapperNode = this.config.contentWrapperNode;
} else {
/** config [l1],[g1] */
this._contentWrapperNode = document.createElement('div');
this.__contentWrapperNode = document.createElement('div');
}
this._contentWrapperNode.style.cssText = null;
this._contentWrapperNode.style.display = 'none';
this.contentWrapperNode.style.cssText = '';
this.contentWrapperNode.style.display = 'none';

@@ -350,10 +574,11 @@ if (getComputedStyle(this.contentNode).position === 'absolute') {

if (this.__isContentNodeProjected && this._contentWrapperNode.isConnected) {
if (this.__isContentNodeProjected && this.contentWrapperNode.isConnected) {
// We need to keep track of the original local context.
/** config [l2], [l4] */
this.__originalContentParent = this._contentWrapperNode.parentNode;
this.__originalContentParent = /** @type {HTMLElement} */ (this.contentWrapperNode
.parentNode);
} else if (cfgToAdd.contentNode && cfgToAdd.contentNode.isConnected) {
// We need to keep track of the original local context.
/** config [l1], [l3], [g1] */
this.__originalContentParent = this.contentNode.parentNode;
this.__originalContentParent = /** @type {HTMLElement} */ (this.contentNode?.parentNode);
}

@@ -363,3 +588,4 @@ }

/**
* @desc Display local overlays on top of elements with no z-index that appear later in the DOM
* Display local overlays on top of elements with no z-index that appear later in the DOM
* @param {{ phase: OverlayPhase }} config
*/

@@ -374,3 +600,3 @@ _handleZIndex({ phase }) {

if (zIndexNumber < 1 || Number.isNaN(zIndexNumber)) {
this._contentWrapperNode.style.zIndex = 1;
this.contentWrapperNode.style.zIndex = '1';
}

@@ -380,2 +606,5 @@ }

/**
* @param {{ phase: OverlayPhase }} config
*/
__setupTeardownAccessibility({ phase }) {

@@ -406,3 +635,3 @@ if (phase === 'init') {

if (this.invokerNode) {
this.invokerNode.setAttribute('aria-expanded', this.isShown);
this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
}

@@ -418,2 +647,6 @@ if (!this.contentNode.getAttribute('role')) {

/**
* @param {HTMLElement} node
* @param {string[]} attrs
*/
__storeOriginalAttrs(node, attrs) {

@@ -441,3 +674,3 @@ const attrMap = {};

get isShown() {
return Boolean(this._contentWrapperNode.style.display !== 'none');
return Boolean(this.contentWrapperNode.style.display !== 'none');
}

@@ -460,3 +693,3 @@

if (this.isShown) {
this._showResolve();
/** @type {function} */ (this._showResolve)();
return;

@@ -468,3 +701,3 @@ }

if (!event.defaultPrevented) {
this._contentWrapperNode.style.display = '';
this.contentWrapperNode.style.display = '';
this._keepBodySize({ phase: 'before-show' });

@@ -474,8 +707,11 @@ await this._handleFeatures({ phase: 'show' });

await this._handlePosition({ phase: 'show' });
this.elementToFocusAfterHide = elementToFocusAfterHide;
this.__elementToFocusAfterHide = elementToFocusAfterHide;
this.dispatchEvent(new Event('show'));
}
this._showResolve();
/** @type {function} */ (this._showResolve)();
}
/**
* @param {{ phase: OverlayPhase }} config
*/
async _handlePosition({ phase }) {

@@ -485,4 +721,4 @@ if (this.placementMode === 'global') {

const placementClass = `${GLOBAL_OVERLAYS_CONTAINER_CLASS}--${this.viewportConfig.placement}`;
this._contentWrapperNode.classList[addOrRemove](GLOBAL_OVERLAYS_CONTAINER_CLASS);
this._contentWrapperNode.classList[addOrRemove](placementClass);
this.contentWrapperNode.classList[addOrRemove](GLOBAL_OVERLAYS_CONTAINER_CLASS);
this.contentWrapperNode.classList[addOrRemove](placementClass);
this.contentNode.classList[addOrRemove](GLOBAL_OVERLAYS_CLASS);

@@ -498,6 +734,9 @@ } else if (this.placementMode === 'local' && phase === 'show') {

await this.__createPopperInstance();
this._popper.update();
/** @type {Popper} */ (this._popper).update();
}
}
/**
* @param {{ phase: OverlayPhase }} config
*/
_keepBodySize({ phase }) {

@@ -513,3 +752,5 @@ switch (phase) {

if (supportsCSSTypedObject) {
// @ts-expect-error types attributeStyleMap not available yet
this.__bodyMarginRight = document.body.computedStyleMap().get('margin-right').value;
// @ts-expect-error types computedStyleMap not available yet
this.__bodyMarginBottom = document.body.computedStyleMap().get('margin-bottom').value;

@@ -523,8 +764,12 @@ } else if (window.getComputedStyle) {

}
const scrollbarWidth = document.body.clientWidth - this.__bodyClientWidth;
const scrollbarHeight = document.body.clientHeight - this.__bodyClientHeight;
const scrollbarWidth =
document.body.clientWidth - /** @type {number} */ (this.__bodyClientWidth);
const scrollbarHeight =
document.body.clientHeight - /** @type {number} */ (this.__bodyClientHeight);
const newMarginRight = this.__bodyMarginRight + scrollbarWidth;
const newMarginBottom = this.__bodyMarginBottom + scrollbarHeight;
if (supportsCSSTypedObject) {
// @ts-expect-error types attributeStyleMap + CSS.px not available yet
document.body.attributeStyleMap.set('margin-right', CSS.px(newMarginRight));
// @ts-expect-error types attributeStyleMap + CSS.px not available yet
document.body.attributeStyleMap.set('margin-bottom', CSS.px(newMarginBottom));

@@ -539,3 +784,5 @@ } else {

if (supportsCSSTypedObject) {
// @ts-expect-error types attributeStyleMap + CSS.px not available yet
document.body.attributeStyleMap.set('margin-right', CSS.px(this.__bodyMarginRight));
// @ts-expect-error types attributeStyleMap + CSS.px not available yet
document.body.attributeStyleMap.set('margin-bottom', CSS.px(this.__bodyMarginBottom));

@@ -565,3 +812,3 @@ } else {

if (!this.isShown) {
this._hideResolve();
/** @type {function} */ (this._hideResolve)();
return;

@@ -574,3 +821,3 @@ }

// await this.transitionHide({ backdropNode: this.backdropNode, contentNode: this.contentNode });
this._contentWrapperNode.style.display = 'none';
this.contentWrapperNode.style.display = 'none';
this._handleFeatures({ phase: 'hide' });

@@ -581,7 +828,10 @@ this._keepBodySize({ phase: 'hide' });

}
this._hideResolve();
/** @type {function} */ (this._hideResolve)();
}
/**
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} config
*/
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async transitionHide({ backdropNode, contentNode }) {}
async transitionHide(config) {}

@@ -591,7 +841,5 @@ _restoreFocus() {

// Otherwise we assume the 'outside world' has, purposefully, taken over
// if (this._contentWrapperNode.activeElement) {
if (this.elementToFocusAfterHide) {
this.elementToFocusAfterHide.focus();
}
// }
}

@@ -604,6 +852,4 @@

/**
* @desc All features are handled here. Every feature is set up on show
* and torn
* @param {object} config
* @param {'init'|'show'|'hide'|'teardown'} config.phase
* All features are handled here.
* @param {{ phase: OverlayPhase }} config
*/

@@ -642,2 +888,5 @@ _handleFeatures({ phase }) {

/**
* @param {{ phase: OverlayPhase }} config
*/
_handlePreventsScroll({ phase }) {

@@ -655,2 +904,5 @@ switch (phase) {

/**
* @param {{ phase: OverlayPhase }} config
*/
_handleBlocking({ phase }) {

@@ -673,5 +925,6 @@ switch (phase) {

/**
* @desc Sets up backdrop on the given overlay. If there was a backdrop on another element
* Sets up backdrop on the given overlay. If there was a backdrop on another element
* it is removed. Otherwise this is the first time displaying a backdrop, so a fade-in
* animation is played.
* @param {{ animation?: boolean, phase: OverlayPhase }} config
*/

@@ -683,7 +936,9 @@ _handleBackdrop({ animation = true, phase }) {

if (!this.backdropNode) {
this.backdropNode = document.createElement('div');
this.backdropNode.classList.add('local-overlays__backdrop');
this.__backdropNode = document.createElement('div');
/** @type {HTMLElement} */
(this.backdropNode).classList.add('local-overlays__backdrop');
}
this.backdropNode.slot = '_overlay-shadow-outlet';
this.contentNode.parentNode.insertBefore(this.backdropNode, this.contentNode);
/** @type {HTMLElement} */
(this.contentNode.parentNode).insertBefore(this.backdropNode, this.contentNode);
break;

@@ -704,2 +959,3 @@ case 'show':

this.backdropNode.parentNode.removeChild(this.backdropNode);
this.__backdropNode = undefined;
break;

@@ -710,16 +966,16 @@ /* no default */

}
const { backdropNode } = this;
switch (phase) {
case 'init':
this.backdropNode = document.createElement('div');
this.__backdropNode = document.createElement('div');
this.backdropNode.classList.add('global-overlays__backdrop');
this._contentWrapperNode.parentElement.insertBefore(
/** @type {HTMLElement} */
(this.contentWrapperNode.parentElement).insertBefore(
this.backdropNode,
this._contentWrapperNode,
this.contentWrapperNode,
);
break;
case 'show':
backdropNode.classList.add('global-overlays__backdrop--visible');
this.backdropNode.classList.add('global-overlays__backdrop--visible');
if (animation === true) {
backdropNode.classList.add('global-overlays__backdrop--fade-in');
this.backdropNode.classList.add('global-overlays__backdrop--fade-in');
}

@@ -729,21 +985,23 @@ this.__hasActiveBackdrop = true;

case 'hide':
if (!backdropNode) {
if (!this.backdropNode) {
return;
}
backdropNode.classList.remove('global-overlays__backdrop--fade-in');
this.backdropNode.classList.remove('global-overlays__backdrop--fade-in');
if (animation) {
/** @type {(ev:AnimationEvent) => void} */
let afterFadeOut;
backdropNode.classList.add('global-overlays__backdrop--fade-out');
this.backdropNode.classList.add('global-overlays__backdrop--fade-out');
this.__backDropAnimation = new Promise(resolve => {
afterFadeOut = () => {
backdropNode.classList.remove('global-overlays__backdrop--fade-out');
backdropNode.classList.remove('global-overlays__backdrop--visible');
backdropNode.removeEventListener('animationend', afterFadeOut);
this.backdropNode.classList.remove('global-overlays__backdrop--fade-out');
this.backdropNode.classList.remove('global-overlays__backdrop--visible');
this.backdropNode.removeEventListener('animationend', afterFadeOut);
resolve();
};
});
backdropNode.addEventListener('animationend', afterFadeOut);
// @ts-expect-error
this.backdropNode.addEventListener('animationend', afterFadeOut);
} else {
backdropNode.classList.remove('global-overlays__backdrop--visible');
this.backdropNode.classList.remove('global-overlays__backdrop--visible');
}

@@ -753,3 +1011,3 @@ this.__hasActiveBackdrop = false;

case 'teardown':
if (!backdropNode || !backdropNode.parentNode) {
if (!this.backdropNode || !this.backdropNode.parentNode) {
return;

@@ -759,6 +1017,7 @@ }

this.__backDropAnimation.then(() => {
backdropNode.parentNode.removeChild(backdropNode);
/** @type {HTMLElement} */
(this.backdropNode.parentNode).removeChild(this.backdropNode);
});
} else {
backdropNode.parentNode.removeChild(backdropNode);
this.backdropNode.parentNode.removeChild(this.backdropNode);
}

@@ -774,2 +1033,5 @@ break;

/**
* @param {{ phase: OverlayPhase }} config
*/
_handleTrapsKeyboardFocus({ phase }) {

@@ -811,5 +1073,11 @@ if (phase === 'show') {

__escKeyHandler(/** @type {KeyboardEvent} */ ev) {
return ev.key === 'Escape' && this.hide();
}
/**
* @param {{ phase: OverlayPhase }} config
*/
_handleHidesOnEsc({ phase }) {
if (phase === 'show') {
this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide();
this.contentNode.addEventListener('keyup', this.__escKeyHandler);

@@ -827,5 +1095,9 @@ if (this.invokerNode) {

/**
* @param {{ phase: OverlayPhase }} config
*/
_handleHidesOnOutsideEsc({ phase }) {
if (phase === 'show') {
this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide();
this.__escKeyHandler = (/** @type {KeyboardEvent} */ ev) =>
ev.key === 'Escape' && this.hide();
document.addEventListener('keyup', this.__escKeyHandler);

@@ -845,10 +1117,10 @@ } else if (phase === 'hide') {

case 'max':
this._contentWrapperNode.style.maxWidth = referenceWidth;
this.contentWrapperNode.style.maxWidth = referenceWidth;
break;
case 'full':
this._contentWrapperNode.style.width = referenceWidth;
this.contentWrapperNode.style.width = referenceWidth;
break;
case 'min':
this._contentWrapperNode.style.minWidth = referenceWidth;
this._contentWrapperNode.style.width = 'auto';
this.contentWrapperNode.style.minWidth = referenceWidth;
this.contentWrapperNode.style.width = 'auto';
break;

@@ -859,2 +1131,5 @@ /* no default */

/**
* @param {{ phase: OverlayPhase }} config
*/
_handleHidesOnOutsideClick({ phase }) {

@@ -867,2 +1142,3 @@ const addOrRemoveListener = phase === 'show' ? 'addEventListener' : 'removeEventListener';

// Handle on capture phase and remember till the next task that there was an inside click
/** @type {EventListenerOrEventListenerObject} */
this.__preventCloseOutsideClick = () => {

@@ -885,2 +1161,3 @@ if (wasClickInside) {

// handle on capture phase and schedule the hide if needed
/** @type {EventListenerOrEventListenerObject} */
this.__onCaptureHtmlClick = () => {

@@ -895,9 +1172,27 @@ setTimeout(() => {

this._contentWrapperNode[addOrRemoveListener]('click', this.__preventCloseOutsideClick, true);
this.contentWrapperNode[addOrRemoveListener](
'click',
/** @type {EventListenerOrEventListenerObject} */
(this.__preventCloseOutsideClick),
true,
);
if (this.invokerNode) {
this.invokerNode[addOrRemoveListener]('click', this.__preventCloseOutsideClick, true);
this.invokerNode[addOrRemoveListener](
'click',
/** @type {EventListenerOrEventListenerObject} */
(this.__preventCloseOutsideClick),
true,
);
}
document.documentElement[addOrRemoveListener]('click', this.__onCaptureHtmlClick, true);
document.documentElement[addOrRemoveListener](
'click',
/** @type {EventListenerOrEventListenerObject} */
(this.__onCaptureHtmlClick),
true,
);
}
/**
* @param {{ phase: OverlayPhase }} config
*/
_handleAccessibility({ phase }) {

@@ -908,3 +1203,3 @@ if (phase === 'init' || phase === 'teardown') {

if (this.invokerNode && !this.isTooltip) {
this.invokerNode.setAttribute('aria-expanded', phase === 'show');
this.invokerNode.setAttribute('aria-expanded', `${phase === 'show'}`);
}

@@ -917,3 +1212,3 @@ }

if (this.placementMode === 'global' && this.__isContentNodeProjected) {
this.__originalContentParent.appendChild(this.contentNode);
/** @type {HTMLElement} */ (this.__originalContentParent).appendChild(this.contentNode);
}

@@ -928,6 +1223,6 @@

this.placementMode === 'global' &&
this._contentWrapperNode &&
this._contentWrapperNode.parentNode
this.contentWrapperNode &&
this.contentWrapperNode.parentNode
) {
this._contentWrapperNode.parentNode.removeChild(this._contentWrapperNode);
this.contentWrapperNode.parentNode.removeChild(this.contentWrapperNode);
}

@@ -939,16 +1234,13 @@ }

this._popper.destroy();
this._popper = null;
this._popper = undefined;
}
const { default: Popper } = await this.constructor.popperModule;
this._popper = new Popper(this._referenceNode, this._contentWrapperNode, {
...this.config.popperConfig,
// @ts-expect-error
const { default: Popper } = await OverlayController.popperModule;
/** @type {Popper} */
this._popper = new Popper(this._referenceNode, this.contentWrapperNode, {
...this.config?.popperConfig,
});
}
__fakeExtendsEventTarget() {
const delegate = document.createDocumentFragment();
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
this[funcName] = (...args) => delegate[funcName](...args);
});
}
}
/** @type {PopperModule | undefined} */
OverlayController.popperModule = undefined;

@@ -5,253 +5,296 @@ import { dedupeMixin } from '@lion/core';

/**
* @type {Function()}
* @polymerMixinOverlayMixin
* @mixinFunction
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
* @typedef {import('../types/OverlayMixinTypes').DefineOverlayConfig} DefineOverlayConfig
* @typedef {import('../types/OverlayMixinTypes').OverlayHost} OverlayHost
* @typedef {import('../types/OverlayMixinTypes').OverlayMixin} OverlayMixin
*/
export const OverlayMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-shadow
class OverlayMixin extends superclass {
static get properties() {
return {
opened: {
type: Boolean,
reflect: true,
},
};
}
constructor() {
super();
this.opened = false;
this.__needsSetup = true;
this.config = {};
}
/**
* @type {OverlayMixin}
*/
export const OverlayMixinImplementation = superclass =>
class OverlayMixin extends superclass {
static get properties() {
return {
opened: {
type: Boolean,
reflect: true,
},
};
}
get config() {
return this.__config;
}
constructor() {
super();
this.opened = false;
this.__needsSetup = true;
/** @type {OverlayConfig} */
this.config = {};
}
set config(value) {
if (this._overlayCtrl) {
this._overlayCtrl.updateConfig(value);
}
this.__config = value;
get config() {
return /** @type {OverlayConfig} */ (this.__config);
}
/** @param {OverlayConfig} value */
set config(value) {
if (this._overlayCtrl) {
this._overlayCtrl.updateConfig(value);
}
this.__config = value;
}
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
if (name === 'opened') {
this.dispatchEvent(new Event('opened-changed'));
}
/**
* @override
* @param {string} name
* @param {any} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
if (name === 'opened') {
this.dispatchEvent(new Event('opened-changed'));
}
}
/**
* @overridable method `_defineOverlay`
* @desc returns an instance of a (dynamic) overlay controller
* In case overriding _defineOverlayConfig is not enough
* @returns {OverlayController}
*/
// eslint-disable-next-line
_defineOverlay({ contentNode, invokerNode, backdropNode, contentWrapperNode }) {
return new OverlayController({
contentNode,
invokerNode,
backdropNode,
contentWrapperNode,
...this._defineOverlayConfig(), // wc provided in the class as defaults
...this.config, // user provided (e.g. in template)
popperConfig: {
...(this._defineOverlayConfig().popperConfig || {}),
...(this.config.popperConfig || {}),
modifiers: {
...((this._defineOverlayConfig().popperConfig &&
this._defineOverlayConfig().popperConfig.modifiers) ||
{}),
...((this.config.popperConfig && this.config.popperConfig.modifiers) || {}),
},
/**
* @overridable method `_defineOverlay`
* @desc returns an instance of a (dynamic) overlay controller
* In case overriding _defineOverlayConfig is not enough
* @param {DefineOverlayConfig} config
* @returns {OverlayController}
*/
// eslint-disable-next-line
_defineOverlay({ contentNode, invokerNode, backdropNode, contentWrapperNode }) {
return new OverlayController({
contentNode,
invokerNode,
backdropNode,
contentWrapperNode,
...this._defineOverlayConfig(), // wc provided in the class as defaults
...this.config, // user provided (e.g. in template)
popperConfig: {
...(this._defineOverlayConfig().popperConfig || {}),
...(this.config.popperConfig || {}),
modifiers: {
...((this._defineOverlayConfig().popperConfig &&
this._defineOverlayConfig()?.popperConfig?.modifiers) ||
{}),
...((this.config.popperConfig && this.config.popperConfig.modifiers) || {}),
},
});
}
},
});
}
/**
* @overridable method `_defineOverlay`
* @desc returns an object with default configuration options for your overlay component.
* This is generally speaking easier to override than _defineOverlay method entirely.
* @returns {OverlayController}
*/
// eslint-disable-next-line
_defineOverlayConfig() {
return {
placementMode: 'local',
};
}
/**
* @overridable method `_defineOverlay`
* @desc returns an object with default configuration options for your overlay component.
* This is generally speaking easier to override than _defineOverlay method entirely.
* @returns {OverlayConfig}
*/
// eslint-disable-next-line
_defineOverlayConfig() {
return {
placementMode: 'local',
};
}
updated(changedProperties) {
super.updated(changedProperties);
/**
* @param {{ has: (arg0: string) => any; }} changedProperties
*/
updated(changedProperties) {
super.updated(changedProperties);
if (
changedProperties.has('opened') &&
this._overlayCtrl &&
!this.__blockSyncToOverlayCtrl
) {
this.__syncToOverlayController();
}
if (changedProperties.has('opened') && this._overlayCtrl && !this.__blockSyncToOverlayCtrl) {
this.__syncToOverlayController();
}
}
/**
* @overridable
* @desc use this method to setup your open and close event listeners
* For example, set a click event listener on _overlayInvokerNode to set opened to true
*/
// eslint-disable-next-line class-methods-use-this
_setupOpenCloseListeners() {
/**
* @overridable
* @desc use this method to setup your open and close event listeners
* For example, set a click event listener on _overlayInvokerNode to set opened to true
* @param {{ stopPropagation: () => void; }} ev
*/
// eslint-disable-next-line class-methods-use-this
_setupOpenCloseListeners() {
this.__closeEventInContentNodeHandler = ev => {
ev.stopPropagation();
this._overlayCtrl.hide();
};
if (this._overlayContentNode) {
this._overlayContentNode.addEventListener(
'close-overlay',
this.__closeEventInContentNodeHandler,
);
}
this.__closeEventInContentNodeHandler = ev => {
ev.stopPropagation();
/** @type {OverlayController} */ (this._overlayCtrl).hide();
};
if (this._overlayContentNode) {
this._overlayContentNode.addEventListener(
'close-overlay',
this.__closeEventInContentNodeHandler,
);
}
}
/**
* @overridable
* @desc use this method to tear down your event listeners
*/
// eslint-disable-next-line class-methods-use-this
_teardownOpenCloseListeners() {
if (this._overlayContentNode) {
this._overlayContentNode.removeEventListener(
'close-overlay',
this.__closeEventInContentNodeHandler,
);
}
/**
* @overridable
* @desc use this method to tear down your event listeners
*/
// eslint-disable-next-line class-methods-use-this
_teardownOpenCloseListeners() {
if (this._overlayContentNode) {
this._overlayContentNode.removeEventListener(
'close-overlay',
this.__closeEventInContentNodeHandler,
);
}
}
connectedCallback() {
super.connectedCallback();
// we do a setup after every connectedCallback as firstUpdated will only be called once
this.__needsSetup = true;
this.updateComplete.then(() => {
if (this.__needsSetup) {
this._setupOverlayCtrl();
}
this.__needsSetup = false;
});
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
connectedCallback() {
super.connectedCallback();
// we do a setup after every connectedCallback as firstUpdated will only be called once
this.__needsSetup = true;
this.updateComplete.then(() => {
if (this.__needsSetup) {
this._setupOverlayCtrl();
}
if (this._overlayCtrl) {
this._teardownOverlayCtrl();
}
}
this.__needsSetup = false;
});
}
get _overlayInvokerNode() {
return Array.from(this.children).find(child => child.slot === 'invoker');
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
get _overlayBackdropNode() {
return Array.from(this.children).find(child => child.slot === 'backdrop');
if (this._overlayCtrl) {
this._teardownOverlayCtrl();
}
}
get _overlayContentNode() {
if (!this._cachedOverlayContentNode) {
this._cachedOverlayContentNode = Array.from(this.children).find(
child => child.slot === 'content',
);
}
return this._cachedOverlayContentNode;
}
get _overlayInvokerNode() {
return Array.from(this.children).find(child => child.slot === 'invoker');
}
get _overlayContentWrapperNode() {
return this.shadowRoot.querySelector('#overlay-content-node-wrapper');
}
get _overlayBackdropNode() {
return Array.from(this.children).find(child => child.slot === 'backdrop');
}
_setupOverlayCtrl() {
this._overlayCtrl = this._defineOverlay({
contentNode: this._overlayContentNode,
contentWrapperNode: this._overlayContentWrapperNode,
invokerNode: this._overlayInvokerNode,
backdropNode: this._overlayBackdropNode,
});
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
this._setupOpenCloseListeners();
get _overlayContentNode() {
if (!this._cachedOverlayContentNode) {
this._cachedOverlayContentNode = Array.from(this.children).find(
child => child.slot === 'content',
);
}
return this._cachedOverlayContentNode;
}
_teardownOverlayCtrl() {
this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController();
this._overlayCtrl.teardown();
}
get _overlayContentWrapperNode() {
return this.shadowRoot.querySelector('#overlay-content-node-wrapper');
}
/**
* When the opened state is changed by an Application Developer,cthe OverlayController is
* requested to show/hide. It might happen that this request is not honoured
* (intercepted in before-hide for instance), so that we need to sync the controller state
* to this webcomponent again, preventing eternal loops.
*/
async _setOpenedWithoutPropertyEffects(newOpened) {
this.__blockSyncToOverlayCtrl = true;
this.opened = newOpened;
await this.updateComplete;
this.__blockSyncToOverlayCtrl = false;
}
_setupOverlayCtrl() {
/** @type {OverlayController} */
this._overlayCtrl = this._defineOverlay({
contentNode: this._overlayContentNode,
contentWrapperNode: this._overlayContentWrapperNode,
invokerNode: this._overlayInvokerNode,
backdropNode: this._overlayBackdropNode,
});
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
this._setupOpenCloseListeners();
}
__setupSyncFromOverlayController() {
this.__onOverlayCtrlShow = () => {
this.opened = true;
};
_teardownOverlayCtrl() {
this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController();
/** @type {OverlayController} */
(this._overlayCtrl).teardown();
}
this.__onOverlayCtrlHide = () => {
this.opened = false;
};
/**
* When the opened state is changed by an Application Developer,cthe OverlayController is
* requested to show/hide. It might happen that this request is not honoured
* (intercepted in before-hide for instance), so that we need to sync the controller state
* to this webcomponent again, preventing eternal loops.
* @param {boolean} newOpened
*/
async _setOpenedWithoutPropertyEffects(newOpened) {
this.__blockSyncToOverlayCtrl = true;
this.opened = newOpened;
await this.updateComplete;
this.__blockSyncToOverlayCtrl = false;
}
this.__onBeforeShow = beforeShowEvent => {
const event = new CustomEvent('before-opened', { cancelable: true });
this.dispatchEvent(event);
if (event.defaultPrevented) {
// Check whether our current `.opened` state is not out of sync with overlayCtrl
this._setOpenedWithoutPropertyEffects(this._overlayCtrl.isShown);
beforeShowEvent.preventDefault();
}
};
__setupSyncFromOverlayController() {
this.__onOverlayCtrlShow = () => {
this.opened = true;
};
this.__onBeforeHide = beforeHideEvent => {
const event = new CustomEvent('before-closed', { cancelable: true });
this.dispatchEvent(event);
if (event.defaultPrevented) {
// Check whether our current `.opened` state is not out of sync with overlayCtrl
this._setOpenedWithoutPropertyEffects(this._overlayCtrl.isShown);
beforeHideEvent.preventDefault();
}
};
this.__onOverlayCtrlHide = () => {
this.opened = false;
};
this._overlayCtrl.addEventListener('show', this.__onOverlayCtrlShow);
this._overlayCtrl.addEventListener('hide', this.__onOverlayCtrlHide);
this._overlayCtrl.addEventListener('before-show', this.__onBeforeShow);
this._overlayCtrl.addEventListener('before-hide', this.__onBeforeHide);
}
/**
* @param {{ preventDefault: () => void; }} beforeShowEvent
*/
this.__onBeforeShow = beforeShowEvent => {
const event = new CustomEvent('before-opened', { cancelable: true });
this.dispatchEvent(event);
if (event.defaultPrevented) {
// Check whether our current `.opened` state is not out of sync with overlayCtrl
this._setOpenedWithoutPropertyEffects(
/** @type {OverlayController} */ (this._overlayCtrl).isShown,
);
beforeShowEvent.preventDefault();
}
};
__teardownSyncFromOverlayController() {
this._overlayCtrl.removeEventListener('show', this.__onOverlayCtrlShow);
this._overlayCtrl.removeEventListener('hide', this.__onOverlayCtrlHide);
this._overlayCtrl.removeEventListener('before-show', this.__onBeforeShow);
this._overlayCtrl.removeEventListener('before-hide', this.__onBeforeHide);
}
/**
* @param {{ preventDefault: () => void; }} beforeHideEvent
*/
this.__onBeforeHide = beforeHideEvent => {
const event = new CustomEvent('before-closed', { cancelable: true });
this.dispatchEvent(event);
if (event.defaultPrevented) {
// Check whether our current `.opened` state is not out of sync with overlayCtrl
this._setOpenedWithoutPropertyEffects(
/** @type {OverlayController} */
(this._overlayCtrl).isShown,
);
beforeHideEvent.preventDefault();
}
};
__syncToOverlayController() {
if (this.opened) {
this._overlayCtrl.show();
} else {
this._overlayCtrl.hide();
}
/** @type {OverlayController} */
(this._overlayCtrl).addEventListener('show', this.__onOverlayCtrlShow);
/** @type {OverlayController} */
(this._overlayCtrl).addEventListener('hide', this.__onOverlayCtrlHide);
/** @type {OverlayController} */
(this._overlayCtrl).addEventListener('before-show', this.__onBeforeShow);
/** @type {OverlayController} */
(this._overlayCtrl).addEventListener('before-hide', this.__onBeforeHide);
}
__teardownSyncFromOverlayController() {
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
'show',
/** @type {EventListener} */ (this.__onOverlayCtrlShow),
);
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
'hide',
/** @type {EventListener} */ (this.__onOverlayCtrlHide),
);
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
'before-show',
/** @type {EventListener} */ (this.__onBeforeShow),
);
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
'before-hide',
/** @type {EventListener} */ (this.__onBeforeHide),
);
}
__syncToOverlayController() {
if (this.opened) {
/** @type {OverlayController} */ (this._overlayCtrl).show();
} else {
/** @type {OverlayController} */ (this._overlayCtrl).hide();
}
},
);
}
};
export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
import { singletonManager } from 'singleton-manager';
// eslint-disable-next-line import/no-cycle
import { OverlaysManager } from './OverlaysManager.js';

@@ -8,4 +9,7 @@

/**
* @param {OverlaysManager} newOverlays
*/
export function setOverlays(newOverlays) {
overlays = newOverlays;
}
import { unsetSiblingsInert, setSiblingsInert } from './utils/inert-siblings.js';
import { globalOverlaysStyle } from './globalOverlaysStyle.js';
const isIOS = navigator.userAgent.match(/iPhone|iPad|iPod/i);
/**
* @typedef {object} OverlayController
* @param {(object) => TemplateResult} contentTemplate the template function
* which is called on update
* @param {(boolean, object) => void} sync updates shown state and data all together
* @param {(object) => void} update updates the overlay (with data if provided as a first argument)
* @param {Function} show shows the overlay
* @param {Function} hide hides the overlay
* @param {boolean} hasBackdrop displays a gray backdrop while the overlay is opened
* @param {boolean} isBlocking hides all other overlays once shown
* @param {boolean} preventsScroll prevents scrolling the background
* while this overlay is opened
* @param {boolean} trapsKeyboardFocus keeps focus within the overlay,
* and prevents interaction with the overlay background
* @typedef {import('./OverlayController.js').OverlayController} OverlayController
*/
const isIOS = navigator.userAgent.match(/iPhone|iPad|iPod/i);
/**

@@ -45,8 +33,9 @@ * `OverlaysManager` which manages overlays which are rendered into the body

*/
// eslint-disable-next-line class-methods-use-this
get globalRootNode() {
if (!this.constructor.__globalRootNode) {
this.constructor.__globalRootNode = this.constructor.__createGlobalRootNode();
this.constructor.__globalStyleNode = this.constructor.__createGlobalStyleNode();
if (!OverlaysManager.__globalRootNode) {
OverlaysManager.__globalRootNode = OverlaysManager.__createGlobalRootNode();
OverlaysManager.__globalStyleNode = OverlaysManager.__createGlobalStyleNode();
}
return this.constructor.__globalRootNode;
return OverlaysManager.__globalRootNode;
}

@@ -71,5 +60,8 @@

constructor() {
/** @type {OverlayController[]} */
this.__list = [];
/** @type {OverlayController[]} */
this.__shownList = [];
this.__siblingsInert = false;
/** @type {WeakMap<OverlayController, OverlayController[]>} */
this.__blockingMap = new WeakMap();

@@ -91,2 +83,5 @@ }

/**
* @param {OverlayController} ctrlToRemove
*/
remove(ctrlToRemove) {

@@ -99,2 +94,5 @@ if (!this.list.find(ctrl => ctrlToRemove === ctrl)) {

/**
* @param {OverlayController} ctrlToShow
*/
show(ctrlToShow) {

@@ -115,2 +113,5 @@ if (this.list.find(ctrl => ctrlToShow === ctrl)) {

/**
* @param {any} ctrlToHide
*/
hide(ctrlToHide) {

@@ -132,9 +133,13 @@ if (!this.list.find(ctrl => ctrlToHide === ctrl)) {

const rootNode = this.constructor.__globalRootNode;
const rootNode = OverlaysManager.__globalRootNode;
if (rootNode) {
rootNode.parentElement.removeChild(rootNode);
this.constructor.__globalRootNode = undefined;
if (rootNode.parentElement) {
rootNode.parentElement.removeChild(rootNode);
}
OverlaysManager.__globalRootNode = undefined;
document.head.removeChild(this.constructor.__globalStyleNode);
this.constructor.__globalStyleNode = undefined;
document.head.removeChild(
/** @type {HTMLStyleElement} */ (OverlaysManager.__globalStyleNode),
);
OverlaysManager.__globalStyleNode = undefined;
}

@@ -159,3 +164,3 @@ }

if (this.siblingsInert === false) {
if (this.constructor.__globalRootNode) {
if (OverlaysManager.__globalRootNode) {
setSiblingsInert(this.globalRootNode);

@@ -167,2 +172,3 @@ }

// @ts-ignore
informTrapsKeyboardFocusGotDisabled({ disabledCtrl, findNewTrap = true } = {}) {

@@ -177,3 +183,3 @@ const next = this.shownList.find(

} else if (this.siblingsInert === true) {
if (this.constructor.__globalRootNode) {
if (OverlaysManager.__globalRootNode) {
unsetSiblingsInert(this.globalRootNode);

@@ -207,3 +213,6 @@ }

/** Blocking */
/**
* Blocking
* @param {OverlayController} blockingCtrl
*/
requestToShowOnly(blockingCtrl) {

@@ -216,5 +225,10 @@ const controllersToHide = this.shownList.filter(ctrl => ctrl !== blockingCtrl);

/**
* @param {OverlayController} blockingCtrl
*/
retractRequestToShowOnly(blockingCtrl) {
if (this.__blockingMap.has(blockingCtrl)) {
const controllersWhichGotHidden = this.__blockingMap.get(blockingCtrl);
const controllersWhichGotHidden = /** @type {OverlayController[]} */ (this.__blockingMap.get(
blockingCtrl,
));
controllersWhichGotHidden.map(ctrl => ctrl.show());

@@ -224,1 +238,5 @@ }

}
/** @type {HTMLElement | undefined} */
OverlaysManager.__globalRootNode = undefined;
/** @type {HTMLStyleElement | undefined} */
OverlaysManager.__globalStyleNode = undefined;

@@ -48,3 +48,3 @@ /* eslint-disable no-param-reassign */

// Get the currently focused element
const activeElement = getDeepActiveElement();
const activeElement = /** @type {HTMLElement} */ (getDeepActiveElement());

@@ -78,3 +78,4 @@ /**

const initialFocus = focusableElements.find(e => e.hasAttribute('autofocus')) || rootElement;
let /** @type {HTMLElement} */ tabDetectionElement;
/** @type {HTMLElement} */
let tabDetectionElement;

@@ -108,3 +109,5 @@ // If root element will receive focus, it should have a tabindex of -1.

function isForwardTabInWindow() {
const compareMask = tabDetectionElement.compareDocumentPosition(document.activeElement);
const compareMask = tabDetectionElement.compareDocumentPosition(
/** @type {Element} */ (document.activeElement),
);
return compareMask === Node.DOCUMENT_POSITION_PRECEDING;

@@ -111,0 +114,0 @@ }

@@ -39,8 +39,7 @@ /**

/**
* @param {HTMLElement} element
* @param {HTMLElement|HTMLSlotElement} element
*/
function getChildNodes(element) {
if (element.localName === 'slot') {
/** @type {HTMLSlotElement} */
const slot = element;
const slot = /** @type {HTMLSlotElement} */ (element);
return slot.assignedNodes({ flatten: true });

@@ -55,7 +54,7 @@ }

/**
* @param {Node} node
* @param {Element} element
* @returns {boolean}
*/
function isVisibleElement(node) {
if (node.nodeType !== Node.ELEMENT_NODE) {
function isVisibleElement(element) {
if (element.nodeType !== Node.ELEMENT_NODE) {
return false;

@@ -66,7 +65,7 @@ }

// to treat is as such.
if (node.localName === 'slot') {
if (element.localName === 'slot') {
return true;
}
return isVisible(/** @type {HTMLElement} */ (node));
return isVisible(/** @type {HTMLElement} */ (element));
}

@@ -78,3 +77,3 @@

*
* @param {Node} node
* @param {Element} element
* @param {HTMLElement[]} nodes

@@ -84,18 +83,16 @@ * @returns {boolean} whether the returned node list should be sorted. This happens when

*/
function collectFocusableElements(node, nodes) {
function collectFocusableElements(element, nodes) {
// If not an element or not visible, no need to explore children.
if (!isVisibleElement(node)) {
if (!isVisibleElement(element)) {
return false;
}
/** @type {HTMLElement} */
const element = node;
const tabIndex = getTabindex(element);
const el = /** @type {HTMLElement} */ (element);
const tabIndex = getTabindex(el);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
nodes.push(element);
nodes.push(el);
}
const childNodes = getChildNodes(element);
const childNodes = /** @type {Element[]} */ (getChildNodes(el));
for (let i = 0; i < childNodes.length; i += 1) {

@@ -108,11 +105,11 @@ needsSort = collectFocusableElements(childNodes[i], nodes) || needsSort;

/**
* @param {Node} node
* @param {Element} element
* @returns {HTMLElement[]}
*/
export function getFocusableElements(node) {
export function getFocusableElements(element) {
/** @type {HTMLElement[]} */
const nodes = [];
const needsSort = collectFocusableElements(node, nodes);
const needsSort = collectFocusableElements(element, nodes);
return needsSort ? sortByTabIndex(nodes) : nodes;
}

@@ -11,3 +11,3 @@ /**

export function setSiblingsInert(element) {
const parentChildren = element.parentElement.children;
const parentChildren = /** @type {HTMLCollection} */ (element.parentElement?.children);
for (let i = 0; i < parentChildren.length; i += 1) {

@@ -28,3 +28,3 @@ const sibling = parentChildren[i];

export function unsetSiblingsInert(element) {
const parentChildren = element.parentElement.children;
const parentChildren = /** @type {HTMLCollection} */ (element.parentElement?.children);
for (let i = 0; i < parentChildren.length; i += 1) {

@@ -34,6 +34,6 @@ const sibling = parentChildren[i];

if (sibling !== element) {
sibling.removeAttribute('inert', '');
sibling.removeAttribute('aria-hidden', 'true');
sibling.removeAttribute('inert');
sibling.removeAttribute('aria-hidden');
}
}
}
import { getFocusableElements } from './get-focusable-elements.js';
export function simulateTab(node = document.body) {
const current = document.activeElement;
const current = /** @type {HTMLElement} */ (document.activeElement);
const all = getFocusableElements(node);

@@ -6,0 +6,0 @@

@@ -6,7 +6,13 @@ import { expect, html } from '@open-wc/testing';

const withDefaultGlobalConfig = () => ({
placementMode: 'global',
contentNode: fixtureSync(html`<p>my content</p>`),
});
/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
* @typedef {import('../types/OverlayConfig').ViewportPlacement} ViewportPlacement
*/
const withDefaultGlobalConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'global',
contentNode: fixtureSync(html`<p>my content</p>`),
});
describe('Global Positioning', () => {

@@ -54,3 +60,3 @@ afterEach(() => {

viewportConfig: {
placement: viewportPlacement,
placement: /** @type {ViewportPlacement} */ (viewportPlacement),
},

@@ -57,0 +63,0 @@ });

import { expect, fixture, fixtureSync, html } from '@open-wc/testing';
// @ts-ignore
import Popper from 'popper.js/dist/esm/popper.min.js';

@@ -6,10 +7,16 @@ import { OverlayController } from '../src/OverlayController.js';

const withLocalTestConfig = () => ({
placementMode: 'local',
contentNode: fixtureSync(html` <div>my content</div> `),
invokerNode: fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;">Invoker</div>
`),
});
/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
* @typedef {import('../types/OverlayConfig').ViewportPlacement} ViewportPlacement
*/
const withLocalTestConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'local',
contentNode: /** @type {HTMLElement} */ (fixtureSync(html` <div>my content</div> `)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;">Invoker</div>
`)),
});
describe('Local Positioning', () => {

@@ -24,7 +31,7 @@ // Please use absolute positions in the tests below to prevent the HTML generated by

await ctrl.show();
expect(ctrl._popper).to.be.an.instanceof(Popper);
expect(ctrl._popper.modifiers).to.exist;
expect(/** @type {Popper} */ (ctrl._popper)).to.be.an.instanceof(Popper);
expect(/** @type {Popper} */ (ctrl._popper).modifiers).to.exist;
await ctrl.hide();
expect(ctrl._popper).to.be.an.instanceof(Popper);
expect(ctrl._popper.modifiers).to.exist;
expect(/** @type {Popper} */ (ctrl._popper)).to.be.an.instanceof(Popper);
expect(/** @type {Popper} */ (ctrl._popper).modifiers).to.exist;
});

@@ -36,8 +43,8 @@

...withLocalTestConfig(),
contentNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div style="width: 80px; height: 30px; background: green;"></div>
`),
invokerNode: fixtureSync(html`
`)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 20px; height: 10px; background: orange;"></div>
`),
`)),
});

@@ -60,6 +67,8 @@ await fixture(html`

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;"></div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}></div>
`),
`)),
});

@@ -78,6 +87,8 @@ await fixture(html`

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;"></div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}></div>
`),
`)),
popperConfig: {

@@ -100,8 +111,10 @@ placement: 'left-start',

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;">invoker</div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;">invoker</div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}>
content
</div>
`),
`)),
popperConfig: {

@@ -122,6 +135,8 @@ placement: 'top-start',

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;"></div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}></div>
`),
`)),
popperConfig: {

@@ -146,4 +161,8 @@ modifiers: {

await ctrl.show();
const keepTogether = ctrl._popper.modifiers.find(item => item.name === 'keepTogether');
const offset = ctrl._popper.modifiers.find(item => item.name === 'offset');
const keepTogether = /** @type {Popper} */ (ctrl._popper).modifiers.find(
(/** @type {{ name: string }} */ item) => item.name === 'keepTogether',
);
const offset = /** @type {Popper} */ (ctrl._popper).modifiers.find(
(/** @type {{ name: string }} */ item) => item.name === 'offset',
);
expect(keepTogether.enabled).to.be.false;

@@ -157,6 +176,8 @@ expect(offset.enabled).to.be.true;

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;"></div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}></div>
`),
`)),
popperConfig: {

@@ -189,6 +210,8 @@ placement: 'top',

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;"></div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}></div>
`),
`)),
popperConfig: {

@@ -228,3 +251,5 @@ placement: 'top',

await ctrl.show();
expect(ctrl._popper.options.modifiers.offset.offset).to.equal('0, 20px');
expect(/** @type {Popper} */ (ctrl._popper).options.modifiers.offset.offset).to.equal(
'0, 20px',
);
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(

@@ -240,8 +265,10 @@ 'translate3d(10px, -40px, 0px)',

...withLocalTestConfig(),
contentNode: fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `),
invokerNode: fixtureSync(html`
contentNode: /** @type {HTMLElement} */ (fixtureSync(
html` <div style="width: 80px; height: 20px;"></div> `,
)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;" @click=${() => ctrl.show()}>
Invoker
</div>
`),
`)),
popperConfig: {

@@ -286,5 +313,5 @@ placement: 'top',

it('can set the contentNode minWidth as the invokerNode width', async () => {
const invokerNode = await fixture(html`
const invokerNode = /** @type {HTMLElement} */ (await fixture(html`
<div role="button" style="width: 60px;">invoker</div>
`);
`));
const ctrl = new OverlayController({

@@ -300,5 +327,5 @@ ...withLocalTestConfig(),

it('can set the contentNode maxWidth as the invokerNode width', async () => {
const invokerNode = await fixture(html`
const invokerNode = /** @type {HTMLElement} */ (await fixture(html`
<div role="button" style="width: 60px;">invoker</div>
`);
`));
const ctrl = new OverlayController({

@@ -314,5 +341,5 @@ ...withLocalTestConfig(),

it('can set the contentNode width as the invokerNode width', async () => {
const invokerNode = await fixture(html`
const invokerNode = /** @type {HTMLElement} */ (await fixture(html`
<div role="button" style="width: 60px;">invoker</div>
`);
`));
const ctrl = new OverlayController({

@@ -319,0 +346,0 @@ ...withLocalTestConfig(),

@@ -19,15 +19,22 @@ /* eslint-disable no-new */

const withGlobalTestConfig = () => ({
placementMode: 'global',
contentNode: fixtureSync(html`<div>my content</div>`),
});
/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
* @typedef {import('../types/OverlayConfig').ViewportPlacement} ViewportPlacement
*/
const withLocalTestConfig = () => ({
placementMode: 'local',
contentNode: fixtureSync(html`<div>my content</div>`),
invokerNode: fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;">Invoker</div>
`),
});
const withGlobalTestConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'global',
contentNode: /** @type {HTMLElement} */ (fixtureSync(html`<div>my content</div>`)),
});
const withLocalTestConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'local',
contentNode: /** @type {HTMLElement} */ (fixtureSync(html`<div>my content</div>`)),
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
<div role="button" style="width: 100px; height: 20px;">Invoker</div>
`)),
});
afterEach(() => {

@@ -56,6 +63,11 @@ overlays.teardown();

describe('Z-index on local overlays', () => {
/** @type {HTMLElement} */
let contentNode;
/**
* @param {string} zIndexVal
* @param {{ mode?: string }} options
*/
async function createZNode(zIndexVal, { mode } = {}) {
if (mode === 'global') {
contentNode = await fixture(html`
contentNode = /** @type {HTMLElement} */ (await fixture(html`
<div class="z-index--${zIndexVal}">

@@ -69,6 +81,8 @@ <style>

</div>
`);
`));
}
if (mode === 'inline') {
contentNode = await fixture(html` <div>I should be on top</div> `);
contentNode = /** @type {HTMLElement} */ (await fixture(
html` <div>I should be on top</div> `,
));
contentNode.style.zIndex = zIndexVal;

@@ -137,15 +151,15 @@ }

...withLocalTestConfig(),
invokerNode: await fixture(html`<button>Invoker</button>`),
invokerNode: /** @type {HTMLElement} */ (await fixture(html`<button>Invoker</button>`)),
});
expect(ctrl._renderTarget).to.be.undefined;
expect(ctrl.content).to.equal(ctrl.invokerNode.nextElementSibling);
expect(ctrl.content).to.equal(ctrl.invokerNode?.nextElementSibling);
});
it('keeps local target for placement mode "local" when already connected', async () => {
const parentNode = await fixture(html`
const parentNode = /** @type {HTMLElement} */ (await fixture(html`
<div id="parent">
<div id="content">Content</div>
</div>
`);
const contentNode = parentNode.querySelector('#content');
`));
const contentNode = /** @type {HTMLElement} */ (parentNode.querySelector('#content'));
const ctrl = new OverlayController({

@@ -200,3 +214,4 @@ ...withLocalTestConfig(),

shadowHost.attachShadow({ mode: 'open' });
shadowHost.shadowRoot.innerHTML = `
/** @type {ShadowRoot} */
(shadowHost.shadowRoot).innerHTML = `
<div id="contentWrapperNode">

@@ -211,3 +226,3 @@ <slot name="contentNode"></slot>

const wrapper = await fixture('<div id="wrapper"></div>');
const wrapper = /** @type {HTMLElement} */ (await fixture('<div id="wrapper"></div>'));
// Ensure the contentNode is connected to DOM

@@ -239,3 +254,3 @@ wrapper.appendChild(shadowHost);

...withGlobalTestConfig(),
contentNode: await fixture('<p>direct node</p>'),
contentNode: /** @type {HTMLElement} */ (await fixture('<p>direct node</p>')),
});

@@ -248,3 +263,3 @@ expect(ctrl.contentNode).to.have.trimmed.text('direct node');

...withGlobalTestConfig(),
invokerNode: await fixture('<button>invoke</button>'),
invokerNode: /** @type {HTMLElement} */ (await fixture('<button>invoke</button>')),
});

@@ -258,3 +273,3 @@ expect(ctrl.invokerNode).to.have.trimmed.text('invoke');

shadowHost.attachShadow({ mode: 'open' });
shadowHost.shadowRoot.innerHTML = `
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `
<div id="contentWrapperNode">

@@ -275,3 +290,5 @@ <slot name="contentNode"></slot>

contentNode,
contentWrapperNode: shadowHost.shadowRoot.getElementById('contentWrapperNode'),
contentWrapperNode: /** @type {HTMLElement} */ (
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).getElementById('contentWrapperNode')
),
});

@@ -285,3 +302,3 @@

it('uses contentWrapperNode as provided for local positioning', async () => {
const el = await fixture(html`
const el = /** @type {HTMLElement} */ (await fixture(html`
<div id="contentWrapperNode">

@@ -291,5 +308,5 @@ <div id="contentNode"></div>

</div>
`);
`));
const contentNode = el.querySelector('#contentNode');
const contentNode = /** @type {HTMLElement} */ (el.querySelector('#contentNode'));
const contentWrapperNode = el;

@@ -303,3 +320,3 @@

expect(ctrl._contentWrapperNode).to.equal(contentWrapperNode);
expect(ctrl.contentWrapperNode).to.equal(contentWrapperNode);
});

@@ -332,5 +349,5 @@ });

it('keeps focus within the overlay e.g. you can not tab out by accident', async () => {
const contentNode = await fixture(html`
const contentNode = /** @type {HTMLElement} */ (await fixture(html`
<div><input id="input1" /><input id="input2" /></div>
`);
`));
const ctrl = new OverlayController({

@@ -343,3 +360,5 @@ ...withGlobalTestConfig(),

const elOutside = await fixture(html`<button>click me</button>`);
const elOutside = /** @type {HTMLElement} */ (await fixture(
html`<button>click me</button>`,
));
const input1 = ctrl.contentNode.querySelectorAll('input')[0];

@@ -351,2 +370,3 @@ const input2 = ctrl.contentNode.querySelectorAll('input')[1];

const event = new CustomEvent('keydown', { detail: 0, bubbles: true });
// @ts-ignore override private key
event.keyCode = keyCodes.tab;

@@ -360,3 +380,3 @@ window.dispatchEvent(event);

it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
const contentNode = await fixture(html`<div><input /></div>`);
const contentNode = /** @type {HTMLElement} */ (await fixture(html`<div><input /></div>`));

@@ -369,7 +389,7 @@ const ctrl = new OverlayController({

// add element to dom to allow focus
await fixture(html`${ctrl.content}`);
/** @type {HTMLElement} */ (await fixture(html`${ctrl.content}`));
await ctrl.show();
const elOutside = await fixture(html`<input />`);
const input = ctrl.contentNode.querySelector('input');
const elOutside = /** @type {HTMLElement} */ (await fixture(html`<input />`));
const input = /** @type {HTMLInputElement} */ (ctrl.contentNode.querySelector('input'));

@@ -412,3 +432,3 @@ input.focus();

ctrl.contentNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
await aTimeout();
await aTimeout(0);
expect(ctrl.isShown).to.be.false;

@@ -436,3 +456,3 @@ });

document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
await aTimeout();
await aTimeout(0);
expect(ctrl.isShown).to.be.false;

@@ -454,3 +474,3 @@ });

it('hides on outside click', async () => {
const contentNode = await fixture('<div>Content</div>');
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
const ctrl = new OverlayController({

@@ -464,3 +484,3 @@ ...withGlobalTestConfig(),

document.body.click();
await aTimeout();
await aTimeout(0);
expect(ctrl.isShown).to.be.false;

@@ -470,4 +490,4 @@ });

it('doesn\'t hide on "inside" click', async () => {
const invokerNode = await fixture('<button>Invoker</button>');
const contentNode = await fixture('<div>Content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture('<button>Invoker</button>'));
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
const ctrl = new OverlayController({

@@ -482,4 +502,4 @@ ...withGlobalTestConfig(),

// Don't hide on invoker click
ctrl.invokerNode.click();
await aTimeout();
ctrl.invokerNode?.click();
await aTimeout(0);
expect(ctrl.isShown).to.be.true;

@@ -489,3 +509,3 @@

ctrl.contentNode.click();
await aTimeout();
await aTimeout(0);

@@ -502,4 +522,4 @@ expect(ctrl.isShown).to.be.true;

it('doesn\'t hide on "inside sub shadow dom" click', async () => {
const invokerNode = await fixture('<button>Invoker</button>');
const contentNode = await fixture('<div>Content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture('<button>Invoker</button>'));
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
const ctrl = new OverlayController({

@@ -522,3 +542,4 @@ ...withGlobalTestConfig(),

connectedCallback() {
this.shadowRoot.innerHTML = '<div><button>click me</button></div>';
/** @type {ShadowRoot} */
(this.shadowRoot).innerHTML = '<div><button>click me</button></div>';
}

@@ -529,8 +550,8 @@ },

ctrl.updateConfig({
contentNode: await fixture(html`
<div>
<div>Content</div>
<${tag}></${tag}>
</div>
`),
contentNode: /** @type {HTMLElement} */ (await fixture(html`
<div>
<div>Content</div>
<${tag}></${tag}>
</div>
`)),
});

@@ -540,5 +561,7 @@ await ctrl.show();

// Don't hide on inside shadowDom click
ctrl.contentNode.querySelector(tagString).shadowRoot.querySelector('button').click();
/** @type {ShadowRoot} */
// @ts-expect-error
(ctrl.contentNode.querySelector(tagString).shadowRoot).querySelector('button').click();
await aTimeout();
await aTimeout(0);
expect(ctrl.isShown).to.be.true;

@@ -554,4 +577,6 @@

it('works with 3rd party code using "event.stopPropagation()" on bubble phase', async () => {
const invokerNode = await fixture('<div role="button">Invoker</div>');
const contentNode = await fixture('<div>Content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">Invoker</div>',
));
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
const ctrl = new OverlayController({

@@ -563,3 +588,7 @@ ...withLocalTestConfig(),

});
const dom = await fixture(`
const dom = await fixture(
/**
* @param {{ stopPropagation: () => any; }} e
*/
`
<div>

@@ -573,7 +602,8 @@ <div id="popup">${invokerNode}${contentNode}</div>

></div>
<third-party-noise @click="${e => e.stopPropagation()}">
<third-party-noise @click="${(/** @type {Event} */ e) => e.stopPropagation()}">
This element prevents our handlers from reaching the document click handler.
</third-party-noise>
</div>
`);
`,
);

@@ -583,4 +613,5 @@ await ctrl.show();

dom.querySelector('third-party-noise').click();
await aTimeout();
/** @type {HTMLElement} */
(dom.querySelector('third-party-noise')).click();
await aTimeout(0);
expect(ctrl.isShown).to.equal(false);

@@ -594,4 +625,6 @@

it('works with 3rd party code using "event.stopPropagation()" on capture phase', async () => {
const invokerNode = await fixture(html`<div role="button">Invoker</div>`);
const contentNode = await fixture('<div>Content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
html`<div role="button">Invoker</div>`,
));
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
const ctrl = new OverlayController({

@@ -603,3 +636,3 @@ ...withLocalTestConfig(),

});
const dom = await fixture(`
const dom = /** @type {HTMLElement} */ (await fixture(`
<div>

@@ -617,7 +650,8 @@ <div id="popup">${invokerNode}${ctrl.content}</div>

</div>
`);
`));
dom.querySelector('third-party-noise').addEventListener(
/** @type {HTMLElement} */
(dom.querySelector('third-party-noise')).addEventListener(
'click',
event => {
(/** @type {Event} */ event) => {
event.stopPropagation();

@@ -631,4 +665,5 @@ },

dom.querySelector('third-party-noise').click();
await aTimeout();
/** @type {HTMLElement} */
(dom.querySelector('third-party-noise')).click();
await aTimeout(0);
expect(ctrl.isShown).to.equal(false);

@@ -642,3 +677,3 @@

it('doesn\'t hide on "inside label" click', async () => {
const contentNode = await fixture(`
const contentNode = /** @type {HTMLElement} */ (await fixture(`
<div>

@@ -648,4 +683,4 @@ <label for="test">test</label>

Content
</div>`);
const labelNode = contentNode.querySelector('label[for=test]');
</div>`));
const labelNode = /** @type {HTMLElement} */ (contentNode.querySelector('label[for=test]'));
const ctrl = new OverlayController({

@@ -660,3 +695,3 @@ ...withGlobalTestConfig(),

labelNode.click();
await aTimeout();
await aTimeout(0);

@@ -669,3 +704,3 @@ expect(ctrl.isShown).to.be.true;

it('focuses body when hiding by default', async () => {
const contentNode = await fixture('<div><input /></div>');
const contentNode = /** @type {HTMLElement} */ (await fixture('<div><input /></div>'));
const ctrl = new OverlayController({

@@ -680,3 +715,3 @@ ...withGlobalTestConfig(),

await ctrl.show();
const input = contentNode.querySelector('input');
const input = /** @type {HTMLInputElement} */ (contentNode.querySelector('input'));
input.focus();

@@ -691,4 +726,6 @@ expect(document.activeElement).to.equal(input);

it('supports elementToFocusAfterHide option to focus it when hiding', async () => {
const input = await fixture('<input />');
const contentNode = await fixture('<div><textarea></textarea></div>');
const input = /** @type {HTMLElement} */ (await fixture('<input />'));
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div><textarea></textarea></div>',
));
const ctrl = new OverlayController({

@@ -701,3 +738,3 @@ ...withGlobalTestConfig(),

await ctrl.show();
const textarea = contentNode.querySelector('textarea');
const textarea = /** @type {HTMLTextAreaElement} */ (contentNode.querySelector('textarea'));
textarea.focus();

@@ -711,4 +748,6 @@ expect(document.activeElement).to.equal(textarea);

it('allows to set elementToFocusAfterHide on show', async () => {
const input = await fixture('<input />');
const contentNode = await fixture('<div><textarea></textarea></div>');
const input = /** @type {HTMLElement} */ (await fixture('<input />'));
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div><textarea></textarea></div>',
));
const ctrl = new OverlayController({

@@ -723,3 +762,3 @@ ...withGlobalTestConfig(),

await ctrl.show(input);
const textarea = contentNode.querySelector('textarea');
const textarea = /** @type {HTMLTextAreaElement} */ (contentNode.querySelector('textarea'));
textarea.focus();

@@ -1116,3 +1155,3 @@ expect(document.activeElement).to.equal(textarea);

...withLocalTestConfig(),
contentNode: await fixture(html`<div>content1</div>`),
contentNode: /** @type {HTMLElement} */ (await fixture(html`<div>content1</div>`)),
});

@@ -1125,3 +1164,3 @@ await ctrl.show(); // Popper adds inline styles

placementMode: 'local',
contentNode: await fixture(html`<div>content2</div>`),
contentNode: /** @type {HTMLElement} */ (await fixture(html`<div>content2</div>`)),
});

@@ -1132,3 +1171,3 @@ expect(ctrl.contentNode.textContent).to.include('content2');

it('respects the initial config provided to new OverlayController(initialConfig)', async () => {
const contentNode = fixtureSync(html`<div>my content</div>`);
const contentNode = /** @type {HTMLElement} */ (fixtureSync(html`<div>my content</div>`));

@@ -1138,3 +1177,3 @@ const ctrl = new OverlayController({

placementMode: 'global',
handlesAccesibility: true,
handlesAccessibility: true,
contentNode,

@@ -1148,3 +1187,3 @@ });

expect(ctrl.placementMode).to.equal('local');
expect(ctrl.handlesAccesibility).to.equal(true);
expect(ctrl.handlesAccessibility).to.equal(true);
expect(ctrl.contentNode).to.equal(contentNode);

@@ -1155,3 +1194,3 @@ });

it.skip('allows for updating viewport config placement only, while keeping the content shown', async () => {
const contentNode = fixtureSync(html`<div>my content</div>`);
const contentNode = /** @type {HTMLElement} */ (fixtureSync(html`<div>my content</div>`));

@@ -1161,3 +1200,3 @@ const ctrl = new OverlayController({

placementMode: 'global',
handlesAccesibility: true,
handlesAccessibility: true,
contentNode,

@@ -1168,3 +1207,3 @@ });

expect(
ctrl._contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
);

@@ -1175,5 +1214,3 @@ expect(ctrl.isShown).to.be.true;

expect(
ctrl._contentWrapperNode.classList.contains(
'global-overlays__overlay-container--top-right',
),
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--top-right'),
);

@@ -1186,3 +1223,5 @@ expect(ctrl.isShown).to.be.true;

it('synchronizes [aria-expanded] on invoker', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1193,7 +1232,7 @@ ...withLocalTestConfig(),

});
expect(ctrl.invokerNode.getAttribute('aria-expanded')).to.equal('false');
expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal('false');
await ctrl.show();
expect(ctrl.invokerNode.getAttribute('aria-expanded')).to.equal('true');
expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal('true');
await ctrl.hide();
expect(ctrl.invokerNode.getAttribute('aria-expanded')).to.equal('false');
expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal('false');
});

@@ -1210,3 +1249,5 @@

it('preserves content id when present', async () => {
const contentNode = await fixture('<div id="preserved">content</div>');
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div id="preserved">content</div>',
));
const ctrl = new OverlayController({

@@ -1221,3 +1262,5 @@ ...withLocalTestConfig(),

it('adds [role=dialog] on content', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1232,4 +1275,8 @@ ...withLocalTestConfig(),

it('preserves [role] on content when present', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const contentNode = await fixture('<div role="menu">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div role="menu">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1250,3 +1297,3 @@ ...withLocalTestConfig(),

handlesAccessibility: true,
invokerNode: null,
invokerNode: undefined,
});

@@ -1346,3 +1393,5 @@ properlyInstantiated = true;

it('adds [aria-describedby] on invoker', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1354,7 +1403,9 @@ ...withLocalTestConfig(),

});
expect(ctrl.invokerNode.getAttribute('aria-describedby')).to.equal(ctrl._contentId);
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(ctrl._contentId);
});
it('adds [aria-labelledby] on invoker when invokerRelation is label', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1367,8 +1418,10 @@ ...withLocalTestConfig(),

});
expect(ctrl.invokerNode.getAttribute('aria-describedby')).to.equal(null);
expect(ctrl.invokerNode.getAttribute('aria-labelledby')).to.equal(ctrl._contentId);
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(null);
expect(ctrl.invokerNode?.getAttribute('aria-labelledby')).to.equal(ctrl._contentId);
});
it('adds [role=tooltip] on content', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1385,3 +1438,5 @@ ...withLocalTestConfig(),

it('restores [role] on dialog content', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const ctrl = new OverlayController({

@@ -1398,4 +1453,8 @@ ...withLocalTestConfig(),

it('restores [role] on tooltip content', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const contentNode = await fixture('<div role="presentation">content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div role="presentation">content</div>',
));
const ctrl = new OverlayController({

@@ -1414,4 +1473,8 @@ ...withLocalTestConfig(),

it('restores [aria-describedby] on content', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const contentNode = await fixture('<div role="presentation">content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div role="presentation">content</div>',
));
const ctrl = new OverlayController({

@@ -1430,4 +1493,8 @@ ...withLocalTestConfig(),

it('restores [aria-labelledby] on content', async () => {
const invokerNode = await fixture('<div role="button">invoker</div>');
const contentNode = await fixture('<div role="presentation">content</div>');
const invokerNode = /** @type {HTMLElement} */ (await fixture(
'<div role="button">invoker</div>',
));
const contentNode = /** @type {HTMLElement} */ (await fixture(
'<div role="presentation">content</div>',
));
const ctrl = new OverlayController({

@@ -1464,2 +1531,3 @@ ...withLocalTestConfig(),

new OverlayController({
// @ts-ignore
placementMode: 'invalid',

@@ -1483,3 +1551,3 @@ });

shadowHost.attachShadow({ mode: 'open' });
shadowHost.shadowRoot.innerHTML = `
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `
<div id="contentWrapperNode">

@@ -1486,0 +1554,0 @@ <slot name="contentNode"></slot>

@@ -5,8 +5,14 @@ import { expect, fixture, html } from '@open-wc/testing';

/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
*/
describe('OverlaysManager', () => {
/** @type {OverlayConfig} */
let defaultOptions;
/** @type {OverlaysManager} */
let mngr;
beforeEach(async () => {
const contentNode = await fixture(html`<p>my content</p>`);
const contentNode = /** @type {HTMLElement} */ (await fixture(html`<p>my content</p>`));

@@ -40,4 +46,4 @@ defaultOptions = {

// safety check via private access (do not use this)
expect(mngr.constructor.__globalRootNode).to.be.undefined;
expect(mngr.constructor.__globalStyleNode).to.be.undefined;
expect(OverlaysManager.__globalRootNode).to.be.undefined;
expect(OverlaysManager.__globalStyleNode).to.be.undefined;
});

@@ -44,0 +50,0 @@

@@ -16,5 +16,5 @@ import { expect, fixture, defineCE } from '@open-wc/testing';

const el1 = element.querySelector('#el-1');
const el2 = element.querySelector('#el-2');
const el3 = element.querySelector('#el-3');
const el1 = /** @type {HTMLElement} */ (element.querySelector('#el-1'));
const el2 = /** @type {HTMLElement} */ (element.querySelector('#el-2'));
const el3 = /** @type {HTMLElement} */ (element.querySelector('#el-3'));

@@ -63,10 +63,13 @@ el1.focus();

const elA = element.querySelector(elTag).shadowRoot;
const elB = elA.querySelector(elNestedTag).shadowRoot;
const elA1 = elA.querySelector('#el-a-1');
const elA2 = elA.querySelector('#el-a-2');
const elB1 = elB.querySelector('#el-b-1');
const elB2 = elB.querySelector('#el-b-1');
const el1 = element.querySelector('#el-1');
const elTagEl = /** @type {HTMLElement} */ (element.querySelector(elTag));
const elA = /** @type {ShadowRoot} */ (elTagEl.shadowRoot);
const elNestedTagEl = /** @type {HTMLElement} */ (elA.querySelector(elNestedTag));
const elB = /** @type {ShadowRoot} */ (elNestedTagEl.shadowRoot);
const elA1 = /** @type {HTMLElement} */ (elA.querySelector('#el-a-1'));
const elA2 = /** @type {HTMLElement} */ (elA.querySelector('#el-a-2'));
const elB1 = /** @type {HTMLElement} */ (elB.querySelector('#el-b-1'));
const elB2 = /** @type {HTMLElement} */ (elB.querySelector('#el-b-1'));
const el1 = /** @type {HTMLElement} */ (element.querySelector('#el-1'));
elA1.focus();

@@ -73,0 +76,0 @@ expect(getDeepActiveElement()).to.eql(elA1);

import { expect, fixture, html } from '@open-wc/testing';
// @ts-expect-error
import { renderLitAsNode } from '@lion/helpers';
import { getDeepActiveElement } from '../../src/utils/get-deep-active-element.js';

@@ -11,2 +11,3 @@ import { getFocusableElements } from '../../src/utils/get-focusable-elements.js';

const event = new CustomEvent('keydown', { detail: 0, bubbles: true });
// @ts-ignore override keyCode
event.keyCode = keyCodes.tab;

@@ -16,2 +17,5 @@ window.dispatchEvent(event);

/**
* @param {HTMLElement} elToRecieveFocus
*/
function simulateTabInWindow(elToRecieveFocus) {

@@ -82,3 +86,3 @@ window.dispatchEvent(new Event('blur'));

await fixture(lightDomTemplate);
const root = document.getElementById('rootElement');
const root = /** @type {HTMLElement} */ (document.getElementById('rootElement'));
const { disconnect } = containFocus(root);

@@ -95,3 +99,3 @@

await fixture(lightDomAutofocusTemplate);
const el = document.querySelector('input[autofocus]');
const el = /** @type {HTMLElement} */ (document.querySelector('input[autofocus]'));
const { disconnect } = containFocus(el);

@@ -106,7 +110,7 @@

await fixture(lightDomTemplate);
const root = document.getElementById('rootElement');
const root = /** @type {HTMLElement} */ (document.getElementById('rootElement'));
const focusableElements = getFocusableElements(root);
const { disconnect } = containFocus(root);
document.getElementById('outside-1').focus();
/** @type {HTMLElement} */ (document.getElementById('outside-1')).focus();

@@ -121,3 +125,3 @@ simulateTabWithinContainFocus();

await fixture(lightDomTemplate);
const root = document.getElementById('rootElement');
const root = /** @type {HTMLElement} */ (document.getElementById('rootElement'));
const focusableElements = getFocusableElements(root);

@@ -136,3 +140,3 @@ const { disconnect } = containFocus(root);

await fixture(lightDomTemplate);
const root = document.getElementById('rootElement');
const root = /** @type {HTMLElement} */ (document.getElementById('rootElement'));
const focusableElements = getFocusableElements(root);

@@ -157,3 +161,3 @@ const { disconnect } = containFocus(root);

await fixture(lightDomTemplate);
const root = document.getElementById('rootElement');
const root = /** @type {HTMLElement} */ (document.getElementById('rootElement'));
const focusableElements = getFocusableElements(root);

@@ -163,7 +167,7 @@ const { disconnect } = containFocus(root);

// Simulate tab in window
simulateTabInWindow(document.getElementById('outside-1'));
simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-1')));
expect(getDeepActiveElement()).to.equal(focusableElements[0]);
// Simulate shift+tab in window
simulateTabInWindow(document.getElementById('outside-2'));
simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-2')));
expect(getDeepActiveElement()).to.equal(focusableElements[focusableElements.length - 1]);

@@ -176,3 +180,3 @@

const el = await fixture(html`${createShadowDomNode()}`);
const root = el.querySelector('#rootElementShadow');
const root = /** @type {HTMLElement} */ (el.querySelector('#rootElementShadow'));
const focusableElements = getFocusableElements(root);

@@ -182,7 +186,7 @@ const { disconnect } = containFocus(root);

// Simulate tab in window
simulateTabInWindow(document.getElementById('outside-1'));
simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-1')));
expect(getDeepActiveElement()).to.equal(focusableElements[0]);
// Simulate shift+tab in window
simulateTabInWindow(document.getElementById('outside-2'));
simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-2')));
expect(getDeepActiveElement()).to.equal(focusableElements[focusableElements.length - 1]);

@@ -195,3 +199,3 @@

const el = await fixture(html`${createShadowDomNode()}`);
const root = el.querySelector('#rootElementShadow');
const root = /** @type {HTMLElement} */ (el.querySelector('#rootElementShadow'));
const focusableElements = getFocusableElements(root);

@@ -198,0 +202,0 @@ const { disconnect } = containFocus(root);

@@ -9,3 +9,3 @@ /**

// eslint-disable-next-line no-unused-vars
const [_, transformType, positionPart] = cssValue.match(/(.*)\((.*?)\)/);
const [, transformType, positionPart] = cssValue.match(/(.*)\((.*?)\)/) || [];
const normalizedNumbers = positionPart

@@ -12,0 +12,0 @@ .split(',')

@@ -7,3 +7,5 @@ import { expect, fixture } from '@open-wc/testing';

it('returns true for static block elements', async () => {
const element = await fixture(`<div style="width:10px; height:10px;"></div>`);
const element = /** @type {HTMLElement} */ (await fixture(
`<div style="width:10px; height:10px;"></div>`,
));

@@ -14,3 +16,5 @@ expect(isVisible(element)).to.equal(true);

it('returns false for hidden static block elements', async () => {
const element = await fixture(`<div style="width:10px; height:10px;" hidden></div>`);
const element = /** @type {HTMLElement} */ (await fixture(
`<div style="width:10px; height:10px;" hidden></div>`,
));

@@ -21,5 +25,5 @@ expect(isVisible(element)).to.equal(false);

it('returns true for relative block elements', async () => {
const element = await fixture(
const element = /** @type {HTMLElement} */ (await fixture(
`<div style="width:10px; height:10px; position:relative; top:10px; left:10px;"></div>`,
);
));

@@ -30,5 +34,5 @@ expect(isVisible(element)).to.equal(true);

it('returns false for hidden relative block elements', async () => {
const element = await fixture(
const element = /** @type {HTMLElement} */ (await fixture(
`<div style="width:10px; height:10px; position:relative; top:10px; left:10px;" hidden></div>`,
);
));

@@ -39,5 +43,5 @@ expect(isVisible(element)).to.equal(false);

it('returns true for absolute block elements', async () => {
const element = await fixture(`
const element = /** @type {HTMLElement} */ (await fixture(`
<div style="width:10px; height:10px; position:absolute; top:10px; left:10px;"></div>
`);
`));

@@ -48,5 +52,5 @@ expect(isVisible(element)).to.equal(true);

it('returns false for hidden absolute block elements', async () => {
const element = await fixture(`
const element = /** @type {HTMLElement} */ (await fixture(`
<div style="width:10px; height:10px; position:absolute; top:10px; left:10px;" hidden></div>
`);
`));

@@ -57,5 +61,5 @@ expect(isVisible(element)).to.equal(false);

it('returns true for relative block elements', async () => {
const element = await fixture(`
const element = /** @type {HTMLElement} */ (await fixture(`
<div style="width:10px; height:10px; position:fixed;top:10px; left:10px;"></div>
`);
`));

@@ -66,5 +70,5 @@ expect(isVisible(element)).to.equal(true);

it('returns true for relative block elements', async () => {
const element = await fixture(`
const element = /** @type {HTMLElement} */ (await fixture(`
<div style="width:10px; height:10px; position:fixed;top:10px; left:10px;" hidden></div>
`);
`));

@@ -75,3 +79,3 @@ expect(isVisible(element)).to.equal(false);

it('returns true for inline elements', async () => {
const element = await fixture(`<span>Inline content</span>`);
const element = /** @type {HTMLElement} */ (await fixture(`<span>Inline content</span>`));

@@ -82,3 +86,3 @@ expect(isVisible(element)).to.equal(true);

it('returns true for inline elements without content', async () => {
const element = await fixture(`<span></span>`);
const element = /** @type {HTMLElement} */ (await fixture(`<span></span>`));

@@ -89,3 +93,5 @@ expect(isVisible(element)).to.equal(true);

it('returns true for static block elements with 0 dimensions', async () => {
const element = await fixture(`<div style="width:0; height:0;"></div>`);
const element = /** @type {HTMLElement} */ (await fixture(
`<div style="width:0; height:0;"></div>`,
));

@@ -96,3 +102,5 @@ expect(isVisible(element)).to.equal(true);

it('returns false for hidden inline elements', async () => {
const element = await fixture(`<span hidden>Inline content</span>`);
const element = /** @type {HTMLElement} */ (await fixture(
`<span hidden>Inline content</span>`,
));

@@ -103,5 +111,5 @@ expect(isVisible(element)).to.equal(false);

it('returns false invisible elements', async () => {
const element = await fixture(
const element = /** @type {HTMLElement} */ (await fixture(
`<div style="width:10px; height:10px; visibility: hidden;"></div>`,
);
));

@@ -112,3 +120,3 @@ expect(isVisible(element)).to.equal(false);

it('returns false when hidden by parent', async () => {
const element = await fixture(`
const element = /** @type {HTMLElement} */ (await fixture(`
<div hidden>

@@ -118,5 +126,5 @@ <div id="target" style="width:10px; height:10px;"></div>

</div>
`);
`));
const target = element.querySelector('#target');
const target = /** @type {HTMLElement} */ (element.querySelector('#target'));
expect(isVisible(target)).to.equal(false);

@@ -126,3 +134,3 @@ });

it('returns false when invisible by parent', async () => {
const element = await fixture(`
const element = /** @type {HTMLElement} */ (await fixture(`
<div style="visibility: hidden;">

@@ -132,7 +140,7 @@ <div id="target" style="width:10px; height:10px;"></div>

</div>
`);
`));
const target = element.querySelector('#target');
const target = /** @type {HTMLElement} */ (element.querySelector('#target'));
expect(isVisible(target)).to.equal(false);
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc