`;
}
}
diff --git a/packages/overlays/src/OverlayController.js b/packages/overlays/src/OverlayController.js
index 963a268f8..85a796683 100644
--- a/packages/overlays/src/OverlayController.js
+++ b/packages/overlays/src/OverlayController.js
@@ -10,6 +10,66 @@ async function preloadPopper() {
const GLOBAL_OVERLAYS_CONTAINER_CLASS = 'global-overlays__overlay-container';
const GLOBAL_OVERLAYS_CLASS = 'global-overlays__overlay';
+/**
+ * @desc OverlayController is the fundament for every single type of overlay. With the right
+ * configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
+ * bottom/top/left/right sheets etc.
+ *
+ * ### About contentNode, contentWrapperNode and renderTarget.
+ *
+ * #### contentNode
+ * Node containing actual overlay contents.
+ * It will not be touched by the OverlayController, it will only set attributes needed
+ * for accessibility.
+ *
+ * #### contentWrapperNode
+ * The 'positioning' element.
+ * For local overlays, this node will be provided to Popper and all
+ * inline positioning styles will be added here. It will also act as the container of an arrow
+ * element (the arrow needs to be a sibling of contentNode for Popper to work correctly).
+ * When projecting a contentNode from a shadowRoot, it is essential to have the wrapper in
+ * shadow dom, so that contentNode can be styled via `::slotted` from the shadow root.
+ * The Popper arrow can then be styled from that same shadow root as well.
+ * For global overlays, the contentWrapperNode will be appended to the globalRootNode structure.
+ *
+ * #### renderTarget
+ * 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)
+ * we need to know where we should reappend contentWrapperNode (or contentNode in case it's projected)
+ *
+ * So a regular flow can be summarized as follows:
+ * 1. Application Developer spawns an OverlayController with a contentNode reference
+ * 2. OverlayController will create a contentWrapperNode around contentNode (or consumes when provided)
+ * 3. contentWrapperNode will be appended to the right renderTarget
+ *
+ * There are subtle differences depending on the following factors:
+ * - whether in global/local placement mode
+ * - whether contentNode projected
+ * - whether an arrow is provided
+ *
+ * This leads to the following possible combinations:
+ * - [l1]. local + no content projection + no arrow
+ * - [l2]. local + content projection + no arrow
+ * - [l3]. local + no content projection + arrow
+ * - [l4]. local + content projection + arrow
+ * - [g1]. global
+ *
+ * #### html structure for a content projected node
+ *
+ *
+ *
+ *
+ *
+ * Structure above depicts [l4]
+ * So in case of [l1] and [l3], the element would be a regular element
+ * In case of [l1] and [l2], there would be no arrow.
+ * Note that a contentWrapperNode should be provided for [l2], [l3] and [l4]
+ * In case of a global overlay ([g1]), it's enough to provide just the contentNode.
+ * In case of a local overlay or a responsive overlay switching from placementMode, one should
+ * always configure as if it was a local overlay.
+ *
+ */
+
export class OverlayController {
/**
* @constructor
@@ -20,14 +80,17 @@ export class OverlayController {
this.__fakeExtendsEventTarget();
this.manager = manager;
this.__sharedConfig = config;
+
+ /** @type {OverlayConfig} */
this._defaultConfig = {
placementMode: null,
contentNode: config.contentNode,
+ contentWrapperNode: config.contentWrapperNode,
invokerNode: config.invokerNode,
backdropNode: config.backdropNode,
referenceNode: null,
elementToFocusAfterHide: config.invokerNode,
- inheritsReferenceWidth: '',
+ inheritsReferenceWidth: 'none',
hasBackdrop: false,
isBlocking: false,
preventsScroll: false,
@@ -69,12 +132,14 @@ export class OverlayController {
};
this.manager.add(this);
+ this._contentId = `overlay-content--${Math.random()
+ .toString(36)
+ .substr(2, 10)}`;
- this._contentNodeWrapper = document.createElement('div');
- this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
-
+ if (this._defaultConfig.contentNode) {
+ this.__isContentNodeProjected = Boolean(this._defaultConfig.contentNode.assignedSlot);
+ }
this.updateConfig(config);
-
this.__hasActiveTrapsKeyboardFocus = false;
this.__hasActiveBackdrop = true;
}
@@ -84,31 +149,40 @@ export class OverlayController {
}
get content() {
- return this._contentNodeWrapper;
+ return this._contentWrapperNode;
}
/**
- * @desc The element ._contentNodeWrapper will be appended to.
- * If viewportConfig is configured, this will be OverlayManager.globalRootNode
- * If popperConfig is configured, this will be a sibling node of invokerNode
+ * @desc 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)
+ * we need to know where we should reappend contentWrapperNode (or contentNode in case it's
+ * projected)
+ * @type {HTMLElement}
*/
get _renderTarget() {
+ /** config [g1] */
if (this.placementMode === 'global') {
return this.manager.globalRootNode;
}
+ /** config [l2] or [l4] */
+ if (this.__isContentNodeProjected) {
+ return this.__originalContentParent.getRootNode().host;
+ }
+ /** config [l1] or [l3] */
return this.__originalContentParent;
}
/**
* @desc The element our local overlay will be positioned relative to.
+ * @type {HTMLElement}
*/
get _referenceNode() {
return this.referenceNode || this.invokerNode;
}
set elevation(value) {
- if (this._contentNodeWrapper) {
- this._contentNodeWrapper.style.zIndex = value;
+ if (this._contentWrapperNode) {
+ this._contentWrapperNode.style.zIndex = value;
}
if (this.backdropNode) {
this.backdropNode.style.zIndex = value;
@@ -116,7 +190,7 @@ export class OverlayController {
}
get elevation() {
- return this._contentNodeWrapper.zIndex;
+ return this._contentWrapperNode.zIndex;
}
/**
@@ -124,16 +198,12 @@ export class OverlayController {
* presentation of the overlay changes depending on screen size.
* Note that this method is the only allowed way to update a configuration of an
* OverlayController instance.
- * @param {OverlayConfig} cfgToAdd
+ * @param { OverlayConfig } cfgToAdd
*/
updateConfig(cfgToAdd) {
// Teardown all previous configs
this._handleFeatures({ phase: 'teardown' });
- if (cfgToAdd.contentNode && cfgToAdd.contentNode.isConnected) {
- // We need to keep track of the original local context.
- this.__originalContentParent = cfgToAdd.contentNode.parentElement;
- }
this.__prevConfig = this.config || {};
this.config = {
@@ -172,18 +242,23 @@ export class OverlayController {
if (!newConfig.contentNode) {
throw new Error('You need to provide a .contentNode');
}
+ if (this.__isContentNodeProjected && !newConfig.contentWrapperNode) {
+ throw new Error('You need to provide a .contentWrapperNode when .contentNode is projected');
+ }
+ // if (newConfig.popperConfig.modifiers.arrow && !newConfig.contentWrapperNode) {
+ // throw new Error('You need to provide a .contentWrapperNode when Popper arrow is enabled');
+ // }
}
async _init({ cfgToAdd }) {
- this.__initContentNodeWrapper();
+ this.__initcontentWrapperNode({ cfgToAdd });
this.__initConnectionTarget();
if (this.handlesAccessibility) {
this.__initAccessibility({ cfgToAdd });
}
if (this.placementMode === 'local') {
- // Now, it is time to lazily load Popper if not done yet
- // Do we really want to add display: inline or is this up to user?
+ // Lazily load Popper if not done yet
if (!this.constructor.popperModule) {
this.constructor.popperModule = preloadPopper();
}
@@ -192,34 +267,56 @@ export class OverlayController {
}
__initConnectionTarget() {
- // Now, add our node to the right place in dom (rendeTarget)
- if (this.contentNode !== this.__prevConfig.contentNode) {
- this._contentNodeWrapper.appendChild(this.contentNode);
+ // 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._renderTarget && this._renderTarget !== this._contentNodeWrapper.parentNode) {
- this._renderTarget.appendChild(this._contentNodeWrapper);
+
+ if (this.__isContentNodeProjected && this.placementMode === 'local') {
+ // We add the contentNode in its slot, so that it will be projected by contentWrapperNode
+ this._renderTarget.appendChild(this.contentNode);
+ } else {
+ const isInsideRenderTarget = this._renderTarget === this._contentWrapperNode.parentNode;
+ if (!isInsideRenderTarget) {
+ // contentWrapperNode becomes the direct (non projected) parent of contentNode
+ this._renderTarget.appendChild(this._contentWrapperNode);
+ }
}
}
/**
- * @desc Cleanup ._contentNodeWrapper. We do this, because creating a fresh wrapper
+ * @desc Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
* can lead to problems with event listeners...
*/
- __initContentNodeWrapper() {
- Array.from(this._contentNodeWrapper.attributes).forEach(attrObj => {
- this._contentNodeWrapper.removeAttribute(attrObj.name);
- });
- this._contentNodeWrapper.style.cssText = null;
- this._contentNodeWrapper.style.display = 'none';
+ __initcontentWrapperNode({ cfgToAdd }) {
+ if (this.config.contentWrapperNode && this.placementMode === 'local') {
+ /** config [l2],[l3],[l4] */
+ this._contentWrapperNode = this.config.contentWrapperNode;
+ } else {
+ /** config [l1],[g1] */
+ this._contentWrapperNode = document.createElement('div');
+ }
- // Make sure that your shadow dom contains this outlet, when we are adding to light dom
- this._contentNodeWrapper.slot = '_overlay-shadow-outlet';
+ this._contentWrapperNode.style.cssText = null;
+ this._contentWrapperNode.style.display = 'none';
if (getComputedStyle(this.contentNode).position === 'absolute') {
// Having a _contWrapperNode and a contentNode with 'position:absolute' results in
// computed height of 0...
this.contentNode.style.position = 'static';
}
+
+ if (this.__isContentNodeProjected && this._contentWrapperNode.isConnected) {
+ // We need to keep track of the original local context.
+ /** config [l2], [l4] */
+ this.__originalContentParent = 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;
+ }
}
/**
@@ -233,7 +330,7 @@ export class OverlayController {
if (phase === 'setup') {
const zIndexNumber = Number(getComputedStyle(this.contentNode).zIndex);
if (zIndexNumber < 1 || Number.isNaN(zIndexNumber)) {
- this._contentNodeWrapper.style.zIndex = 1;
+ this._contentWrapperNode.style.zIndex = 1;
}
}
}
@@ -262,7 +359,7 @@ export class OverlayController {
}
get isShown() {
- return Boolean(this._contentNodeWrapper.style.display !== 'none');
+ return Boolean(this._contentWrapperNode.style.display !== 'none');
}
/**
@@ -282,7 +379,7 @@ export class OverlayController {
const event = new CustomEvent('before-show', { cancelable: true });
this.dispatchEvent(event);
if (!event.defaultPrevented) {
- this._contentNodeWrapper.style.display = this.placementMode === 'local' ? 'inline-block' : '';
+ this._contentWrapperNode.style.display = '';
await this._handleFeatures({ phase: 'show' });
await this._handlePosition({ phase: 'show' });
this.elementToFocusAfterHide = elementToFocusAfterHide;
@@ -294,8 +391,8 @@ export class OverlayController {
if (this.placementMode === 'global') {
const addOrRemove = phase === 'show' ? 'add' : 'remove';
const placementClass = `${GLOBAL_OVERLAYS_CONTAINER_CLASS}--${this.viewportConfig.placement}`;
- this._contentNodeWrapper.classList[addOrRemove](GLOBAL_OVERLAYS_CONTAINER_CLASS);
- this._contentNodeWrapper.classList[addOrRemove](placementClass);
+ this._contentWrapperNode.classList[addOrRemove](GLOBAL_OVERLAYS_CONTAINER_CLASS);
+ this._contentWrapperNode.classList[addOrRemove](placementClass);
this.contentNode.classList[addOrRemove](GLOBAL_OVERLAYS_CLASS);
} else if (this.placementMode === 'local' && phase === 'show') {
/**
@@ -327,7 +424,7 @@ export class OverlayController {
this.dispatchEvent(event);
if (!event.defaultPrevented) {
// await this.transitionHide({ backdropNode: this.backdropNode, conentNode: this.contentNode });
- this._contentNodeWrapper.style.display = 'none';
+ this._contentWrapperNode.style.display = 'none';
this._handleFeatures({ phase: 'hide' });
this.dispatchEvent(new Event('hide'));
this._restoreFocus();
@@ -340,7 +437,7 @@ export class OverlayController {
_restoreFocus() {
// We only are allowed to move focus if we (still) 'own' it.
// Otherwise we assume the 'outside world' has, purposefully, taken over
- // if (this._contentNodeWrapper.activeElement) {
+ // if (this._contentWrapperNode.activeElement) {
if (this.elementToFocusAfterHide) {
this.elementToFocusAfterHide.focus();
}
@@ -430,10 +527,9 @@ export class OverlayController {
this.backdropNode = document.createElement('div');
this.backdropNode.classList.add('local-overlays__backdrop');
}
- this.backdropNode.slot = '_overlay-shadow-outlet';
- this._contentNodeWrapper.parentElement.insertBefore(
+ this._contentWrapperNode.parentNode.insertBefore(
this.backdropNode,
- this._contentNodeWrapper,
+ this._contentWrapperNode,
);
break;
case 'show':
@@ -460,10 +556,9 @@ export class OverlayController {
case 'init':
this.backdropNode = document.createElement('div');
this.backdropNode.classList.add('global-overlays__backdrop');
- this.backdropNode.slot = '_overlay-shadow-outlet';
- this._contentNodeWrapper.parentElement.insertBefore(
+ this._contentWrapperNode.parentElement.insertBefore(
this.backdropNode,
- this._contentNodeWrapper,
+ this._contentWrapperNode,
);
break;
case 'show':
@@ -577,21 +672,21 @@ export class OverlayController {
}
_handleInheritsReferenceWidth() {
- if (!this._referenceNode) {
+ if (!this._referenceNode || this.placementMode === 'global') {
return;
}
const referenceWidth = `${this._referenceNode.clientWidth}px`;
switch (this.inheritsReferenceWidth) {
case 'max':
- this._contentNodeWrapper.style.maxWidth = referenceWidth;
+ this._contentWrapperNode.style.maxWidth = referenceWidth;
break;
case 'full':
- this._contentNodeWrapper.style.width = referenceWidth;
+ this._contentWrapperNode.style.width = referenceWidth;
break;
case 'min':
- this._contentNodeWrapper.style.minWidth = referenceWidth;
- this._contentNodeWrapper.style.width = 'auto';
+ this._contentWrapperNode.style.minWidth = referenceWidth;
+ this._contentWrapperNode.style.width = 'auto';
break;
/* no default */
}
@@ -619,7 +714,7 @@ export class OverlayController {
};
}
- this._contentNodeWrapper[addOrRemoveListener]('click', this.__preventCloseOutsideClick, true);
+ this._contentWrapperNode[addOrRemoveListener]('click', this.__preventCloseOutsideClick, true);
if (this.invokerNode) {
this.invokerNode[addOrRemoveListener]('click', this.__preventCloseOutsideClick, true);
}
@@ -634,9 +729,18 @@ export class OverlayController {
teardown() {
this._handleFeatures({ phase: 'teardown' });
- // IE11 compatibility (does not support `Node.remove()`)
- if (this._contentNodeWrapper && this._contentNodeWrapper.parentElement) {
- this._contentNodeWrapper.parentElement.removeChild(this._contentNodeWrapper);
+
+ // Remove the content node wrapper from the global rootnode
+ this._teardowncontentWrapperNode();
+ }
+
+ _teardowncontentWrapperNode() {
+ if (
+ this.placementMode === 'global' &&
+ this._contentWrapperNode &&
+ this._contentWrapperNode.parentNode
+ ) {
+ this._contentWrapperNode.parentNode.removeChild(this._contentWrapperNode);
}
}
@@ -646,7 +750,7 @@ export class OverlayController {
this._popper = null;
}
const { default: Popper } = await this.constructor.popperModule;
- this._popper = new Popper(this._referenceNode, this._contentNodeWrapper, {
+ this._popper = new Popper(this._referenceNode, this._contentWrapperNode, {
...this.config.popperConfig,
});
}
diff --git a/packages/overlays/src/OverlayMixin.js b/packages/overlays/src/OverlayMixin.js
index 163127b8f..fdce45c36 100644
--- a/packages/overlays/src/OverlayMixin.js
+++ b/packages/overlays/src/OverlayMixin.js
@@ -23,6 +23,10 @@ export const OverlayMixin = dedupeMixin(
super();
this.opened = false;
this.config = {};
+
+ this._overlaySetupComplete = new Promise(resolve => {
+ this.__overlaySetupCompleteResolve = resolve;
+ });
}
get config() {
@@ -50,11 +54,12 @@ export const OverlayMixin = dedupeMixin(
* @returns {OverlayController}
*/
// eslint-disable-next-line
- _defineOverlay({ contentNode, invokerNode, backdropNode }) {
+ _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: {
@@ -160,22 +165,7 @@ export const OverlayMixin = dedupeMixin(
}
get _overlayContentNode() {
- if (this._cachedOverlayContentNode) {
- return this._cachedOverlayContentNode;
- }
-
- // (@jorenbroekema) This should shadow outlet in between the host and the content slot,
- // is a problem.
- // Should simply be Array.from(this.children).find(child => child.slot === 'content')
- // Issue: https://github.com/ing-bank/lion/issues/382
- const shadowOutlet = Array.from(this.children).find(
- child => child.slot === '_overlay-shadow-outlet',
- );
- if (shadowOutlet) {
- this._cachedOverlayContentNode = Array.from(shadowOutlet.children).find(
- child => child.slot === 'content',
- );
- } else {
+ if (!this._cachedOverlayContentNode) {
this._cachedOverlayContentNode = Array.from(this.children).find(
child => child.slot === 'content',
);
@@ -183,8 +173,8 @@ export const OverlayMixin = dedupeMixin(
return this._cachedOverlayContentNode;
}
- get _overlayContentNodeWrapper() {
- return this._overlayContentNode.parentElement;
+ get _overlayContentWrapperNode() {
+ return this.shadowRoot.querySelector('#overlay-content-node-wrapper');
}
_setupOverlayCtrl() {
@@ -199,6 +189,7 @@ export const OverlayMixin = dedupeMixin(
contentNode: this._overlayContentNode,
invokerNode: this._overlayInvokerNode,
backdropNode: this._overlayBackdropNode,
+ contentWrapperNode: this._overlayContentWrapperNode,
});
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
diff --git a/packages/overlays/stories/20-index.stories.mdx b/packages/overlays/stories/20-index.stories.mdx
index 5ccba7414..360ae8142 100644
--- a/packages/overlays/stories/20-index.stories.mdx
+++ b/packages/overlays/stories/20-index.stories.mdx
@@ -123,10 +123,9 @@ or in your Web Component with `OverlayMixin`, make sure you override these metho
- Define configuration
- Handle setting up event listeners of toggling the opened state of your overlay
- Handle the tearing down of those event listeners
-- Define a template which includes
+- Define a template which includes:
- invoker slot for your user to provide the invoker node (the element that invokes the overlay content)
- content slot for your user to provide the content that shows when the overlay is opened
- - _overlay-shadow-outlet, this slot is currently necessary under the hood for acting as a wrapper element for placement purposes, but is not something your end user should be concerned with, unless they are extending your component.
```js
_defineOverlayConfig() {
@@ -157,8 +156,9 @@ _teardownOpenCloseListeners() {
render() {
return html`
-
-
+
+
+
`;
}
```
@@ -296,7 +296,7 @@ Below is another demo where you can toggle between configurations using buttons.
Dropdown
-
+
Hello! You can close this notification here:
@@ -327,7 +327,7 @@ Change config to:
Dropdown
-
+
Hello! You can close this notification here:
@@ -507,8 +507,9 @@ class MyOverlayWC extends OverlayMixin(LitElement) {
render() {
return html`
-
-
+
+
+
`;
}
}
@@ -742,3 +743,38 @@ Another way to add custom backdrop is declaratively add an element with `slot="b
```
+
+
+### Nested Overlays
+
+Overlays can be nested, as the demo below shows.
+It's also possible to compose a nested construction by moving around dom nodes.
+
+
+
+ {html`
+
+
+ `,
+ ),
});
expect(ctrl.contentNode.textContent).to.include('content2');
});
it('respects the initial config provided to new OverlayController(initialConfig)', async () => {
- const contentNode = fixtureSync(html`
my content
`);
+ const contentNode = fixtureSync(html`
+
my content
+ `);
const ctrl = new OverlayController({
// This is the shared config
@@ -972,7 +1047,9 @@ describe('OverlayController', () => {
// Currently not working, enable again when we fix updateConfig
it.skip('allows for updating viewport config placement only, while keeping the content shown', async () => {
- const contentNode = fixtureSync(html`
my content
`);
+ const contentNode = fixtureSync(html`
+
my content
+ `);
const ctrl = new OverlayController({
// This is the shared config
@@ -983,13 +1060,13 @@ describe('OverlayController', () => {
ctrl.show();
expect(
- ctrl._contentNodeWrapper.classList.contains('global-overlays__overlay-container--center'),
+ ctrl._contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
);
expect(ctrl.isShown).to.be.true;
ctrl.updateConfig({ viewportConfig: { placement: 'top-right' } });
expect(
- ctrl._contentNodeWrapper.classList.contains(
+ ctrl._contentWrapperNode.classList.contains(
'global-overlays__overlay-container--top-right',
),
);
@@ -1186,5 +1263,26 @@ describe('OverlayController', () => {
});
}).to.throw('You need to provide a .contentNode');
});
+
+ it('throws if contentNodewrapper is not provided for projected contentNode', async () => {
+ const shadowHost = document.createElement('div');
+ shadowHost.attachShadow({ mode: 'open' });
+ shadowHost.shadowRoot.innerHTML = `
+
+
+
+
+ `;
+ const contentNode = document.createElement('div');
+ contentNode.slot = 'contentNode';
+ shadowHost.appendChild(contentNode);
+
+ expect(() => {
+ new OverlayController({
+ ...withLocalTestConfig(),
+ contentNode,
+ });
+ }).to.throw('You need to provide a .contentWrapperNode when .contentNode is projected');
+ });
});
});
diff --git a/packages/overlays/test/OverlayMixin.test.js b/packages/overlays/test/OverlayMixin.test.js
index 55e675cb2..842b90e72 100644
--- a/packages/overlays/test/OverlayMixin.test.js
+++ b/packages/overlays/test/OverlayMixin.test.js
@@ -1,12 +1,25 @@
import { defineCE, unsafeStatic } from '@open-wc/testing';
-import { LitElement } from '@lion/core';
+import { LitElement, html } from '@lion/core';
import { runOverlayMixinSuite } from '../test-suites/OverlayMixin.suite.js';
import { OverlayMixin } from '../src/OverlayMixin.js';
-const tagString = defineCE(class extends OverlayMixin(LitElement) {});
+const tagString = defineCE(
+ class extends OverlayMixin(LitElement) {
+ render() {
+ return html`
+
+
`),
@@ -71,7 +79,11 @@ describe('Local Positioning', () => {
it('positions to preferred place if placement is set and space is available', async () => {
const ctrl = new OverlayController({
...withLocalTestConfig(),
- contentNode: fixtureSync(html``),
+ contentNode: fixtureSync(
+ html`
+
+ `,
+ ),
invokerNode: fixtureSync(html`
ctrl.show()}>
`),
@@ -92,7 +104,11 @@ describe('Local Positioning', () => {
it('positions to different place if placement is set and no space is available', async () => {
const ctrl = new OverlayController({
...withLocalTestConfig(),
- contentNode: fixtureSync(html`