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('