Override ShadowRoot.adoptedStyleSheets to handle cross-document cases.

This also migrate the `<devtools-icon>`, `<devtools-toolbar>` and
`<devtools-button>` 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 <alexrudenko@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
This commit is contained in:
Benedikt Meurer
2025-01-21 23:54:23 -08:00
committed by Devtools-frontend LUCI CQ
parent a48d7b9b0a
commit fbedad605d
8 changed files with 76 additions and 31 deletions
+3 -3
View File
@@ -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",
@@ -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<CSSStyleSheet, WeakMap<Document, CSSStyleSheet>>();
/**
* 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<Document, CSSStyleSheet>();
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);
}
});
}
-2
View File
@@ -10,8 +10,6 @@ import("../visibility.gni")
generate_css("legacy_css_files") {
sources = [ "button.css" ]
legacy = true
}
devtools_module("button") {
+2 -9
View File
@@ -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();
}
@@ -17,8 +17,6 @@ generate_css("css_files") {
generate_css("legacy_css_files") {
sources = [ "icon.css" ]
legacy = true
}
devtools_module("icon_button") {
+4 -11
View File
@@ -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 <style> tag into the
// ShadowRoot (which has been working well for the legacy UI components for
// a long time).
const styleElement = document.createElement('style');
styleElement.textContent = iconStyles.cssContent;
this.#shadowRoot.appendChild(styleElement);
connectedCallback(): void {
this.#shadowRoot.adoptedStyleSheets = [iconStyles];
}
/**
+1 -1
View File
@@ -12,6 +12,7 @@ generate_css("css_files") {
sources = [
"emptyWidget.css",
"inspectorCommon.css",
"toolbar.css",
]
}
@@ -50,7 +51,6 @@ generate_css("legacy_css_files") {
"textPrompt.css",
"themeColors.css",
"tokens.css",
"toolbar.css",
"treeoutline.css",
"viewContainers.css",
]
+5 -3
View File
@@ -45,9 +45,9 @@ import {GlassPane, PointerEventsBehavior} from './GlassPane.js';
import {bindCheckbox} from './SettingsUI.js';
import type {Suggestion} from './SuggestBox.js';
import {Events as TextPromptEvents, TextPrompt} from './TextPrompt.js';
import toolbarStyles from './toolbar.css.legacy.js';
import toolbarStyles from './toolbar.css.js';
import {Tooltip} from './Tooltip.js';
import {CheckboxLabel, createShadowRootWithCoreStyles, LongClickController} from './UIUtils.js';
import {CheckboxLabel, LongClickController} from './UIUtils.js';
const UIStrings = {
/**
@@ -83,19 +83,21 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
* @prop {boolean} wrappable - The `"wrappable"` attribute is reflected as property.
*/
export class Toolbar extends HTMLElement {
#shadowRoot = this.attachShadow({mode: 'open'});
private items: ToolbarItem[] = [];
enabled: boolean = true;
private compactLayout = false;
constructor() {
super();
createShadowRootWithCoreStyles(this, {cssFile: toolbarStyles}).createChild('slot');
this.#shadowRoot.createChild('slot');
}
connectedCallback(): void {
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'toolbar');
}
this.#shadowRoot.adoptedStyleSheets = [toolbarStyles];
}
/**