mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 20:11:38 +08:00
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:
committed by
Devtools-frontend LUCI CQ
parent
2fc9ae9f46
commit
bfe1683fe8
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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́"
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 ',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user