fix(ui): add adopted stylesheets once; attach correctly to body
This commit is contained in:
parent
9b61aac7a1
commit
fafd922251
3 changed files with 103 additions and 9 deletions
5
.changeset/tall-spiders-tie.md
Normal file
5
.changeset/tall-spiders-tie.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
overlays: add adopted stylesheets once; attach correctly to body
|
||||||
|
|
@ -14,6 +14,22 @@ import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
|
||||||
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
|
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const rootNodeStylesMap = new WeakSet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns element that adopts stylesheet
|
||||||
|
* @param {Element} shadowOrBodyEl
|
||||||
|
* @returns {ShadowRoot}
|
||||||
|
*/
|
||||||
|
function getRootNodeOrBodyElThatAdoptsStylesheet(shadowOrBodyEl) {
|
||||||
|
const rootNode = /** @type {* & DocumentOrShadowRoot} */ (shadowOrBodyEl.getRootNode());
|
||||||
|
if (rootNode === document) {
|
||||||
|
// @ts-ignore
|
||||||
|
return document.body;
|
||||||
|
}
|
||||||
|
return rootNode;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From:
|
* From:
|
||||||
* - wrappingDialogNodeL1: `<dialog role="none"/>`
|
* - wrappingDialogNodeL1: `<dialog role="none"/>`
|
||||||
|
|
@ -519,11 +535,19 @@ export class OverlayController extends EventTargetShim {
|
||||||
OverlayController.popperModule = preloadPopper();
|
OverlayController.popperModule = preloadPopper();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
|
this.__initOverlayStyles();
|
||||||
adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
|
|
||||||
this._handleFeatures({ phase: 'init' });
|
this._handleFeatures({ phase: 'init' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__initOverlayStyles() {
|
||||||
|
const rootNode = getRootNodeOrBodyElThatAdoptsStylesheet(this.contentWrapperNode);
|
||||||
|
if (!rootNodeStylesMap.has(rootNode)) {
|
||||||
|
// TODO: ideally we should also support a teardown
|
||||||
|
adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
|
||||||
|
rootNodeStylesMap.add(rootNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Here we arrange our content node via:
|
* Here we arrange our content node via:
|
||||||
* 1. HTMLDialogElement: the content will always be painted to the browser's top layer
|
* 1. HTMLDialogElement: the content will always be painted to the browser's top layer
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { OverlayController, overlays } from '@lion/ui/overlays.js';
|
import { OverlayController, overlays } from '@lion/ui/overlays.js';
|
||||||
import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
|
import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
|
||||||
|
import { overlayShadowDomStyle } from '../src/overlayShadowDomStyle.js';
|
||||||
import { keyCodes } from '../src/utils/key-codes.js';
|
import { keyCodes } from '../src/utils/key-codes.js';
|
||||||
import { simulateTab } from '../src/utils/simulate-tab.js';
|
import { simulateTab } from '../src/utils/simulate-tab.js';
|
||||||
|
|
||||||
|
|
@ -23,6 +23,22 @@ import { simulateTab } from '../src/utils/simulate-tab.js';
|
||||||
const wrappingDialogNodeStyle = 'display: none; z-index: 9999;';
|
const wrappingDialogNodeStyle = 'display: none; z-index: 9999;';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns element that adopts stylesheet
|
||||||
|
* @param {Element} shadowOrBodyEl
|
||||||
|
* @returns {ShadowRoot}
|
||||||
|
*/
|
||||||
|
function getRootNodeOrBodyElThatAdoptsStylesheet(shadowOrBodyEl) {
|
||||||
|
const rootNode = /** @type {* & DocumentOrShadowRoot} */ (shadowOrBodyEl.getRootNode());
|
||||||
|
if (rootNode === document) {
|
||||||
|
// @ts-ignore
|
||||||
|
return document.body;
|
||||||
|
}
|
||||||
|
return rootNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure that all browsers serialize html in a similar way
|
||||||
|
* (Firefox tends to output empty style attrs)
|
||||||
* @param {HTMLElement} node
|
* @param {HTMLElement} node
|
||||||
*/
|
*/
|
||||||
function normalizeOverlayContentWapper(node) {
|
function normalizeOverlayContentWapper(node) {
|
||||||
|
|
@ -80,6 +96,55 @@ describe('OverlayController', () => {
|
||||||
expect(ctrl.contentNode.parentElement).to.equal(ctrl.contentWrapperNode);
|
expect(ctrl.contentNode.parentElement).to.equal(ctrl.contentWrapperNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Stylesheets', () => {
|
||||||
|
it('adds a stylesheet to the body when contentWrapper is located there', async () => {
|
||||||
|
new OverlayController({
|
||||||
|
...withLocalTestConfig(),
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
if (document.body.adoptedStyleSheets) {
|
||||||
|
// @ts-ignore
|
||||||
|
expect(document.body.adoptedStyleSheets).to.include(overlayShadowDomStyle.styleSheet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a stylesheet to the shadowRoot when contentWrappeNode is located there', async () => {
|
||||||
|
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>contentful</div>'));
|
||||||
|
const shadowHost = document.createElement('div');
|
||||||
|
shadowHost.attachShadow({ mode: 'open' });
|
||||||
|
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `<slot></slot>`;
|
||||||
|
shadowHost.appendChild(contentNode);
|
||||||
|
document.body.appendChild(shadowHost);
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withLocalTestConfig(),
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootNodeOrBody = getRootNodeOrBodyElThatAdoptsStylesheet(ctrl.contentWrapperNode);
|
||||||
|
expect(rootNodeOrBody).to.not.equal(document.body);
|
||||||
|
|
||||||
|
if (rootNodeOrBody.adoptedStyleSheets) {
|
||||||
|
expect(rootNodeOrBody.adoptedStyleSheets).to.include(overlayShadowDomStyle.styleSheet);
|
||||||
|
}
|
||||||
|
document.body.removeChild(shadowHost);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not add same stylesheet twice', async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (!document.body.adoptedStyleSheets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new OverlayController({ ...withLocalTestConfig() });
|
||||||
|
// @ts-ignore
|
||||||
|
const amountOfStylesheetsAfterOneInit = document.body.adoptedStyleSheets.length;
|
||||||
|
|
||||||
|
new OverlayController({ ...withLocalTestConfig() });
|
||||||
|
// @ts-ignore
|
||||||
|
expect(document.body.adoptedStyleSheets.length).to.equal(amountOfStylesheetsAfterOneInit);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Z-index on local overlays', () => {
|
describe('Z-index on local overlays', () => {
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
let contentNode;
|
let contentNode;
|
||||||
|
|
@ -895,11 +960,11 @@ describe('OverlayController', () => {
|
||||||
await fixture('<div><textarea></textarea></div>')
|
await fixture('<div><textarea></textarea></div>')
|
||||||
);
|
);
|
||||||
|
|
||||||
const shadowEl = document.createElement('div');
|
const shadowHost = document.createElement('div');
|
||||||
shadowEl.attachShadow({ mode: 'open' });
|
shadowHost.attachShadow({ mode: 'open' });
|
||||||
/** @type {ShadowRoot} */ (shadowEl.shadowRoot).innerHTML = `<slot></slot>`;
|
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `<slot></slot>`;
|
||||||
shadowEl.appendChild(contentNode);
|
shadowHost.appendChild(contentNode);
|
||||||
document.body.appendChild(shadowEl);
|
document.body.appendChild(shadowHost);
|
||||||
|
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withGlobalTestConfig(),
|
...withGlobalTestConfig(),
|
||||||
|
|
@ -915,7 +980,7 @@ describe('OverlayController', () => {
|
||||||
await ctrl.hide();
|
await ctrl.hide();
|
||||||
expect(document.activeElement).to.equal(input);
|
expect(document.activeElement).to.equal(input);
|
||||||
|
|
||||||
document.body.removeChild(shadowEl);
|
document.body.removeChild(shadowHost);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`only sets focus when outside world didn't take over already`, async () => {
|
it(`only sets focus when outside world didn't take over already`, async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue