Merge pull request #911 from ing-bank/feat/syncOverlays

feat(overlays): make OverlayController constructor tasks sync
This commit is contained in:
Thijs Louisse 2020-09-03 16:57:16 +02:00 committed by GitHub
commit c7b0709e98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 189 additions and 139 deletions

View file

@ -0,0 +1,15 @@
---
'@lion/overlays': minor
'@lion/select-rich': minor
'@lion/dialog': minor
'@lion/input-datepicker': minor
'@lion/tooltip': minor
'@lion/form-integrations': minor
---
- Make the OverlayController constructor phase synchronous.
- Trigger a setup of the OverlayController on every connectedCallback
- Execute a new OverlayController after (shadowDom) rendering of the element is done
- Teardown the OverlayController on every disconnectedCallback
- This means moving a dialog triggers teardown in the old location and setup in the new location
- Restore the original light dom (if needed) in the teardown phase of the OverlayController

View file

@ -9,7 +9,7 @@
"build:docs": "wca analyze \"packages/tabs/**/*.js\"",
"build:types": "tsc -p tsconfig.build.types.json",
"bundlesize": "rollup -c bundlesize/rollup.config.js && bundlesize",
"debug": "web-test-runner \"packages/dialog/test/**/*.test.js\" --watch",
"debug": "web-test-runner \"packages/input-datepicker/test/**/*.test.js\" --watch",
"dev-server": "es-dev-server",
"format": "npm run format:eslint && npm run format:prettier",
"format:eslint": "eslint --ext .js,.html . --fix",
@ -48,7 +48,7 @@
"@storybook/addon-a11y": "~5.0.0",
"@types/chai-dom": "^0.0.8",
"@web/dev-server-legacy": "^0.1.1",
"@web/test-runner": "^0.7.3",
"@web/test-runner": "^0.7.13",
"@web/test-runner-browserstack": "^0.1.1",
"@web/test-runner-playwright": "^0.5.1",
"@web/test-runner-puppeteer": "^0.6.1",

View file

