From fbedad605d7fc36932e2aedfdb4cfdcdd74fc686 Mon Sep 17 00:00:00 2001 From: Benedikt Meurer Date: Wed, 22 Jan 2025 07:20:44 +0100 Subject: [PATCH] Override `ShadowRoot.adoptedStyleSheets` to handle cross-document cases. This also migrate the ``, `` and `` custom elements to use the new CSS approach (instead of the legacy approach), since it works now, even across multiple documents. Bug: 391381439 Change-Id: I48c629c6073579051082dd1ca9360bb7d9cc30e2 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6184803 Reviewed-by: Alex Rudenko Commit-Queue: Alex Rudenko Auto-Submit: Benedikt Meurer --- config/gni/devtools_grd_files.gni | 6 +- front_end/core/dom_extension/DOMExtension.ts | 61 ++++++++++++++++++++ front_end/ui/components/buttons/BUILD.gn | 2 - front_end/ui/components/buttons/Button.ts | 11 +--- front_end/ui/components/icon_button/BUILD.gn | 2 - front_end/ui/components/icon_button/Icon.ts | 15 ++--- front_end/ui/legacy/BUILD.gn | 2 +- front_end/ui/legacy/Toolbar.ts | 8 ++- 8 files changed, 76 insertions(+), 31 deletions(-) diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index e8b8d2e39d..6bf078831d 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -2193,7 +2193,7 @@ grd_files_debug_sources = [ "front_end/ui/components/adorners/Adorner.js", "front_end/ui/components/adorners/adorner.css.js", "front_end/ui/components/buttons/Button.js", - "front_end/ui/components/buttons/button.css.legacy.js", + "front_end/ui/components/buttons/button.css.js", "front_end/ui/components/cards/Card.js", "front_end/ui/components/cards/card.css.js", "front_end/ui/components/chrome_link/ChromeLink.js", @@ -2231,7 +2231,7 @@ grd_files_debug_sources = [ "front_end/ui/components/icon_button/Icon.js", "front_end/ui/components/icon_button/IconButton.js", "front_end/ui/components/icon_button/fileSourceIcon.css.js", - "front_end/ui/components/icon_button/icon.css.legacy.js", + "front_end/ui/components/icon_button/icon.css.js", "front_end/ui/components/icon_button/iconButton.css.js", "front_end/ui/components/input/checkbox.css.js", "front_end/ui/components/input/textInput.css.js", @@ -2502,7 +2502,7 @@ grd_files_debug_sources = [ "front_end/ui/legacy/themeColors.css.legacy.js", "front_end/ui/legacy/theme_support/ThemeSupport.js", "front_end/ui/legacy/tokens.css.legacy.js", - "front_end/ui/legacy/toolbar.css.legacy.js", + "front_end/ui/legacy/toolbar.css.js", "front_end/ui/legacy/treeoutline.css.legacy.js", "front_end/ui/legacy/viewContainers.css.legacy.js", "front_end/ui/lit-html/i18n-template.js", diff --git a/front_end/core/dom_extension/DOMExtension.ts b/front_end/core/dom_extension/DOMExtension.ts index 5060011dba..b9a70629ba 100644 --- a/front_end/core/dom_extension/DOMExtension.ts +++ b/front_end/core/dom_extension/DOMExtension.ts @@ -399,3 +399,64 @@ DOMTokenList.prototype['toggle'] = function(token: string, force: boolean|undefi return originalToggle.call(this, token, Boolean(force)); }; })(); + +// DevTools uses multiple documents when the main window is undocked, and the +// device mode toolbar is enabled. Since `CSSStyleSheet` objects cannot be +// shared across multiple documents, we override the `adoptedStyleSheets` +// accessor here, and clone the `CSSStyleSheet` objects under hood on-demand. +// +// NOTE: Since `adoptedStyleSheets` is an `ObservableArray`, which can be +// mutated in place, we need to override both the setter and the getter, and +// for the latter, we need to return a proxy that intercepts mutations to the +// array-indexed properties. +const originalAdoptedStyleSheets = Object.getOwnPropertyDescriptor(ShadowRoot.prototype, 'adoptedStyleSheets'); +if (originalAdoptedStyleSheets) { + const styleSheetInDocumentCache = new WeakMap>(); + + /** + * Returns a clone of the `styleSheet` within the given `document`. + */ + function styleSheetInDocument(styleSheet: CSSStyleSheet, document: Document): CSSStyleSheet { + let styleSheetCache = styleSheetInDocumentCache.get(styleSheet); + if (styleSheetCache) { + const cachedSheet = styleSheetCache.get(document); + if (cachedSheet) { + return cachedSheet; + } + } else { + styleSheetCache = new WeakMap(); + styleSheetInDocumentCache.set(styleSheet, styleSheetCache); + } + + const clonedStyleSheet = new document.defaultView.CSSStyleSheet(); + for (const {cssText} of styleSheet.cssRules) { + clonedStyleSheet.insertRule(cssText); + } + styleSheetCache.set(document, clonedStyleSheet); + return clonedStyleSheet; + } + + Object.defineProperty(ShadowRoot.prototype, 'adoptedStyleSheets', { + configurable: true, + enumerable: true, + get(this: ShadowRoot): CSSStyleSheet[] { + const target = originalAdoptedStyleSheets.get.call(this); + const {ownerDocument} = this.host; + return new Proxy(target, { + set(target, propertyKey, value, receiver) { + if (propertyKey === String(propertyKey >>> 0) && ownerDocument !== document) { + value = styleSheetInDocument(value, ownerDocument); + } + return Reflect.set(target, propertyKey, value, receiver); + }, + }); + }, + set(this: ShadowRoot, styleSheets: CSSStyleSheet[]) { + const {ownerDocument} = this.host; + if (ownerDocument !== document) { + styleSheets = styleSheets.map(styleSheet => styleSheetInDocument(styleSheet, ownerDocument)); + } + originalAdoptedStyleSheets.set.call(this, styleSheets); + } + }); +} diff --git a/front_end/ui/components/buttons/BUILD.gn b/front_end/ui/components/buttons/BUILD.gn index 96956b8b9b..f54715437d 100644 --- a/front_end/ui/components/buttons/BUILD.gn +++ b/front_end/ui/components/buttons/BUILD.gn @@ -10,8 +10,6 @@ import("../visibility.gni") generate_css("legacy_css_files") { sources = [ "button.css" ] - - legacy = true } devtools_module("button") { diff --git a/front_end/ui/components/buttons/Button.ts b/front_end/ui/components/buttons/Button.ts index edf7980245..cb4c4415ad 100644 --- a/front_end/ui/components/buttons/Button.ts +++ b/front_end/ui/components/buttons/Button.ts @@ -7,7 +7,7 @@ import '../icon_button/icon_button.js'; import * as LitHtml from '../../lit-html/lit-html.js'; import * as VisualLogging from '../../visual_logging/visual_logging.js'; -import buttonStyles from './button.css.legacy.js'; +import buttonStyles from './button.css.js'; const {html, Directives: {ifDefined, ref, classMap}} = LitHtml; @@ -122,14 +122,6 @@ export class Button extends HTMLElement { super(); this.setAttribute('role', 'presentation'); this.addEventListener('click', this.#boundOnClick, true); - - // TODO(crbug.com/359141904): Ideally we would be using - // adopted style sheets for installing css styles, but this - // currently throws an error when sharing the styles across - // multiple documents. This is a workaround. - const styleElement = document.createElement('style'); - styleElement.textContent = buttonStyles.cssContent; - this.#shadow.appendChild(styleElement); } /** @@ -272,6 +264,7 @@ export class Button extends HTMLElement { } connectedCallback(): void { + this.#shadow.adoptedStyleSheets = [buttonStyles]; this.#render(); } diff --git a/front_end/ui/components/icon_button/BUILD.gn b/front_end/ui/components/icon_button/BUILD.gn index 18acf4709a..a96e816c0f 100644 --- a/front_end/ui/components/icon_button/BUILD.gn +++ b/front_end/ui/components/icon_button/BUILD.gn @@ -17,8 +17,6 @@ generate_css("css_files") { generate_css("legacy_css_files") { sources = [ "icon.css" ] - - legacy = true } devtools_module("icon_button") { diff --git a/front_end/ui/components/icon_button/Icon.ts b/front_end/ui/components/icon_button/Icon.ts index 27f070c573..012cb95a43 100644 --- a/front_end/ui/components/icon_button/Icon.ts +++ b/front_end/ui/components/icon_button/Icon.ts @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import iconStyles from './icon.css.legacy.js'; +import iconStyles from './icon.css.js'; /** * @deprecated @@ -75,17 +75,10 @@ export class Icon extends HTMLElement { this.#icon = document.createElement('span'); this.#shadowRoot = this.attachShadow({mode: 'open'}); this.#shadowRoot.appendChild(this.#icon); + } - // TODO(crbug.com/359141904): Ideally we'd have a `connectedCallback()` that would just - // install the CSS via `adoptedStyleSheets`, but that throws when using the - // same `CSSStyleSheet` across two different documents (which happens in the - // case of undocked DevTools windows and using the DeviceMode). So the work- - // around for now is to use legacy CSS injected as a