Add indentation and ancestor braces for nesting styles Screenshots: https://imgur.com/a/Ulph3UF Bug: b/40166888 Change-Id: Id0af2ae023643810c1a445375aeec7eb549df9e6 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5307622 Commit-Queue: Changhao Han <changhaohan@google.com> Reviewed-by: Ergün Erdoğmuş <ergunsh@chromium.org>
diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 8f273b7..0b9a73e 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni
@@ -1232,7 +1232,7 @@ "front_end/panels/elements/nodeStackTraceWidget.css.js", "front_end/panels/elements/platformFontsWidget.css.js", "front_end/panels/elements/propertiesWidget.css.js", - "front_end/panels/elements/stylesSectionTree.css.js", + "front_end/panels/elements/stylePropertiesTreeOutline.css.js", "front_end/panels/elements/stylesSidebarPane.css.js", "front_end/panels/emulation/AdvancedApp.js", "front_end/panels/emulation/DeviceModeToolbar.js",
diff --git a/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js b/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js index 4b9b305..3c7b444 100644 --- a/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js +++ b/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js
@@ -510,7 +510,7 @@ TestRunner.addResult( '[expanded] ' + ((section.propertiesTreeOutline.element.classList.contains('no-affect') ? '[no-affect] ' : ''))); - const queries = section.titleElement.querySelectorAll('devtools-css-query'); + const queries = section.element.querySelectorAll('devtools-css-query'); for (const query of queries) { const queryElement = query.shadowRoot.querySelector('.query');
diff --git a/front_end/panels/elements/BUILD.gn b/front_end/panels/elements/BUILD.gn index 88ef618..7529ceb 100644 --- a/front_end/panels/elements/BUILD.gn +++ b/front_end/panels/elements/BUILD.gn
@@ -22,7 +22,7 @@ "nodeStackTraceWidget.css", "platformFontsWidget.css", "propertiesWidget.css", - "stylesSectionTree.css", + "stylePropertiesTreeOutline.css", "stylesSidebarPane.css", ] }
diff --git a/front_end/panels/elements/StylePropertiesSection.test.ts b/front_end/panels/elements/StylePropertiesSection.test.ts index a5954d8..ab0a1d9 100644 --- a/front_end/panels/elements/StylePropertiesSection.test.ts +++ b/front_end/panels/elements/StylePropertiesSection.test.ts
@@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import type * as TextUtils from '../../models/text_utils/text_utils.js'; -import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js'; +import {createTarget} from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; @@ -158,8 +159,37 @@ assert.strictEqual(linkifier.linkifyCSSLocation.args[0][0].url, ''); }); + it('properly renders ancestor rules', async () => { + Common.Settings.Settings.instance().moduleSetting('text-editor-indent').set(' '); + const cssModel = createTarget().model(SDK.CSSModel.CSSModel); + Platform.assertNotNullOrUndefined(cssModel); + const stylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane.instance({forceNew: true}); + const origin = Protocol.CSS.StyleSheetOrigin.Regular; + const styleSheetId = '0' as Protocol.CSS.StyleSheetId; + const range = {startLine: 0, startColumn: 0, endLine: 0, endColumn: 6}; + const matchedPayload: Protocol.CSS.RuleMatch[] = [{ + rule: { + nestingSelectors: ['body', '& ul', 'div'], + ruleTypes: [ + Protocol.CSS.CSSRuleType.StyleRule, + Protocol.CSS.CSSRuleType.StyleRule, + Protocol.CSS.CSSRuleType.StyleRule, + ], + selectorList: {selectors: [{text: 'div'}], text: 'div'}, + origin, + style: {cssProperties: [{name: 'color', value: 'red'}], shorthandEntries: []}, + }, + matchingSelectors: [0], + }]; + const matchedStyles = await setUpStyles(cssModel, origin, styleSheetId, {...range}, {matchedPayload}); + const declaration = matchedStyles.nodeStyles()[0]; + Platform.assertNotNullOrUndefined(declaration); + const section = new Elements.StylePropertiesSection.StylePropertiesSection( + stylesSidebarPane, matchedStyles, declaration, 0, null, null); + assert.strictEqual(section.element.textContent, 'div { & ul { body { div { } } }}'); + }); + it('updates property rule property names', async () => { - stubNoopSettings(); const cssModel = createTarget().model(SDK.CSSModel.CSSModel); Platform.assertNotNullOrUndefined(cssModel); const stylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane.instance({forceNew: true});
diff --git a/front_end/panels/elements/StylePropertiesSection.ts b/front_end/panels/elements/StylePropertiesSection.ts index 4498470..c5e691b 100644 --- a/front_end/panels/elements/StylePropertiesSection.ts +++ b/front_end/panels/elements/StylePropertiesSection.ts
@@ -48,8 +48,8 @@ import * as ElementsComponents from './components/components.js'; import {linkifyDeferredNodeReference} from './DOMLinkifier.js'; import {ElementsPanel} from './ElementsPanel.js'; +import stylePropertiesTreeOutlineStyles from './stylePropertiesTreeOutline.css.js'; import {type Context, StylePropertyTreeElement} from './StylePropertyTreeElement.js'; -import stylesSectionTreeStyles from './stylesSectionTree.css.js'; import {StylesSidebarPane} from './StylesSidebarPane.js'; const UIStrings = { @@ -127,7 +127,7 @@ private forceShowAll: boolean; private readonly originalPropertiesCount: number; element: HTMLDivElement; - private readonly innerElement: HTMLElement; + readonly #styleRuleElement: HTMLElement; private readonly titleElement: HTMLElement; propertiesTreeOutline: UI.TreeOutline.TreeOutlineInShadow; private showAllButton: HTMLButtonElement; @@ -143,7 +143,9 @@ private hoverableSelectorsMode: boolean; private isHiddenInternal: boolean; - private ancestorRuleListElement: HTMLElement; + nestingLevel = 0; + #ancestorRuleListElement: HTMLElement; + #ancestorClosingBracesElement: HTMLElement; // Used to identify buttons that trigger a flexbox or grid editor. nextEditorTriggerButtonIdx = 1; @@ -181,23 +183,34 @@ UI.ARIAUtils.markAsListitem(this.element); this.element.addEventListener('keydown', this.onKeyDown.bind(this), false); parentPane.sectionByElement.set(this.element, this); - this.innerElement = this.element.createChild('div'); + this.#styleRuleElement = this.element.createChild('div', 'style-rule'); - this.titleElement = this.innerElement.createChild('div', 'styles-section-title ' + (rule ? 'styles-selector' : '')); + this.#ancestorRuleListElement = document.createElement('div'); + this.#ancestorRuleListElement.classList.add('ancestor-rule-list'); + this.element.prepend(this.#ancestorRuleListElement); + this.#ancestorClosingBracesElement = document.createElement('div'); + this.#ancestorClosingBracesElement.classList.add('ancestor-closing-braces'); + this.element.append(this.#ancestorClosingBracesElement); + this.updateAncestorRuleList(); + + this.titleElement = + this.#styleRuleElement.createChild('div', 'styles-section-title ' + (rule ? 'styles-selector' : '')); this.propertiesTreeOutline = new UI.TreeOutline.TreeOutlineInShadow(); this.propertiesTreeOutline.setFocusable(false); - this.propertiesTreeOutline.registerCSSFiles([stylesSectionTreeStyles]); + this.propertiesTreeOutline.registerCSSFiles([stylePropertiesTreeOutlineStyles]); this.propertiesTreeOutline.element.classList.add('style-properties', 'matched-styles', 'monospace'); - this.innerElement.appendChild(this.propertiesTreeOutline.element); + this.#styleRuleElement.appendChild(this.propertiesTreeOutline.element); this.showAllButton = UI.UIUtils.createTextButton('', this.showAllItems.bind(this), { className: 'styles-show-all', jslogContext: 'elements.show-all-style-properties', }); - this.innerElement.appendChild(this.showAllButton); + this.#styleRuleElement.appendChild(this.showAllButton); + const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get(); const selectorContainer = document.createElement('div'); + selectorContainer.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel); selectorContainer.classList.add('selector-container'); this.selectorElement = document.createElement('span'); UI.ARIAUtils.setLabel(this.selectorElement, i18nString(UIStrings.cssSelector)); @@ -210,8 +223,9 @@ const openBrace = selectorContainer.createChild('span', 'sidebar-pane-open-brace'); openBrace.textContent = ' {'; - const closeBrace = this.innerElement.createChild('div', 'sidebar-pane-closing-brace'); - closeBrace.textContent = '}'; + const closeBrace = this.#styleRuleElement.createChild('div', 'sidebar-pane-closing-brace'); + closeBrace.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel); + closeBrace.createChild('span').textContent = '}'; if (this.styleInternal.parentRule) { const newRuleButton = new UI.Toolbar.ToolbarButton( @@ -220,14 +234,14 @@ newRuleButton.element.tabIndex = -1; if (!this.newStyleRuleToolbar) { this.newStyleRuleToolbar = - new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar new-rule-toolbar', this.innerElement); + new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar new-rule-toolbar', this.element); } this.newStyleRuleToolbar.appendToolbarItem(newRuleButton); UI.ARIAUtils.markAsHidden(this.newStyleRuleToolbar.element); } if (Root.Runtime.experiments.isEnabled('font-editor') && this.editable) { - this.fontEditorToolbar = new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar', this.innerElement); + this.fontEditorToolbar = new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar', this.#styleRuleElement); this.fontEditorSectionManager = new FontEditorSectionManager(this.parentPane.swatchPopoverHelper(), this); this.fontEditorButton = new UI.Toolbar.ToolbarButton('Font Editor', 'custom-typography'); this.fontEditorButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => { @@ -274,9 +288,7 @@ } } - this.ancestorRuleListElement = this.titleElement.createChild('div', 'ancestor-rule-list'); this.selectorRefElement = this.titleElement.createChild('div', 'styles-section-subtitle'); - this.updateQueryList(); this.updateRuleOrigin(); this.titleElement.appendChild(selectorContainer); @@ -735,7 +747,7 @@ this.styleInternal.rebase(edit); } - this.updateQueryList(); + this.updateAncestorRuleList(); this.updateRuleOrigin(); } @@ -745,6 +757,8 @@ let scopeIndex = 0; let supportsIndex = 0; let nestingIndex = 0; + this.nestingLevel = 0; + const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get(); for (const ruleType of rule.ruleTypes) { let ancestorRuleElement; switch (ruleType) { @@ -764,7 +778,26 @@ ancestorRuleElement = this.createNestingElement(rule.nestingSelectors?.[nestingIndex++]); break; } - ancestorRuleElement && this.ancestorRuleListElement.prepend(ancestorRuleElement); + if (ancestorRuleElement) { + this.#ancestorRuleListElement.prepend(ancestorRuleElement); + const closingBrace = document.createElement('div'); + closingBrace.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel); + closingBrace.style.paddingLeft = `${this.nestingLevel}ch`; + closingBrace.append('}'); + this.#ancestorClosingBracesElement.prepend(closingBrace); + this.nestingLevel++; + } + } + + let curNestingLevel = 0; + for (const element of this.#ancestorRuleListElement.children) { + const indentElement = document.createElement('span'); + indentElement.classList.add('styles-clipboard-only'); + indentElement.setAttribute('slot', 'indent'); + indentElement.textContent = indent.repeat(curNestingLevel); + element.prepend(indentElement); + (element as HTMLElement).style.paddingLeft = `${curNestingLevel}ch`; + curNestingLevel++; } } @@ -872,7 +905,7 @@ return; } const nestingElement = document.createElement('div'); - nestingElement.textContent = nestingSelector; + nestingElement.textContent = `${nestingSelector} {`; return nestingElement; } @@ -900,14 +933,16 @@ } }); - this.ancestorRuleListElement.prepend(containerElement); + this.#ancestorRuleListElement.prepend(containerElement); } - private updateQueryList(): void { - this.ancestorRuleListElement.removeChildren(); + private updateAncestorRuleList(): void { + this.#ancestorRuleListElement.removeChildren(); + this.#ancestorClosingBracesElement.removeChildren(); if (this.styleInternal.parentRule && this.styleInternal.parentRule instanceof SDK.CSSRule.CSSStyleRule) { this.createAncestorRules(this.styleInternal.parentRule); } + this.#styleRuleElement.style.paddingLeft = `${this.nestingLevel}ch`; } isPropertyInherited(propertyName: string): boolean {
diff --git a/front_end/panels/elements/StylePropertyTreeElement.ts b/front_end/panels/elements/StylePropertyTreeElement.ts index 1dbd04a..0277a18 100644 --- a/front_end/panels/elements/StylePropertyTreeElement.ts +++ b/front_end/panels/elements/StylePropertyTreeElement.ts
@@ -1242,7 +1242,7 @@ const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get(); UI.UIUtils.createTextChild( this.listItemElement.createChild('span', 'styles-clipboard-only'), - indent + (this.property.disabled ? '/* ' : '')); + indent.repeat(this.section().nestingLevel + 1) + (this.property.disabled ? '/* ' : '')); if (this.nameElement) { this.listItemElement.appendChild(this.nameElement); }
diff --git a/front_end/panels/elements/components/CSSQuery.test.ts b/front_end/panels/elements/components/CSSQuery.test.ts index 64b2929..1d71937 100644 --- a/front_end/panels/elements/components/CSSQuery.test.ts +++ b/front_end/panels/elements/components/CSSQuery.test.ts
@@ -27,7 +27,7 @@ assert.strictEqual( queryElement.innerText, - '@container (min-width: 10px)', + '@container (min-width: 10px) {', 'text content of query element should match query text', ); }); @@ -55,7 +55,7 @@ assert.strictEqual( queryElement.innerText, - '@container container-query-1 (max-width: 10px)', + '@container container-query-1 (max-width: 10px) {', 'text content of query element should match query text', );
diff --git a/front_end/panels/elements/components/CSSQuery.ts b/front_end/panels/elements/components/CSSQuery.ts index 656d245..e0b5560 100644 --- a/front_end/panels/elements/components/CSSQuery.ts +++ b/front_end/panels/elements/components/CSSQuery.ts
@@ -55,7 +55,7 @@ render(html` <div class=${queryClasses}> - ${this.#queryPrefix ? html`<span>${this.#queryPrefix + ' '}</span>` : LitHtml.nothing}${this.#queryName ? html`<span>${this.#queryName + ' '}</span>` : LitHtml.nothing}${queryText} + <slot name="indent"></slot>${this.#queryPrefix ? html`<span>${this.#queryPrefix + ' '}</span>` : LitHtml.nothing}${this.#queryName ? html`<span>${this.#queryName + ' '}</span>` : LitHtml.nothing}${queryText} { </div> `, this.#shadow, { host: this,
diff --git a/front_end/panels/elements/stylesSectionTree.css b/front_end/panels/elements/stylePropertiesTreeOutline.css similarity index 97% rename from front_end/panels/elements/stylesSectionTree.css rename to front_end/panels/elements/stylePropertiesTreeOutline.css index f8ec990..4927d0c 100644 --- a/front_end/panels/elements/stylesSectionTree.css +++ b/front_end/panels/elements/stylePropertiesTreeOutline.css
@@ -114,8 +114,8 @@ height: 13px; } -.tree-outline li.editing .enabled-button { - display: none !important; /* stylelint-disable-line declaration-no-important */ +.tree-outline li.child-editing .enabled-button { + display: none; } .overloaded:not(.has-ignorable-error, .invalid-property-value),
diff --git a/front_end/panels/elements/stylesSidebarPane.css b/front_end/panels/elements/stylesSidebarPane.css index 837641f..64ec36e 100644 --- a/front_end/panels/elements/stylesSidebarPane.css +++ b/front_end/panels/elements/stylesSidebarPane.css
@@ -11,10 +11,7 @@ border-bottom: 1px solid var(--sys-color-divider); position: relative; overflow: hidden; - - > div { - padding: 2px 2px 4px 4px; - } + padding: 2px 2px 4px 4px; &:last-child { border-bottom: none; @@ -54,8 +51,14 @@ } .selector, - .try-rule-selector-element { - color: var(--sys-color-state-disabled); + .try-rule-selector-element, + .ancestor-rule-list, + .ancestor-closing-braces { + color: var(--sys-color-token-meta); + } + + .ancestor-rule-list devtools-css-query { + display: block; } .simple-selector.selector-matches, @@ -96,10 +99,6 @@ background-clip: padding; word-wrap: break-word; white-space: normal; - - .ancestor-rule-list { - color: var(--sys-color-state-disabled); - } } .styles-section-subtitle { @@ -155,6 +154,15 @@ cursor: text; } +/* TODO(changhaohan): restructure this in relation to stylePropertiesTreeOutline.css. */ +.styles-clipboard-only { + display: inline-block; + width: 0; + opacity: 0%; + pointer-events: none; + white-space: pre; +} + .styles-sidebar-pane-toolbar-container { flex-shrink: 0; overflow: hidden;
diff --git a/front_end/testing/EnvironmentHelpers.ts b/front_end/testing/EnvironmentHelpers.ts index 2f22d61..fda536d 100644 --- a/front_end/testing/EnvironmentHelpers.ts +++ b/front_end/testing/EnvironmentHelpers.ts
@@ -273,6 +273,9 @@ createSettingValue( Common.Settings.SettingCategory.PERFORMANCE, 'flamechart-mouse-wheel-action', false, Common.Settings.SettingType.ENUM), + createSettingValue( + Common.Settings.SettingCategory.ELEMENTS, 'show-css-property-documentation-on-hover', false, + Common.Settings.SettingType.BOOLEAN), ]; Common.Settings.registerSettingsForTest(settings, reset);
diff --git a/test/e2e/elements/style-pane-properties_test.ts b/test/e2e/elements/style-pane-properties_test.ts index 9297b9a..1fdad78 100644 --- a/test/e2e/elements/style-pane-properties_test.ts +++ b/test/e2e/elements/style-pane-properties_test.ts
@@ -389,7 +389,7 @@ const rule1PropertiesSection = await getStyleRule(RULE1_SELECTOR); const supportsQuery = await waitFor('.query.editable', rule1PropertiesSection); const supportsQueryText = await supportsQuery.evaluate(node => (node as HTMLElement).innerText as string); - assert.deepEqual(supportsQueryText, '@supports (width: 10px)', 'incorrectly displayed @supports rule'); + assert.deepEqual(supportsQueryText, '@supports (width: 10px) {', 'incorrectly displayed @supports rule'); }); it('can display @layer separators', async () => {