@ -721,7 +721,6 @@ export function runValidateMixinSuite(customConfig) {
.modelValue=${''}
>${lightDom}</${tag}>
`));
console.log(el._inputNode);
expect(el._inputNode?.getAttribute('aria-required')).to.equal('true');
el.validators = [];
expect(el._inputNode?.getAttribute('aria-required')).to.be.null;

View file

@ -0,0 +1,40 @@
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
# Forms in a dialog
```js script
import { html } from 'lit-html';
import '@lion/dialog/lion-dialog.js';
import '@lion/select-rich/lion-select-rich.js';
import '@lion/select-rich/lion-options.js';
import '@lion/select-rich/lion-option.js';
export default {
title: 'Forms/System/Dialog integrations',
};
```
Opening a Rich Select inside a dialog
```js story
export const main = () => html`
<lion-dialog>
<button slot="invoker">Open Dialog</button>
<div slot="content">
<lion-select-rich name="favoriteColor" label="Favorite color">
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</lion-dialog>
`;
```

View file

@ -207,7 +207,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
}
/**
* Defining this overlay as a templates lets OverlayInteraceMixin
* Defining this overlay as a templates from OverlayMixin
* this is our source to give as .contentNode to OverlayController.
* Important: do not change the name of this method.
*/
@ -283,7 +283,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
}
async __openCalendarOverlay() {
this._overlayCtrl.show();
await this._overlayCtrl.show();
await Promise.all([
this._overlayCtrl.contentNode.updateComplete,
this._calendarNode.updateComplete,

View file

@ -175,6 +175,7 @@ describe('<lion-input-datepicker>', () => {
const el = await fixture(html`<lion-input-datepicker></lion-input-datepicker>`);
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
await aTimeout();
expect(isSameDate(elObj.calendarEl.focusedDate, elObj.calendarEl.centralDate)).to.be.true;
});

View file

@ -268,8 +268,8 @@ export class OverlayController {
// }
}
async _init({ cfgToAdd }) {
this.__initcontentWrapperNode({ cfgToAdd });
_init({ cfgToAdd }) {
this.__initContentWrapperNode({ cfgToAdd });
this.__initConnectionTarget();
if (this.placementMode === 'local') {
@ -310,7 +310,7 @@ export class OverlayController {
* @desc Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
* can lead to problems with event listeners...
*/
__initcontentWrapperNode({ cfgToAdd }) {
__initContentWrapperNode({ cfgToAdd }) {
if (this.config.contentWrapperNode && this.placementMode === 'local') {
/** config [l2],[l3],[l4] */
this._contentWrapperNode = this.config.contentWrapperNode;
@ -568,7 +568,7 @@ export class OverlayController {
* @param {object} config
* @param {'init'|'show'|'hide'|'teardown'} config.phase
*/
async _handleFeatures({ phase }) {
_handleFeatures({ phase }) {
this._handleZIndex({ phase });
if (this.preventsScroll) {
@ -856,11 +856,15 @@ export class OverlayController {
teardown() {
this._handleFeatures({ phase: 'teardown' });
if (this.placementMode === 'global' && this.__isContentNodeProjected) {
this.__originalContentParent.appendChild(this.contentNode);
}
// Remove the content node wrapper from the global rootnode
this._teardowncontentWrapperNode();
this._teardownContentWrapperNode();
}
_teardowncontentWrapperNode() {
_teardownContentWrapperNode() {
if (
this.placementMode === 'global' &&
this._contentWrapperNode &&

View file

@ -22,11 +22,8 @@ export const OverlayMixin = dedupeMixin(
constructor() {
super();
this.opened = false;
this.__needsSetup = true;
this.config = {};
this._overlaySetupComplete = new Promise(resolve => {
this.__overlaySetupCompleteResolve = resolve;
});
}
get config() {
@ -134,52 +131,24 @@ export const OverlayMixin = dedupeMixin(
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
// Wait for DOM to be ready before setting up the overlay, else extensions like rich select breaks
super.connectedCallback();
// we do a setup after every connectedCallback as firstUpdated will only be called once
this.__needsSetup = true;
this.updateComplete.then(() => {
if (!this.__isOverlaySetup) {
if (this.__needsSetup) {
this._setupOverlayCtrl();
}
this.__needsSetup = false;
});
// When dom nodes are being moved around (meaning connected/disconnected are being fired
// repeatedly), we need to delay the teardown until we find a 'permanent disconnect'
if (this.__rejectOverlayDisconnectComplete) {
// makes sure _overlayDisconnectComplete never resolves: we don't want a teardown
this.__rejectOverlayDisconnectComplete();
}
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
if (!this._overlayCtrl) {
return;
if (this._overlayCtrl) {
this._teardownOverlayCtrl();
}
this._overlayDisconnectComplete = new Promise((resolve, reject) => {
this.__resolveOverlayDisconnectComplete = resolve;
this.__rejectOverlayDisconnectComplete = reject;
});
setTimeout(() => {
// we start the teardown below
this.__resolveOverlayDisconnectComplete();
});
// We need to prevent that we create a setup/teardown cycle during startup, where it
// is common that the overlay system moves around nodes. Therefore, we make the
// teardown async, so that it only happens when we are permanently disconnecting from dom
this._overlayDisconnectComplete
.then(() => {
this._teardownOverlayCtrl();
})
.catch(() => {});
}
get _overlayInvokerNode() {
@ -213,15 +182,12 @@ export const OverlayMixin = dedupeMixin(
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
this._setupOpenCloseListeners();
this.__overlaySetupCompleteResolve();
this.__isOverlaySetup = true;
}
_teardownOverlayCtrl() {
this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController();
this._overlayCtrl.teardown();
this.__isOverlaySetup = false;
}
/**

View file

@ -195,10 +195,10 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
it('supports nested overlays', async () => {
const el = await fixture(html`
<${tag}>
<${tag} id="main-dialog">
<div slot="content" id="mainContent">
open nested overlay:
<${tag}>
<${tag} id="sub-dialog">
<div slot="content" id="nestedContent">
Nested content
</div>
@ -222,6 +222,23 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
expect(nestedOverlayEl._overlayCtrl.contentNode).to.be.displayed;
});
it('[global] allows for moving of the element', async () => {
const el = await fixture(html`
<${tag}>
<div slot="content" id="nestedContent">content of the nested overlay</div>
<button slot="invoker">invoker nested</button>
</${tag}>
`);
if (el._overlayCtrl.placementMode === 'global') {
expect(getGlobalOverlayNodes().length).to.equal(1);
const moveTarget = await fixture('<div id="target"></div>');
moveTarget.appendChild(el);
await el.updateComplete;
expect(getGlobalOverlayNodes().length).to.equal(1);
}
});
it('reconstructs the overlay when disconnected and reconnected to DOM (support for nested overlay nodes)', async () => {
const nestedEl = await fixture(html`
<${tag} id="nest">
@ -241,15 +258,12 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
`);
if (el._overlayCtrl.placementMode === 'global') {
// Specifically checking the output in global root node, because the _contentOverlayNode still references
// the node that was removed in the teardown but hasn't been garbage collected due to reference to it still existing..
// Find the outlets that are not backdrop outlets
const overlayContainerNodes = getGlobalOverlayNodes();
expect(overlayContainerNodes.length).to.equal(2);
const lastContentNodeInContainer = overlayContainerNodes[0];
const lastContentNodeInContainer = overlayContainerNodes[1];
// Check that the last container is the nested one with the intended content
expect(lastContentNodeInContainer.firstElementChild.innerText).to.equal(
expect(lastContentNodeInContainer.firstElementChild.firstChild.textContent).to.equal(
'content of the nested overlay',
);
expect(lastContentNodeInContainer.firstElementChild.slot).to.equal('content');
@ -259,43 +273,5 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
expect(contentNode.innerText).to.equal('content of the nested overlay');
}
});
it("doesn't tear down controller when dom nodes are being moved around", async () => {
const nestedEl = await fixture(html`
<${tag} id="nest">
<div slot="content" id="nestedContent">content of the nested overlay</div>
<button slot="invoker">invoker nested</button>
</${tag}>
`);
const setupOverlayCtrlSpy = sinon.spy(nestedEl, '_setupOverlayCtrl');
const teardownOverlayCtrlSpy = sinon.spy(nestedEl, '_teardownOverlayCtrl');
const el = await fixture(html`
<${tag} id="main">
<div slot="content" id="mainContent">
open nested overlay:
${nestedEl}
</div>
<button slot="invoker">invoker button</button>
</${tag}>
`);
// Even though many connected/disconnected calls take place,
// we detect we are in the middle of a 'move'
expect(teardownOverlayCtrlSpy).to.not.have.been.called;
expect(setupOverlayCtrlSpy).to.not.have.been.called;
// Now move nestedEl to an offline node
const offlineNode = document.createElement('div');
offlineNode.appendChild(nestedEl);
await aTimeout();
// And we detect this time the disconnect was 'permanent'
expect(teardownOverlayCtrlSpy.callCount).to.equal(1);
el._overlayContentNode.appendChild(nestedEl);
await aTimeout();
expect(setupOverlayCtrlSpy.callCount).to.equal(1);
});
});
}

