Display inherited highlight pseudo styles in style pane

The CSS highlight inheritance model specified at [1] introduces a
parallel cascade for highlight pseudos (::selection, ::spelling-error,
::grammar-error, and ::highlight). This change updates the style
pane to include this parallel pseudo cascade when displaying rules for
highlight pseudos.

The corresponding backend change is at [2].

A screenshot of what this looks like can be viewed at [3].

[1] https://drafts.csswg.org/css-pseudo-4/#highlight-cascade
[2] https://chromium-review.googlesource.com/c/chromium/src/+/3461424
[3] https://docs.google.com/document/d/10qOm--onzEQe6HT6z1kUqzAVOLkN7Rc42wnrahRdsr0/edit?usp=sharing

Bug: 1024156
Change-Id: I1c8a034d5d956acc6238a0965fd8dfdb92f2399b
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3502175
Reviewed-by: Johan Bay <jobay@chromium.org>
Reviewed-by: Delan Azabani <dazabani@igalia.com>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Commit-Queue: Dan Clark <daniec@microsoft.com>
This commit is contained in:
Dan Clark
2022-03-22 16:13:31 +00:00
committed by Devtools-frontend LUCI CQ
parent 2fc9ae9f46
commit bfe1683fe8
9 changed files with 175 additions and 11 deletions
+3
View File
@@ -4892,6 +4892,9 @@
"panels/elements/StylesSidebarPane.ts | inheritedFroms": {
"message": "Inherited from "
},
"panels/elements/StylesSidebarPane.ts | inheritedFromSPseudoOf": {
"message": "Inherited from ::{PH1} pseudo of "
},
"panels/elements/StylesSidebarPane.ts | injectedStylesheet": {
"message": "injected stylesheet"
},
+3
View File
@@ -4892,6 +4892,9 @@
"panels/elements/StylesSidebarPane.ts | inheritedFroms": {
"message": "Îńĥér̂ít̂éd̂ f́r̂óm̂ "
},
"panels/elements/StylesSidebarPane.ts | inheritedFromSPseudoOf": {
"message": "Îńĥér̂ít̂éd̂ f́r̂óm̂ ::{PH1} ṕŝéûd́ô óf̂ "
},
"panels/elements/StylesSidebarPane.ts | injectedStylesheet": {
"message": "îńĵéĉt́êd́ ŝt́ŷĺêśĥéêt́"
},
+46 -6
View File
@@ -28,6 +28,7 @@ export class CSSMatchedStyles {
cssModel: CSSModel, node: DOMNode, inlinePayload: Protocol.CSS.CSSStyle|null,
attributesPayload: Protocol.CSS.CSSStyle|null, matchedPayload: Protocol.CSS.RuleMatch[],
pseudoPayload: Protocol.CSS.PseudoElementMatches[], inheritedPayload: Protocol.CSS.InheritedStyleEntry[],
inheritedPseudoPayload: Protocol.CSS.InheritedPseudoElementMatches[],
animationsPayload: Protocol.CSS.CSSKeyframesRule[]) {
this.#cssModelInternal = cssModel;
this.#nodeInternal = node;
@@ -47,7 +48,7 @@ export class CSSMatchedStyles {
}
this.#mainDOMCascade = this.buildMainCascade(inlinePayload, attributesPayload, matchedPayload, inheritedPayload);
this.#pseudoDOMCascades = this.buildPseudoCascades(pseudoPayload);
this.#pseudoDOMCascades = this.buildPseudoCascades(pseudoPayload, inheritedPseudoPayload);
this.#styleToDOMCascade = new Map();
for (const domCascade of Array.from(this.#pseudoDOMCascades.values()).concat(this.#mainDOMCascade)) {
@@ -205,12 +206,16 @@ export class CSSMatchedStyles {
}
}
private buildPseudoCascades(pseudoPayload: Protocol.CSS.PseudoElementMatches[]):
private buildPseudoCascades(
pseudoPayload: Protocol.CSS.PseudoElementMatches[],
inheritedPseudoPayload: Protocol.CSS.InheritedPseudoElementMatches[]):
Map<Protocol.DOM.PseudoType, DOMInheritanceCascade> {
const pseudoCascades = new Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>();
const pseudoInheritanceCascades = new Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>();
if (!pseudoPayload) {
return pseudoCascades;
return pseudoInheritanceCascades;
}
const pseudoCascades = new Map<Protocol.DOM.PseudoType, NodeCascade[]>();
for (let i = 0; i < pseudoPayload.length; ++i) {
const entryPayload = pseudoPayload[i];
// PseudoElement nodes are not created unless "content" css property is set.
@@ -226,9 +231,44 @@ export class CSSMatchedStyles {
}
}
const nodeCascade = new NodeCascade(this, pseudoStyles, false /* #isInherited */);
pseudoCascades.set(entryPayload.pseudoType, new DOMInheritanceCascade([nodeCascade]));
pseudoCascades.set(entryPayload.pseudoType, [nodeCascade]);
}
return pseudoCascades;
if (inheritedPseudoPayload) {
let parentNode: (DOMNode|null) = this.#nodeInternal.parentNode;
for (let i = 0; parentNode && i < inheritedPseudoPayload.length; ++i) {
const inheritedPseudoMatches = inheritedPseudoPayload[i].pseudoElements;
for (let j = 0; j < inheritedPseudoMatches.length; ++j) {
const inheritedEntryPayload = inheritedPseudoMatches[j];
const pseudoStyles = [];
const rules = inheritedEntryPayload.matches || [];
for (let k = rules.length - 1; k >= 0; --k) {
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[k].rule);
pseudoStyles.push(pseudoRule.style);
this.#nodeForStyleInternal.set(pseudoRule.style, parentNode);
this.#inheritedStyles.add(pseudoRule.style);
}
const nodeCascade = new NodeCascade(this, pseudoStyles, true /* #isInherited */);
const cascadeListForPseudoType = pseudoCascades.get(inheritedEntryPayload.pseudoType);
if (cascadeListForPseudoType) {
cascadeListForPseudoType.push(nodeCascade);
} else {
pseudoCascades.set(inheritedEntryPayload.pseudoType, [nodeCascade]);
}
}
parentNode = parentNode.parentNode;
}
}
// Now that we've build the arrays of NodeCascades each pseudo type, convert them into
// DOMInheritanceCascades.
for (const [pseudoType, nodeCascade] of pseudoCascades.entries()) {
pseudoInheritanceCascades.set(pseudoType, new DOMInheritanceCascade(nodeCascade));
}
return pseudoInheritanceCascades;
}
private addMatchingSelectors(
+1 -1
View File
@@ -303,7 +303,7 @@ export class CSSModel extends SDKModel<EventTypes> {
return new CSSMatchedStyles(
this, (node as DOMNode), response.inlineStyle || null, response.attributesStyle || null,
response.matchedCSSRules || [], response.pseudoElements || [], response.inherited || [],
response.cssKeyframesRules || []);
response.inheritedPseudoElements || [], response.cssKeyframesRules || []);
}
async getClassNames(styleSheetId: Protocol.CSS.StyleSheetId): Promise<string[]> {
+38 -3
View File
@@ -98,6 +98,11 @@ const UIStrings = {
*/
inheritedFroms: 'Inherited from ',
/**
*@description Text of an inherited psuedo element in Styles Sidebar Pane of the Elements panel
*@example {highlight} PH1
*/
inheritedFromSPseudoOf: 'Inherited from ::{PH1} pseudo of ',
/**
*@description Tooltip text that appears when hovering over the largeicon add button in the Styles Sidebar Pane of the Elements panel
*/
insertStyleRuleBelow: 'Insert Style Rule Below',
@@ -921,10 +926,26 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
}
pseudoTypes = pseudoTypes.concat([...keys].sort());
for (const pseudoType of pseudoTypes) {
blocks.push(SectionBlock.createPseudoTypeBlock(pseudoType));
lastLayers = null;
lastParentNode = null;
const pseudoStyles = matchedStyles.pseudoStyles(pseudoType);
for (let i = 0; i < pseudoStyles.length; ++i) {
const style = pseudoStyles[i];
const parentNode = matchedStyles.isInherited(style) ? matchedStyles.nodeForStyle(style) : null;
// Start a new SectionBlock if this is the first rule for this pseudo type, or if this
// rule is inherited from a different parent than the previous rule.
if (i === 0 || parentNode !== lastParentNode) {
lastLayers = null;
if (parentNode) {
const block = await SectionBlock.createInheritedPseudoTypeBlock(pseudoType, parentNode);
blocks.push(block);
} else {
const block = SectionBlock.createPseudoTypeBlock(pseudoType);
blocks.push(block);
}
}
lastParentNode = parentNode;
for (const style of matchedStyles.pseudoStyles(pseudoType)) {
addLayerSeparator(style);
const lastBlock = blocks[blocks.length - 1];
this.idleCallbackManager.schedule(() => {
@@ -1410,6 +1431,20 @@ export class SectionBlock {
return new SectionBlock(separatorElement);
}
static async createInheritedPseudoTypeBlock(pseudoType: Protocol.DOM.PseudoType, node: SDK.DOMModel.DOMNode):
Promise<SectionBlock> {
const separatorElement = document.createElement('div');
separatorElement.className = 'sidebar-separator';
UI.UIUtils.createTextChild(separatorElement, i18nString(UIStrings.inheritedFromSPseudoOf, {PH1: pseudoType}));
const link = await Common.Linkifier.Linkifier.linkify(node, {
preventKeyboardFocus: true,
tooltip: undefined,
});
separatorElement.appendChild(link);
return new SectionBlock(separatorElement);
}
static createKeyframesBlock(keyframesName: string): SectionBlock {
const separatorElement = document.createElement('div');
separatorElement.className = 'sidebar-separator';
+1 -1
View File
@@ -57,7 +57,7 @@ function launchChrome() {
'--ignore-certificate-errors-spki-list=KLy6vv6synForXwI6lDIl+D3ZrMV6Y1EMTY6YpOcAos=',
'--site-per-process', // Default on Desktop anyway, but ensure that we always use out-of-process frames when we intend to.
'--host-resolver-rules=MAP *.test 127.0.0.1', '--disable-gpu',
'--enable-blink-features=CSSContainerQueries', // TODO(crbug.com/1218390) Remove globally enabled flag and conditionally enable it
'--enable-blink-features=CSSContainerQueries,HighlightInheritance', // TODO(crbug.com/1218390) Remove globally enabled flags and conditionally enable them
];
const opts: puppeteer.LaunchOptions&puppeteer.BrowserLaunchArgumentOptions&puppeteer.BrowserConnectOptions = {
headless,
@@ -18,6 +18,7 @@ const FIRST_PROPERTY_VALUE_SELECTOR = '.tree-outline li:nth-of-type(1) > .value'
const RULE1_SELECTOR = '.rule1';
const RULE2_SELECTOR = '.rule2';
const LAYER_SEPARATOR_SELECTOR = '.layer-separator';
const SIDEBAR_SEPARATOR_SELECTOR = '.sidebar-separator';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const deletePropertyByBackspace = async (selector: string, root?: puppeteer.ElementHandle<Element>) => {
@@ -403,4 +404,54 @@ describe('The Styles pane', async () => {
const treeElement = await waitFor('[data-node-key="2: overrule"]');
assertNotNullOrUndefined(treeElement);
});
it('can display inherited CSS highlight pseudo styles', async () => {
const {frontend} = getBrowserAndPages();
await goToResourceAndWaitForStyleSection('elements/highlight-pseudo-inheritance.html');
const onH1RuleAppeared = waitForStyleRule('h1');
// Select the h1 for which we will inspect the pseudo styles
await frontend.keyboard.press('ArrowRight');
await onH1RuleAppeared;
const h1Rules = await getDisplayedStyleRules();
// The 6 rule blocks for the h1 are:
// 1. Inline styles from the style attribute
// 2. The h1's user agent styles
// 3. The h1's own highlight pseudo
// 4. The h1's inherited highlight pseudo
// 5. The h1's own selection pseudo
// 6. The h1's inherited selection pseudo
// And there is no 6th block for the ::first-letter style, since only
// highlight pseudos are inherited.
assert.strictEqual(h1Rules.length, 6, 'The h1 should have 6 style rule blocks');
assert.deepEqual(
h1Rules[2], {selectorText: 'h1::highlight(foo)', propertyNames: ['background-color']},
'The h1\'s own highlight pseudo is displayed');
assert.deepEqual(
h1Rules[3], {selectorText: 'body::highlight(bar)', propertyNames: ['color']},
'The h1\'s inherited highlight pseudo is displayed');
assert.deepEqual(
h1Rules[4], {selectorText: 'h1::selection', propertyNames: ['background-color']},
'The h1\'s own selection pseudo is displayed');
assert.deepEqual(
h1Rules[5], {selectorText: 'body::selection', propertyNames: ['text-shadow']},
'The h1\'s inherited selection pseudo is displayed');
const sidebarSeparators = await waitForFunction(async () => {
const separators = await $$(SIDEBAR_SEPARATOR_SELECTOR);
return separators.length === 4 ? separators : null;
});
assertNotNullOrUndefined(sidebarSeparators);
const layerText = await Promise.all(sidebarSeparators.map(element => element.evaluate(node => node.textContent)));
assert.deepEqual(layerText, [
'Pseudo ::highlight element',
'Inherited from ::highlight pseudo of ',
'Pseudo ::selection element',
'Inherited from ::selection pseudo of ',
]);
});
});
+1
View File
@@ -25,6 +25,7 @@ copy_to_gen("elements") {
"flexbox-editor.html",
"focus.html",
"grid-editor.html",
"highlight-pseudo-inheritance.html",
"hover.html",
"limited-quirks-mode.html",
"low-contrast.html",
@@ -0,0 +1,31 @@
<!--
Copyright 2022 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<!DOCTYPE html>
<meta charset="utf-8">
<style>
h1::highlight(foo) {
background-color: limegreen;
}
h1::selection {
background-color: blue;
}
body::highlight(bar) {
color: lightblue;
}
body::selection {
text-shadow: red 5px 5px;
}
body::first-letter {
font-size: xx-large;
}
</style>
<body>
<h1>target</h1>
</body>