fix(ui): add adopted stylesheets once; attach correctly to body

This commit is contained in:
Thijs Louisse 2022-12-12 13:05:01 +01:00 committed by Thijs Louisse
parent 9b61aac7a1
commit fafd922251
3 changed files with 103 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
overlays: add adopted stylesheets once; attach correctly to body

View file

@ -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

View file

@ -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 () => {