View file

@ -188,6 +188,42 @@ describe('OverlayController', () => {
ctrl.teardown();
expect(ctrl.manager.globalRootNode.children.length).to.equal(0);
});
it('[global] restores contentNode if it was/is a projected node', async () => {
const shadowHost = document.createElement('div');
shadowHost.id = 'shadowHost';
shadowHost.attachShadow({ mode: 'open' });
shadowHost.shadowRoot.innerHTML = `
<div id="contentWrapperNode">
<slot name="contentNode"></slot>
<my-arrow></my-arrow>
</div>
`;
const contentNode = document.createElement('div');
contentNode.slot = 'contentNode';
shadowHost.appendChild(contentNode);
const wrapper = await fixture('<div id="wrapper"></div>');
// Ensure the contentNode is connected to DOM
wrapper.appendChild(shadowHost);
// has one child = <div slot="contentNode"></div>
expect(shadowHost.children.length).to.equal(1);
const ctrl = new OverlayController({
...withLocalTestConfig(),
placementMode: 'global',
contentNode,
contentWrapperNode: shadowHost,
});
// has no children as content gets moved to the body
expect(shadowHost.children.length).to.equal(0);
ctrl.teardown();
// restores original light dom in teardown
expect(shadowHost.children.length).to.equal(1);
});
});
describe('Node Configuration', () => {
@ -1372,7 +1408,7 @@ describe('OverlayController', () => {
}).to.throw('[OverlayController] You need to provide a .contentNode');
});
it('throws if contentNodewrapper is not provided for projected contentNode', async () => {
it('throws if contentNodeWrapper is not provided for projected contentNode', async () => {
const shadowHost = document.createElement('div');
shadowHost.attachShadow({ mode: 'open' });
shadowHost.shadowRoot.innerHTML = `

View file

@ -195,10 +195,11 @@ export class LionSelectRich extends ScopedElementsMixin(
}
connectedCallback() {
// need to do this before anything else
this._listboxNode.registrationTarget = this;
if (super.connectedCallback) {
super.connectedCallback();
}
this._listboxNode.registrationTarget = this;
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
this.__setupInvokerNode();
this.__setupListboxNode();
@ -213,10 +214,6 @@ export class LionSelectRich extends ScopedElementsMixin(
this.registrationComplete.then(() => {
this.__initInteractionStates();
});
this._overlaySetupComplete.then(() => {
this.__setupOverlay();
});
}
disconnectedCallback() {
@ -226,6 +223,10 @@ export class LionSelectRich extends ScopedElementsMixin(
if (this._labelNode) {
this._labelNode.removeEventListener('click', this.__toggleChecked);
}
this._scrollTargetNode.removeEventListener('keydown', this.__overlayOnHide);
this.__teardownInvokerNode();
this.__teardownListboxNode();
this.__teardownEventListeners();
}
requestUpdateInternal(name, oldValue) {
@ -658,7 +659,8 @@ export class LionSelectRich extends ScopedElementsMixin(
}
}
__setupOverlay() {
_setupOverlayCtrl() {
super._setupOverlayCtrl();
this._initialInheritsReferenceWidth = this._overlayCtrl.inheritsReferenceWidth;
this.__overlayBeforeShow = () => {
if (this.hasNoDefaultSelected) {
@ -688,10 +690,6 @@ export class LionSelectRich extends ScopedElementsMixin(
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
this._overlayCtrl.removeEventListener('before-show', this.__overlayBeforeShow);
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
this._scrollTargetNode.removeEventListener('keydown', this.__overlayOnHide);
this.__teardownInvokerNode();
this.__teardownListboxNode();
this.__teardownEventListeners();
}
__preventScrollingWithArrowKeys(ev) {

View file

@ -90,12 +90,6 @@ export class LionTooltip extends OverlayMixin(LitElement) {
this.__setupRepositionCompletePromise();
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
}
render() {
return html`
<slot name="invoker"></slot>

View file

@ -134,7 +134,6 @@ describe('lion-tooltip', () => {
el.opened = true;
await el.repositionComplete;
// Pretty sure we use flex for this now so that's why it fails
/* expect(getComputedStyle(el.__arrowElement).getPropertyValue('top')).to.equal(
'11px',

View file

@ -1,3 +1,4 @@
/* eslint-disable no-console */
/* eslint-disable import/no-extraneous-dependencies */
const { chromium } = require('playwright');
const looksSame = require('looks-same');

View file

@ -2513,10 +2513,10 @@
dependencies:
semver "^7.3.2"
"@web/dev-server-core@^0.2.2", "@web/dev-server-core@^0.2.6":
version "0.2.6"
resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.2.6.tgz#522e524056e184d691138a76f0afa605b639160b"
integrity sha512-C04oUS5LijC5rLurH/+5uRkgRgD9EVAY9tHi+TZv/57a3QvvPME6ipKbA6MFvkLdeiSdYLKNTY/shjukGfNrcg==
"@web/dev-server-core@^0.2.2":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.2.5.tgz#f05dd29c62f303b20589e439af6a838a831b060b"
integrity sha512-t1R1g3/CLwgVjag075dtyCqoX9KRY4gLIXn/12h05Y+wSzuEjFkezOxhZCyDt0qUzDd2eZ60mOuyq+r5SmV7OQ==
dependencies:
chokidar "^3.4.0"
clone "^2.1.2"
@ -2532,7 +2532,28 @@
parse5 "^6.0.0"
picomatch "^2.2.2"
"@web/dev-server-legacy@^0.1.1", "@web/dev-server-legacy@^0.1.2":
"@web/dev-server-legacy@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@web/dev-server-legacy/-/dev-server-legacy-0.1.1.tgz#1f719610710aaf5608b952defa901c8590173201"
integrity sha512-mf/p35fOtT05PTdFQsJy8B69lhXn7JfOaGJTg0bijm1QH7NTkT50ECAL3l76B9bk5ExmYWRSIqLKhq3iFTJOCA==
dependencies:
"@babel/core" "^7.10.5"
"@babel/plugin-proposal-dynamic-import" "^7.10.4"
"@babel/plugin-syntax-class-properties" "^7.10.4"
"@babel/plugin-syntax-import-meta" "^7.10.4"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
"@babel/plugin-transform-modules-systemjs" "^7.10.5"
"@babel/plugin-transform-template-literals" "^7.10.5"
"@babel/preset-env" "^7.10.4"
"@web/dev-server-core" "^0.2.1"
browserslist "^4.13.0"
browserslist-useragent "^3.0.3"
caniuse-api "^3.0.0"
parse5 "^6.0.0"
polyfills-loader "^1.6.1"
valid-url "^1.0.9"
"@web/dev-server-legacy@^0.1.2":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@web/dev-server-legacy/-/dev-server-legacy-0.1.3.tgz#3b508062b14b502a1b8621802875ba01b234d690"
integrity sha512-PbsDnRKfiCtN5hSrxVpT1EOZofpEl009oF72m30YOu/m+cIef6rAPyk+MgKNXsiq3iob+u/zYqTKj0F5/jVj1A==
@ -2553,14 +2574,14 @@
polyfills-loader "^1.6.1"
valid-url "^1.0.9"
"@web/dev-server-rollup@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.2.4.tgz#45bb4595eee9af1b3f1b23ebd623ee7bc53922b2"
integrity sha512-NpW5BRkpzHZXtH73bH4JI/TqIuv4UHxo0LWjqN4V44xweC1mnTL6mzjYDy/HxfscrYs6pp1s2d+hJ2Lb5uldFQ==
"@web/dev-server-rollup@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.2.3.tgz#e95cc3090cb624084c90f5a7f31bb3fc85fd9b53"
integrity sha512-rR7jM4EW1fsTYb2/RksCNPWlDteJXdnoGq7OGAV6+qDk0JvO4GnsIRLcFcKuCEcjgK9oP2ZgqLdv9qMxkzc4ng==
dependencies:
"@web/dev-server-core" "^0.2.6"
"@web/dev-server-core" "^0.2.2"
"@web/test-runner-chrome" "^0.6.4"
"@web/test-runner-core" "^0.7.5"
"@web/test-runner-core" "^0.7.4"
chalk "^4.1.0"
parse5 "^6.0.1"
rollup "^2.20.0"
@ -2624,13 +2645,13 @@
dependencies:
"@web/test-runner-core" "^0.7.4"
"@web/test-runner-core@^0.7.4", "@web/test-runner-core@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.7.5.tgz#8050d97a4e275be122a2dc46334f02c05b52b358"
integrity sha512-5V9gEYL8a6nxurXKFR/RigRQdYs67jO7Mo3Uj/XbGO54guZktOLCsCtkPCSsYGuNkerVUpyOAKX76ftuOTwblQ==
"@web/test-runner-core@^0.7.4":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.7.4.tgz#ee8f031c893b32009ce29002f0de865e2aa718da"
integrity sha512-DS/BxZTjG64HGObybmSus7GRuWoPaNnPt44pTXSWpIxMvRB3nv/Q2I+wE0Nx66PhC/uM2L74o+CIT+g7qRWsdg==
dependencies:
"@babel/code-frame" "^7.10.4"
"@web/dev-server-core" "^0.2.6"
"@web/dev-server-core" "^0.2.2"
co-body "^6.0.0"
debounce "^1.2.0"
deepmerge "^4.2.2"
@ -2684,17 +2705,17 @@
"@web/test-runner-core" "^0.7.4"
selenium-webdriver "^4.0.0-alpha.7"
"@web/test-runner@^0.7.3":
version "0.7.14"
resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.7.14.tgz#ac5840318d0f725a767b895fabeed83ae1e3fcbb"
integrity sha512-tbmEb/1H1KS1vgY6hRMhJgBtynRw+YRufptFkzwxsdAnMM6iHm6lCAGeFOBb96LawDBC+8IWauYtr/IHYLkhhg==
"@web/test-runner@^0.7.13":
version "0.7.13"
resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.7.13.tgz#8bfb9cd249e1bbd5fb5f0c2716689e137922f2ae"
integrity sha512-DOhWTQqKSxUVVq3e409sjCCmZEJo7k1kMv3DqRW8668bsD0KG+YR5SEIWIIg7BuUHDS40cnjWOmnLo4ttRJj7g==
dependencies:
"@rollup/plugin-node-resolve" "^8.1.0"
"@web/dev-server-rollup" "^0.2.4"
"@web/dev-server-rollup" "^0.2.3"
"@web/test-runner-chrome" "^0.6.4"
"@web/test-runner-cli" "^0.5.6"
"@web/test-runner-commands" "^0.1.3"
"@web/test-runner-core" "^0.7.5"
"@web/test-runner-core" "^0.7.4"
"@web/test-runner-mocha" "^0.3.3"
command-line-args "^5.1.1"
deepmerge "^4.2.2"
@ -7430,7 +7451,7 @@ lit-element@^2.2.1, lit-element@^2.3.1, lit-element@~2.4.0:
dependencies:
lit-html "^1.1.1"
lit-html@^1.0.0, lit-html@^1.1.1, lit-html@^1.2.1:
lit-html@^1.0.0, lit-html@^1.1.1, lit-html@^1.2.1, lit-html@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.3.0.tgz#c80f3cc5793a6dea6c07172be90a70ab20e56034"
integrity sha512-0Q1bwmaFH9O14vycPHw8C/IeHMk/uSDldVLIefu/kfbTBGIc44KGH6A8p1bDfxUfHdc8q6Ct7kQklWoHgr4t1Q==