Skip to content

Commit fbedad6

Browse files
bmeurerDevtools-frontend LUCI CQ
authored andcommitted
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 <[email protected]> Commit-Queue: Alex Rudenko <[email protected]> Auto-Submit: Benedikt Meurer <[email protected]>
1 parent a48d7b9 commit fbedad6

File tree

8 files changed

+76
-31
lines changed

8 files changed

+76
-31
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,7 +2193,7 @@ grd_files_debug_sources = [
21932193
"front_end/ui/components/adorners/Adorner.js",
21942194
"front_end/ui/components/adorners/adorner.css.js",
21952195
"front_end/ui/components/buttons/Button.js",
2196-
"front_end/ui/components/buttons/button.css.legacy.js",
2196+
"front_end/ui/components/buttons/button.css.js",
21972197
"front_end/ui/components/cards/Card.js",
21982198
"front_end/ui/components/cards/card.css.js",
21992199
"front_end/ui/components/chrome_link/ChromeLink.js",
@@ -2231,7 +2231,7 @@ grd_files_debug_sources = [
22312231
"front_end/ui/components/icon_button/Icon.js",
22322232
"front_end/ui/components/icon_button/IconButton.js",
22332233
"front_end/ui/components/icon_button/fileSourceIcon.css.js",
2234-
"front_end/ui/components/icon_button/icon.css.legacy.js",
2234+
"front_end/ui/components/icon_button/icon.css.js",
22352235
"front_end/ui/components/icon_button/iconButton.css.js",
22362236
"front_end/ui/components/input/checkbox.css.js",
22372237
"front_end/ui/components/input/textInput.css.js",
@@ -2502,7 +2502,7 @@ grd_files_debug_sources = [
25022502
"front_end/ui/legacy/themeColors.css.legacy.js",
25032503
"front_end/ui/legacy/theme_support/ThemeSupport.js",
25042504
"front_end/ui/legacy/tokens.css.legacy.js",
2505-
"front_end/ui/legacy/toolbar.css.legacy.js",
2505+
"front_end/ui/legacy/toolbar.css.js",
25062506
"front_end/ui/legacy/treeoutline.css.legacy.js",
25072507
"front_end/ui/legacy/viewContainers.css.legacy.js",
25082508
"front_end/ui/lit-html/i18n-template.js",

front_end/core/dom_extension/DOMExtension.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,64 @@ DOMTokenList.prototype['toggle'] = function(token: string, force: boolean|undefi
399399
return originalToggle.call(this, token, Boolean(force));
400400
};
401401
})();
402+
403+
// DevTools uses multiple documents when the main window is undocked, and the
404+
// device mode toolbar is enabled. Since `CSSStyleSheet` objects cannot be
405+
// shared across multiple documents, we override the `adoptedStyleSheets`
406+
// accessor here, and clone the `CSSStyleSheet` objects under hood on-demand.
407+
//
408+
// NOTE: Since `adoptedStyleSheets` is an `ObservableArray`, which can be
409+
// mutated in place, we need to override both the setter and the getter, and
410+
// for the latter, we need to return a proxy that intercepts mutations to the
411+
// array-indexed properties.
412+
const originalAdoptedStyleSheets = Object.getOwnPropertyDescriptor(ShadowRoot.prototype, 'adoptedStyleSheets');
413+
if (originalAdoptedStyleSheets) {
414+
const styleSheetInDocumentCache = new WeakMap<CSSStyleSheet, WeakMap<Document, CSSStyleSheet>>();
415+
416+
/**
417+
* Returns a clone of the `styleSheet` within the given `document`.
418+
*/
419+
function styleSheetInDocument(styleSheet: CSSStyleSheet, document: Document): CSSStyleSheet {
420+
let styleSheetCache = styleSheetInDocumentCache.get(styleSheet);
421+
if (styleSheetCache) {
422+
const cachedSheet = styleSheetCache.get(document);
423+
if (cachedSheet) {
424+
return cachedSheet;
425+
}
426+
} else {
427+
styleSheetCache = new WeakMap<Document, CSSStyleSheet>();
428+
styleSheetInDocumentCache.set(styleSheet, styleSheetCache);
429+
}
430+
431+
const clonedStyleSheet = new document.defaultView.CSSStyleSheet();
432+
for (const {cssText} of styleSheet.cssRules) {
433+
clonedStyleSheet.insertRule(cssText);
434+
}
435+
styleSheetCache.set(document, clonedStyleSheet);
436+
return clonedStyleSheet;
437+
}
438+
439+
Object.defineProperty(ShadowRoot.prototype, 'adoptedStyleSheets', {
440+
configurable: true,
441+
enumerable: true,
442+
get(this: ShadowRoot): CSSStyleSheet[] {
443+
const target = originalAdoptedStyleSheets.get.call(this);
444+
const {ownerDocument} = this.host;
445+
return new Proxy(target, {
446+
set(target, propertyKey, value, receiver) {
447+
if (propertyKey === String(propertyKey >>> 0) && ownerDocument !== document) {
448+
value = styleSheetInDocument(value, ownerDocument);
449+
}
450+
return Reflect.set(target, propertyKey, value, receiver);
451+
},
452+
});
453+
},
454+
set(this: ShadowRoot, styleSheets: CSSStyleSheet[]) {
455+
const {ownerDocument} = this.host;
456+
if (ownerDocument !== document) {
457+
styleSheets = styleSheets.map(styleSheet => styleSheetInDocument(styleSheet, ownerDocument));
458+
}
459+
originalAdoptedStyleSheets.set.call(this, styleSheets);
460+
}
461+
});
462+
}

front_end/ui/components/buttons/BUILD.gn

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import("../visibility.gni")
1010

1111
generate_css("legacy_css_files") {
1212
sources = [ "button.css" ]
13-
14-
legacy = true
1513
}
1614

1715
devtools_module("button") {

front_end/ui/components/buttons/Button.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import '../icon_button/icon_button.js';
77
import * as LitHtml from '../../lit-html/lit-html.js';
88
import * as VisualLogging from '../../visual_logging/visual_logging.js';
99

10-
import buttonStyles from './button.css.legacy.js';
10+
import buttonStyles from './button.css.js';
1111

1212
const {html, Directives: {ifDefined, ref, classMap}} = LitHtml;
1313

@@ -122,14 +122,6 @@ export class Button extends HTMLElement {
122122
super();
123123
this.setAttribute('role', 'presentation');
124124
this.addEventListener('click', this.#boundOnClick, true);
125-
126-
// TODO(crbug.com/359141904): Ideally we would be using
127-
// adopted style sheets for installing css styles, but this
128-
// currently throws an error when sharing the styles across
129-
// multiple documents. This is a workaround.
130-
const styleElement = document.createElement('style');
131-
styleElement.textContent = buttonStyles.cssContent;
132-
this.#shadow.appendChild(styleElement);
133125
}
134126

135127
/**
@@ -272,6 +264,7 @@ export class Button extends HTMLElement {
272264
}
273265

274266
connectedCallback(): void {
267+
this.#shadow.adoptedStyleSheets = [buttonStyles];
275268
this.#render();
276269
}
277270

front_end/ui/components/icon_button/BUILD.gn

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ generate_css("css_files") {
1717

1818
generate_css("legacy_css_files") {
1919
sources = [ "icon.css" ]
20-
21-
legacy = true
2220
}
2321

2422
devtools_module("icon_button") {

front_end/ui/components/icon_button/Icon.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import iconStyles from './icon.css.legacy.js';
5+
import iconStyles from './icon.css.js';
66

77
/**
88
* @deprecated
@@ -75,17 +75,10 @@ export class Icon extends HTMLElement {
7575
this.#icon = document.createElement('span');
7676
this.#shadowRoot = this.attachShadow({mode: 'open'});
7777
this.#shadowRoot.appendChild(this.#icon);
78+
}
7879

79-
// TODO(crbug.com/359141904): Ideally we'd have a `connectedCallback()` that would just
80-
// install the CSS via `adoptedStyleSheets`, but that throws when using the
81-
// same `CSSStyleSheet` across two different documents (which happens in the
82-
// case of undocked DevTools windows and using the DeviceMode). So the work-
83-
// around for now is to use legacy CSS injected as a <style> tag into the
84-
// ShadowRoot (which has been working well for the legacy UI components for
85-
// a long time).
86-
const styleElement = document.createElement('style');
87-
styleElement.textContent = iconStyles.cssContent;
88-
this.#shadowRoot.appendChild(styleElement);
80+
connectedCallback(): void {
81+
this.#shadowRoot.adoptedStyleSheets = [iconStyles];
8982
}
9083

9184
/**

front_end/ui/legacy/BUILD.gn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ generate_css("css_files") {
1212
sources = [
1313
"emptyWidget.css",
1414
"inspectorCommon.css",
15+
"toolbar.css",
1516
]
1617
}
1718

@@ -50,7 +51,6 @@ generate_css("legacy_css_files") {
5051
"textPrompt.css",
5152
"themeColors.css",
5253
"tokens.css",
53-
"toolbar.css",
5454
"treeoutline.css",
5555
"viewContainers.css",
5656
]

front_end/ui/legacy/Toolbar.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ import {GlassPane, PointerEventsBehavior} from './GlassPane.js';
4545
import {bindCheckbox} from './SettingsUI.js';
4646
import type {Suggestion} from './SuggestBox.js';
4747
import {Events as TextPromptEvents, TextPrompt} from './TextPrompt.js';
48-
import toolbarStyles from './toolbar.css.legacy.js';
48+
import toolbarStyles from './toolbar.css.js';
4949
import {Tooltip} from './Tooltip.js';
50-
import {CheckboxLabel, createShadowRootWithCoreStyles, LongClickController} from './UIUtils.js';
50+
import {CheckboxLabel, LongClickController} from './UIUtils.js';
5151

5252
const UIStrings = {
5353
/**
@@ -83,19 +83,21 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
8383
* @prop {boolean} wrappable - The `"wrappable"` attribute is reflected as property.
8484
*/
8585
export class Toolbar extends HTMLElement {
86+
#shadowRoot = this.attachShadow({mode: 'open'});
8687
private items: ToolbarItem[] = [];
8788
enabled: boolean = true;
8889
private compactLayout = false;
8990

9091
constructor() {
9192
super();
92-
createShadowRootWithCoreStyles(this, {cssFile: toolbarStyles}).createChild('slot');
93+
this.#shadowRoot.createChild('slot');
9394
}
9495

9596
connectedCallback(): void {
9697
if (!this.hasAttribute('role')) {
9798
this.setAttribute('role', 'toolbar');
9899
}
100+
this.#shadowRoot.adoptedStyleSheets = [toolbarStyles];
99101
}
100102

101103
/**

0 commit comments

Comments
 (0)