diff --git a/.changeset/tall-spiders-tie.md b/.changeset/tall-spiders-tie.md new file mode 100644 index 000000000..481a32ece --- /dev/null +++ b/.changeset/tall-spiders-tie.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +overlays: add adopted stylesheets once; attach correctly to body diff --git a/packages/ui/components/overlays/src/OverlayController.js b/packages/ui/components/overlays/src/OverlayController.js index c55c49cb3..72228ab85 100644 --- a/packages/ui/components/overlays/src/OverlayController.js +++ b/packages/ui/components/overlays/src/OverlayController.js @@ -14,6 +14,22 @@ import { overlayShadowDomStyle } from './overlayShadowDomStyle.js'; * @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: * - wrappingDialogNodeL1: `` @@ -519,11 +535,19 @@ export class OverlayController extends EventTargetShim { OverlayController.popperModule = preloadPopper(); } } - const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode()); - adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]); + this.__initOverlayStyles(); 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: * 1. HTMLDialogElement: the content will always be painted to the browser's top layer diff --git a/packages/ui/components/overlays/test/OverlayController.test.js b/packages/ui/components/overlays/test/OverlayController.test.js index 2307bb155..2cdc8e8c7 100644 --- a/packages/ui/components/overlays/test/OverlayController.test.js +++ b/packages/ui/components/overlays/test/OverlayController.test.js @@ -11,7 +11,7 @@ import { import sinon from 'sinon'; import { OverlayController, overlays } from '@lion/ui/overlays.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 { 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;'; /** + * 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 */ function normalizeOverlayContentWapper(node) { @@ -80,6 +96,55 @@ describe('OverlayController', () => { 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('
contentful
')); + const shadowHost = document.createElement('div'); + shadowHost.attachShadow({ mode: 'open' }); + /** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = ``; + 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', () => { /** @type {HTMLElement} */ let contentNode; @@ -895,11 +960,11 @@ describe('OverlayController', () => { await fixture('
') ); - const shadowEl = document.createElement('div'); - shadowEl.attachShadow({ mode: 'open' }); - /** @type {ShadowRoot} */ (shadowEl.shadowRoot).innerHTML = ``; - shadowEl.appendChild(contentNode); - document.body.appendChild(shadowEl); + const shadowHost = document.createElement('div'); + shadowHost.attachShadow({ mode: 'open' }); + /** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = ``; + shadowHost.appendChild(contentNode); + document.body.appendChild(shadowHost); const ctrl = new OverlayController({ ...withGlobalTestConfig(), @@ -915,7 +980,7 @@ describe('OverlayController', () => { await ctrl.hide(); 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 () => {