@lion/overlays
Advanced tools
Comparing version 0.18.0 to 0.19.0
# 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 @@ |
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); | ||
}); | ||
}); |
285561
135
5614
+ Added@lion/core@0.12.0(transitive)
- Removed@lion/core@0.11.0(transitive)
Updated@lion/core@0.12.0