Add an expandable table with new pseudo classes specific to the selected element.
The new supported pseudo classes are: enable, disable, valid, invalid, user-valid, user-invalid, required, optional, read-only, read-write, in-range, out-of-range, checked, indeterminate, placeholder-shown, autofill.
Also changed the old table to a `grid` container.
Bug:40280012
Change-Id: I0802a1f7660aee28b3878f067ccfe15497b3cd0d
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5692026
Commit-Queue: Ionuț Marius Voicilă <ionutvoicila@google.com>
Reviewed-by: Ergün Erdoğmuş <ergunsh@chromium.org>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
diff --git a/front_end/panels/elements/BUILD.gn b/front_end/panels/elements/BUILD.gn
index 2dab264..efcfb57 100644
--- a/front_end/panels/elements/BUILD.gn
+++ b/front_end/panels/elements/BUILD.gn
@@ -153,6 +153,7 @@
"CSSRuleValidator.test.ts",
"ClassesPaneWidget.test.ts",
"DOMLinkifier.test.ts",
+ "ElementStatePaneWidget.test.ts",
"ElementsPanel.test.ts",
"ElementsTreeElementHighlighter.test.ts",
"InspectElementModeController.test.ts",
diff --git a/front_end/panels/elements/ElementStatePaneWidget.test.ts b/front_end/panels/elements/ElementStatePaneWidget.test.ts
new file mode 100644
index 0000000..f2b16dd
--- /dev/null
+++ b/front_end/panels/elements/ElementStatePaneWidget.test.ts
@@ -0,0 +1,332 @@
+// Copyright 2024 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.
+
+import * as SDK from '../../core/sdk/sdk.js';
+import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
+import {
+ describeWithMockConnection,
+} from '../../testing/MockConnection.js';
+import * as UI from '../../ui/legacy/legacy.js';
+
+import * as Elements from './elements.js';
+
+describeWithMockConnection('ElementStatePaneWidget', () => {
+ let target: SDK.Target.Target;
+ let view: Elements.ElementStatePaneWidget.ElementStatePaneWidget;
+
+ const pseudoClasses = [
+ 'enabled',
+ 'disabled',
+ 'valid',
+ 'invalid',
+ 'user-valid',
+ 'user-invalid',
+ 'required',
+ 'optional',
+ 'read-only',
+ 'read-write',
+ 'in-range',
+ 'out-of-range',
+ 'visited',
+ 'checked',
+ 'indeterminate',
+ 'placeholder-shown',
+ 'autofill',
+ ];
+
+ beforeEach(() => {
+ stubNoopSettings();
+ target = createTarget();
+ });
+
+ const assertExpectedPseudoClasses = async (
+ nodeName: string, expectedPseudoClasses: string[], formAssociated: boolean = false,
+ attribute?: [string, string]) => {
+ view = new Elements.ElementStatePaneWidget.ElementStatePaneWidget();
+ const tableUpdatedPromise = new Promise<void>(
+ resolve => sinon.stub(view, 'updateElementSpecificStatesTableForTest').callsFake(resolve),
+ );
+
+ const model = target.model(SDK.DOMModel.DOMModel);
+ assert.exists(model);
+
+ const node = new SDK.DOMModel.DOMNode(model);
+
+ sinon.stub(node, 'nodeType').returns(Node.ELEMENT_NODE);
+ sinon.stub(node, 'nodeName').returns(nodeName);
+ sinon.stub(node, 'enclosingElementOrSelf').returns(node);
+ sinon.stub(node, 'callFunction').resolves({value: formAssociated});
+ if (attribute) {
+ sinon.stub(node, 'getAttribute').withArgs(attribute[0]).returns(attribute[1]);
+ }
+
+ const header = view.contentElement.querySelector('.force-specific-element-header');
+ assert.instanceOf(header, HTMLElement);
+ header.click();
+
+ UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
+
+ await tableUpdatedPromise;
+
+ for (const pseudoClass of pseudoClasses) {
+ const div = view.contentElement.querySelector(`#${pseudoClass}`);
+ assert.instanceOf(div, HTMLDivElement);
+ assert.strictEqual(!div.hidden, expectedPseudoClasses.includes(div.id), `Wrong state for ${div.id}`);
+ }
+ };
+
+ it('Calls the right backend functions', async () => {
+ view = new Elements.ElementStatePaneWidget.ElementStatePaneWidget();
+
+ const model = target.model(SDK.DOMModel.DOMModel);
+ assert.exists(model);
+
+ const node = new SDK.DOMModel.DOMNode(model);
+
+ sinon.stub(node, 'nodeType').returns(Node.ELEMENT_NODE);
+ sinon.stub(node, 'nodeName').returns('input');
+ sinon.stub(node, 'enclosingElementOrSelf').returns(node);
+ const checkboxes = sinon.spy(node.domModel().cssModel(), 'forcePseudoState');
+
+ UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
+
+ for (const pseudoClass of pseudoClasses) {
+ const div = view.contentElement.querySelector(`#${pseudoClass}`);
+ assert.exists(div);
+ const span = div.children[0];
+ const shadowRoot = span.shadowRoot;
+ const input = shadowRoot?.querySelector('input');
+ assert.exists(input, 'The span element doesn\'t have an input element');
+ input.click();
+
+ const args = checkboxes.lastCall.args;
+ assert.strictEqual(args[0], node, 'Called forcePseudoState with wrong node');
+ assert.strictEqual(args[1], pseudoClass, 'Called forcePseudoState with wrong pseudo-state');
+ assert.strictEqual(args[2], true, 'Called forcePseudoState with wrong enable state');
+ }
+ });
+
+ it('Hidden state for not ELEMENT_NODE type', async () => {
+ view = new Elements.ElementStatePaneWidget.ElementStatePaneWidget();
+ const tableUpdatedPromise = new Promise<void>(
+ resolve => sinon.stub(view, 'updateElementSpecificStatesTableForTest').callsFake(resolve),
+ );
+
+ const model = target.model(SDK.DOMModel.DOMModel);
+ assert.exists(model);
+ const node = new SDK.DOMModel.DOMNode(model);
+ sinon.stub(node, 'nodeType').returns(Node.TEXT_NODE);
+
+ UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
+ await tableUpdatedPromise;
+
+ const details = view.contentElement.querySelector('.specific-details');
+ assert.exists(details, 'The details element doesn\'t exist');
+ assert.instanceOf(details, HTMLDetailsElement, 'The details element is not an instance of HTMLDetailsElement');
+ assert.isTrue(details.hidden, 'The details element is not hidden');
+ });
+
+ it('Hidden state for not supported element type', async () => {
+ view = new Elements.ElementStatePaneWidget.ElementStatePaneWidget();
+ const tableUpdatedPromise = new Promise<void>(
+ resolve => sinon.stub(view, 'updateElementSpecificStatesTableForTest').callsFake(resolve),
+ );
+
+ const model = target.model(SDK.DOMModel.DOMModel);
+ assert.exists(model);
+ const node = new SDK.DOMModel.DOMNode(model);
+ sinon.stub(node, 'nodeName').returns('not supported');
+ sinon.stub(node, 'enclosingElementOrSelf').returns(node);
+ sinon.stub(node, 'callFunction').resolves({value: false});
+
+ UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
+ await tableUpdatedPromise;
+
+ const details = view.contentElement.querySelector('.specific-details');
+ assert.exists(details, 'The details element doesn\'t exist');
+ assert.instanceOf(details, HTMLDetailsElement, 'The details element is not an instance of HTMLDetailsElement');
+ assert.isTrue(details.hidden, 'The details element is not hidden');
+ });
+
+ it('Shows the specific pseudo-classes for input', async () => {
+ await assertExpectedPseudoClasses(
+ 'input',
+ [
+ 'enabled',
+ 'disabled',
+ 'valid',
+ 'invalid',
+ 'user-valid',
+ 'user-invalid',
+ 'required',
+ 'optional',
+ 'read-write',
+ 'placeholder-shown',
+ 'autofill',
+ ],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for button', async () => {
+ await assertExpectedPseudoClasses(
+ 'button',
+ ['enabled', 'disabled', 'valid', 'invalid', 'read-only'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for fieldset', async () => {
+ await assertExpectedPseudoClasses(
+ 'fieldset',
+ ['enabled', 'disabled', 'valid', 'invalid', 'read-only'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for textarea', async () => {
+ await assertExpectedPseudoClasses(
+ 'textarea',
+ [
+ 'enabled',
+ 'disabled',
+ 'valid',
+ 'invalid',
+ 'user-valid',
+ 'user-invalid',
+ 'required',
+ 'optional',
+ 'read-write',
+ 'placeholder-shown',
+ ],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for select', async () => {
+ await assertExpectedPseudoClasses(
+ 'select',
+ ['enabled', 'disabled', 'valid', 'invalid', 'user-valid', 'user-invalid', 'required', 'optional', 'read-only'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for option', async () => {
+ await assertExpectedPseudoClasses(
+ 'option',
+ ['enabled', 'disabled', 'checked', 'read-only'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for optgroup', async () => {
+ await assertExpectedPseudoClasses(
+ 'optgroup',
+ ['enabled', 'disabled', 'read-only'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for FormAssociated', async () => {
+ await assertExpectedPseudoClasses(
+ 'CustomFormAssociatedElement',
+ ['enabled', 'disabled', 'valid', 'invalid'],
+ true,
+ );
+ });
+
+ it('Shows the specific pseudo-classes for object, output and img', async () => {
+ await assertExpectedPseudoClasses(
+ 'object',
+ ['valid', 'invalid'],
+ );
+
+ await assertExpectedPseudoClasses(
+ 'output',
+ ['valid', 'invalid', 'read-only'],
+ );
+
+ await assertExpectedPseudoClasses(
+ 'img',
+ ['valid', 'invalid'],
+ );
+ });
+ it('Shows the specific pseudo-classes for progress', async () => {
+ await assertExpectedPseudoClasses(
+ 'progress',
+ ['read-only', 'indeterminate'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for a and area with href', async () => {
+ await assertExpectedPseudoClasses(
+ 'a',
+ ['visited'],
+ false,
+ ['href', 'www.google.com'],
+ );
+
+ await assertExpectedPseudoClasses(
+ 'a',
+ ['visited'],
+ false,
+ ['href', 'www.google.com'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for radio or checkbox inputs', async () => {
+ await assertExpectedPseudoClasses(
+ 'input',
+ [
+ 'enabled',
+ 'disabled',
+ 'valid',
+ 'invalid',
+ 'user-valid',
+ 'user-invalid',
+ 'required',
+ 'optional',
+ 'read-write',
+ 'placeholder-shown',
+ 'autofill',
+ 'checked',
+ 'indeterminate',
+ ],
+ false,
+ ['type', 'checkbox'],
+ );
+ await assertExpectedPseudoClasses(
+ 'input',
+ [
+ 'enabled',
+ 'disabled',
+ 'valid',
+ 'invalid',
+ 'user-valid',
+ 'user-invalid',
+ 'required',
+ 'optional',
+ 'read-write',
+ 'placeholder-shown',
+ 'autofill',
+ 'checked',
+ 'indeterminate',
+ ],
+ false,
+ ['type', 'radio'],
+ );
+ });
+
+ it('Shows the specific pseudo-classes for datalist, label, legend and meter', async () => {
+ await assertExpectedPseudoClasses(
+ 'datalist',
+ ['read-only'],
+ );
+ await assertExpectedPseudoClasses(
+ 'label',
+ ['read-only'],
+ );
+ await assertExpectedPseudoClasses(
+ 'legend',
+ ['read-only'],
+ );
+ await assertExpectedPseudoClasses(
+ 'meter',
+ ['read-only'],
+ );
+ });
+});
diff --git a/front_end/panels/elements/ElementStatePaneWidget.ts b/front_end/panels/elements/ElementStatePaneWidget.ts
index deb4e0f..bec9fec 100644
--- a/front_end/panels/elements/ElementStatePaneWidget.ts
+++ b/front_end/panels/elements/ElementStatePaneWidget.ts
@@ -33,13 +33,41 @@
* @description Explanation text for the 'Emulate a focused page' setting in the Rendering tool.
*/
emulatesAFocusedPage: 'Keep page focused. Commonly used for debugging disappearing elements.',
+ /**
+ * @description Similar with forceElementState but allows users to force specific state of the selected element.
+ */
+ forceElementSpecificStates: 'Force specific element state',
};
const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementStatePaneWidget.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
+enum SpecificPseudoStates {
+ ENABLED = 'enabled',
+ DISABLED = 'disabled',
+ VALID = 'valid',
+ INVALID = 'invalid',
+ USER_VALID = 'user-valid',
+ USER_INVALID = 'user-invalid',
+ REQUIRED = 'required',
+ OPTIONAL = 'optional',
+ READ_ONLY = 'read-only',
+ READ_WRITE = 'read-write',
+ IN_RANGE = 'in-range',
+ OUT_OF_RANGE = 'out-of-range',
+ VISITED = 'visited',
+ CHECKED = 'checked',
+ INDETERMINATE = 'indeterminate',
+ PLACEHOLDER_SHOWN = 'placeholder-shown',
+ AUTOFILL = 'autofill',
+} // TODO(crbug.com/332914922): Also add :link and tests for :visited when the bug is fixed.
+
export class ElementStatePaneWidget extends UI.Widget.Widget {
private readonly inputs: HTMLInputElement[];
private readonly inputStates: WeakMap<HTMLInputElement, string>;
private cssModel?: SDK.CSSModel.CSSModel|null;
+ private specificPseudoStateDivs: Map<SpecificPseudoStates, HTMLDivElement>;
+ private specificHeader: HTMLDetailsElement;
+ private readonly throttler: Common.Throttler.Throttler;
+
constructor() {
super(true);
this.contentElement.className = 'styles-element-state-pane';
@@ -65,17 +93,19 @@
}
node.domModel().cssModel().forcePseudoState(node, state, event.target.checked);
};
- const createElementStateCheckbox = (state: string): Element => {
- const td = document.createElement('td');
+ const createElementStateCheckbox = (state: string): HTMLDivElement => {
+ const div = document.createElement('div');
+ div.id = state;
const label = UI.UIUtils.CheckboxLabel.create(':' + state, undefined, undefined, undefined, true);
const input = label.checkboxElement;
this.inputStates.set(input, state);
input.addEventListener('click', (clickListener as EventListener), false);
input.setAttribute('jslog', `${VisualLogging.toggle().track({change: true}).context(state)}`);
inputs.push(input);
- td.appendChild(label);
- return td;
+ div.appendChild(label);
+ return div;
};
+
const createEmulateFocusedPageCheckbox = (): Element => {
const div = document.createElement('div');
div.classList.add('page-state-checkbox');
@@ -112,22 +142,79 @@
// Populate element states
this.contentElement.appendChild(createSectionHeader(i18nString(UIStrings.forceElementState)));
- const table = document.createElement('table');
- table.classList.add('source-code');
- UI.ARIAUtils.markAsPresentation(table);
+ const persistentContainer = document.createElement('div');
+ persistentContainer.classList.add('source-code');
+ persistentContainer.classList.add('pseudo-states-container');
+ UI.ARIAUtils.markAsPresentation(persistentContainer);
- let tr = table.createChild('tr');
- tr.appendChild(createElementStateCheckbox('active'));
- tr.appendChild(createElementStateCheckbox('hover'));
- tr = table.createChild('tr');
- tr.appendChild(createElementStateCheckbox('focus'));
- tr.appendChild(createElementStateCheckbox('visited'));
- tr = table.createChild('tr');
- tr.appendChild(createElementStateCheckbox('focus-within'));
- tr.appendChild(createElementStateCheckbox('focus-visible'));
- tr = table.createChild('tr');
- tr.appendChild(createElementStateCheckbox('target'));
- this.contentElement.appendChild(table);
+ persistentContainer.appendChild(createElementStateCheckbox('active'));
+ persistentContainer.appendChild(createElementStateCheckbox('hover'));
+ persistentContainer.appendChild(createElementStateCheckbox('focus'));
+ persistentContainer.appendChild(createElementStateCheckbox('focus-within'));
+ persistentContainer.appendChild(createElementStateCheckbox('focus-visible'));
+ persistentContainer.appendChild(createElementStateCheckbox('target'));
+ this.contentElement.appendChild(persistentContainer);
+
+ const elementSpecificContainer = document.createElement('div');
+ elementSpecificContainer.classList.add('source-code');
+ elementSpecificContainer.classList.add('pseudo-states-container');
+ elementSpecificContainer.classList.add('specific-pseudo-states');
+ UI.ARIAUtils.markAsPresentation(elementSpecificContainer);
+
+ this.specificPseudoStateDivs = new Map<SpecificPseudoStates, HTMLDivElement>();
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.ENABLED, createElementStateCheckbox(SpecificPseudoStates.ENABLED));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.DISABLED, createElementStateCheckbox(SpecificPseudoStates.DISABLED));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.VALID, createElementStateCheckbox(SpecificPseudoStates.VALID));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.INVALID, createElementStateCheckbox(SpecificPseudoStates.INVALID));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.USER_VALID, createElementStateCheckbox(SpecificPseudoStates.USER_VALID));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.USER_INVALID, createElementStateCheckbox(SpecificPseudoStates.USER_INVALID));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.REQUIRED, createElementStateCheckbox(SpecificPseudoStates.REQUIRED));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.OPTIONAL, createElementStateCheckbox(SpecificPseudoStates.OPTIONAL));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.READ_ONLY, createElementStateCheckbox(SpecificPseudoStates.READ_ONLY));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.READ_WRITE, createElementStateCheckbox(SpecificPseudoStates.READ_WRITE));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.IN_RANGE, createElementStateCheckbox(SpecificPseudoStates.IN_RANGE));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.OUT_OF_RANGE, createElementStateCheckbox(SpecificPseudoStates.OUT_OF_RANGE));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.VISITED, createElementStateCheckbox(SpecificPseudoStates.VISITED));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.CHECKED, createElementStateCheckbox(SpecificPseudoStates.CHECKED));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.INDETERMINATE, createElementStateCheckbox(SpecificPseudoStates.INDETERMINATE));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.PLACEHOLDER_SHOWN, createElementStateCheckbox(SpecificPseudoStates.PLACEHOLDER_SHOWN));
+ this.specificPseudoStateDivs.set(
+ SpecificPseudoStates.AUTOFILL, createElementStateCheckbox(SpecificPseudoStates.AUTOFILL));
+
+ this.specificPseudoStateDivs.forEach(div => {
+ elementSpecificContainer.appendChild(div);
+ });
+
+ this.specificHeader = document.createElement('details');
+ this.specificHeader.classList.add('specific-details');
+
+ const sectionHeaderContainer = document.createElement('summary');
+ sectionHeaderContainer.classList.add('force-specific-element-header');
+ sectionHeaderContainer.classList.add('section-header');
+ UI.UIUtils.createTextChild(
+ sectionHeaderContainer.createChild('span'), i18nString(UIStrings.forceElementSpecificStates));
+
+ this.specificHeader.appendChild(sectionHeaderContainer);
+ this.specificHeader.appendChild(elementSpecificContainer);
+ this.contentElement.appendChild(this.specificHeader);
+
+ this.throttler = new Common.Throttler.Throttler(100);
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.update, this);
}
private updateModel(cssModel: SDK.CSSModel.CSSModel|null): void {
@@ -166,9 +253,143 @@
input.checked = false;
}
}
- ButtonProvider.instance().item().setChecked(this.inputs.some(input => input.checked));
+ void this.throttler.schedule(this.updateElementSpecificStatesTable.bind(this, node));
+ ButtonProvider.instance().item().setToggled(this.inputs.some(input => input.checked));
+ }
+
+ private async updateElementSpecificStatesTable(node: SDK.DOMModel.DOMNode|null = null): Promise<void> {
+ if (!node || node.nodeType() !== Node.ELEMENT_NODE) {
+ this.specificHeader.hidden = true;
+ this.updateElementSpecificStatesTableForTest();
+ return;
+ }
+ let showedACheckbox = false;
+ const hideSpecificCheckbox = (pseudoClass: SpecificPseudoStates, hide: boolean): void => {
+ const checkbox = this.specificPseudoStateDivs.get(pseudoClass);
+ if (checkbox) {
+ checkbox.hidden = hide;
+ }
+ showedACheckbox = showedACheckbox || !hide;
+ };
+ const isElementOfTypes = (node: SDK.DOMModel.DOMNode, types: string[]): boolean => {
+ return types.includes(node.nodeName()?.toLowerCase());
+ };
+ const isInputWithTypeRadioOrCheckbox = (node: SDK.DOMModel.DOMNode): boolean => {
+ return isElementOfTypes(node, ['input']) &&
+ (node.getAttribute('type') === 'checkbox' || node.getAttribute('type') === 'radio');
+ };
+ // An autonomous custom element is called a form-associated custom element if the element is associated with a custom element definition whose form-associated field is set to true.
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#form-associated-custom-element
+ const isFormAssociatedCustomElement = async(node: SDK.DOMModel.DOMNode): Promise<boolean> => {
+ function getFormAssociatedField(this: HTMLElement): boolean {
+ return ('formAssociated' in this.constructor && this.constructor.formAssociated === true);
+ }
+ const response = await node.callFunction(getFormAssociatedField);
+ return response ? response.value : false;
+ };
+ const isFormAssociated = await isFormAssociatedCustomElement(node);
+
+ if (isElementOfTypes(node, ['button', 'input', 'select', 'textarea', 'optgroup', 'option', 'fieldset']) ||
+ isFormAssociated) {
+ hideSpecificCheckbox(SpecificPseudoStates.ENABLED, false);
+ hideSpecificCheckbox(SpecificPseudoStates.DISABLED, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.ENABLED, true);
+ hideSpecificCheckbox(SpecificPseudoStates.DISABLED, true);
+ }
+
+ if (isElementOfTypes(node, ['button', 'fieldset', 'input', 'object', 'output', 'select', 'textarea', 'img']) ||
+ isFormAssociated) {
+ hideSpecificCheckbox(SpecificPseudoStates.VALID, false);
+ hideSpecificCheckbox(SpecificPseudoStates.INVALID, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.VALID, true);
+ hideSpecificCheckbox(SpecificPseudoStates.INVALID, true);
+ }
+
+ if (isElementOfTypes(node, ['input', 'select', 'textarea'])) {
+ hideSpecificCheckbox(SpecificPseudoStates.USER_VALID, false);
+ hideSpecificCheckbox(SpecificPseudoStates.USER_INVALID, false);
+ hideSpecificCheckbox(SpecificPseudoStates.REQUIRED, false);
+ hideSpecificCheckbox(SpecificPseudoStates.OPTIONAL, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.USER_VALID, true);
+ hideSpecificCheckbox(SpecificPseudoStates.USER_INVALID, true);
+ hideSpecificCheckbox(SpecificPseudoStates.REQUIRED, true);
+ hideSpecificCheckbox(SpecificPseudoStates.OPTIONAL, true);
+ }
+
+ if (isElementOfTypes(node, ['input', 'textarea'])) {
+ hideSpecificCheckbox(SpecificPseudoStates.READ_WRITE, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.READ_WRITE, true);
+ }
+
+ if (isElementOfTypes(node, [
+ 'button',
+ 'datalist',
+ 'fieldset',
+ 'label',
+ 'legend',
+ 'meter',
+ 'optgroup',
+ 'option',
+ 'output',
+ 'progress',
+ 'select',
+ ])) {
+ hideSpecificCheckbox(SpecificPseudoStates.READ_ONLY, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.READ_ONLY, true);
+ }
+
+ if (isElementOfTypes(node, ['input']) &&
+ (node.getAttribute('min') !== undefined || node.getAttribute('max') !== undefined)) {
+ hideSpecificCheckbox(SpecificPseudoStates.IN_RANGE, false);
+ hideSpecificCheckbox(SpecificPseudoStates.OUT_OF_RANGE, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.IN_RANGE, true);
+ hideSpecificCheckbox(SpecificPseudoStates.OUT_OF_RANGE, true);
+ }
+
+ if (isElementOfTypes(node, ['a', 'area']) && node.getAttribute('href') !== undefined) {
+ hideSpecificCheckbox(SpecificPseudoStates.VISITED, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.VISITED, true);
+ }
+
+ if (isInputWithTypeRadioOrCheckbox(node) || isElementOfTypes(node, ['option'])) {
+ hideSpecificCheckbox(SpecificPseudoStates.CHECKED, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.CHECKED, true);
+ }
+
+ if (isInputWithTypeRadioOrCheckbox(node) || isElementOfTypes(node, ['progress'])) {
+ hideSpecificCheckbox(SpecificPseudoStates.INDETERMINATE, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.INDETERMINATE, true);
+ }
+
+ if (isElementOfTypes(node, ['input', 'textarea'])) {
+ hideSpecificCheckbox(SpecificPseudoStates.PLACEHOLDER_SHOWN, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.PLACEHOLDER_SHOWN, true);
+ }
+
+ if (isElementOfTypes(node, ['input'])) {
+ hideSpecificCheckbox(SpecificPseudoStates.AUTOFILL, false);
+ } else {
+ hideSpecificCheckbox(SpecificPseudoStates.AUTOFILL, true);
+ }
+
+ this.specificHeader.hidden = showedACheckbox ? false : true;
+ this.updateElementSpecificStatesTableForTest();
+ }
+
+ updateElementSpecificStatesTableForTest(): void {
}
}
+
let buttonProviderInstance: ButtonProvider;
export class ButtonProvider implements UI.Toolbar.Provider {
private readonly button: UI.Toolbar.ToolbarToggle;
diff --git a/front_end/panels/elements/elementStatePaneWidget.css b/front_end/panels/elements/elementStatePaneWidget.css
index c01f540..1d61359 100644
--- a/front_end/panels/elements/elementStatePaneWidget.css
+++ b/front_end/panels/elements/elementStatePaneWidget.css
@@ -20,7 +20,7 @@
gap: 2px;
}
-.styles-element-state-pane > .section-header {
+.styles-element-state-pane .section-header {
margin: 8px 4px 6px;
color: var(--color-text-secondary);
}
@@ -33,3 +33,15 @@
.styles-element-state-pane td {
padding: 0;
}
+
+.pseudo-states-container {
+ display: grid;
+ column-gap: 12px;
+ grid-template-columns: repeat(2, 1fr);
+ grid-auto-flow: row;
+}
+
+.pseudo-states-container.specific-pseudo-states {
+ grid-template-columns: repeat(2, 1fr);
+ margin-bottom: 4px;
+}
diff --git a/front_end/ui/visual_logging/KnownContextValues.ts b/front_end/ui/visual_logging/KnownContextValues.ts
index 081b6f8..463601e 100644
--- a/front_end/ui/visual_logging/KnownContextValues.ts
+++ b/front_end/ui/visual_logging/KnownContextValues.ts
@@ -451,6 +451,7 @@
'changes.copy',
'changes.reveal-source',
'changes.revert',
+ 'checked',
'chrome-ai',
'chrome-android-mobile',
'chrome-android-mobile-high-end',
@@ -873,6 +874,7 @@
'disable-locale-info-bar',
'disable-paused-state-overlay',
'disable-self-xss-warning',
+ 'disabled',
'disconnect-from-network',
'display',
'display-override',
@@ -1022,6 +1024,7 @@
'enable-file-breakpoints',
'enable-header-overrides',
'enable-ignore-listing',
+ 'enabled',
'end-time',
'ended',
'endpoints',
@@ -1330,6 +1333,8 @@
'import-recording',
'important',
'important-dom-properties',
+ 'in-range',
+ 'indeterminate',
'indexed-db',
'indexed-db-data-view',
'indexed-db-database',
@@ -1398,6 +1403,7 @@
'internet-explorer-8',
'internet-explorer-9',
'interpolate-size',
+ 'invalid',
'invert-filter',
'is',
'is-landscape',
@@ -1873,6 +1879,7 @@
'opera-presto-windows',
'opera-windows',
'operator',
+ 'optional',
'or',
'order',
'origin',
@@ -1880,6 +1887,7 @@
'orphans',
'other',
'other-origin',
+ 'out-of-range',
'outermost-target-selector',
'outline',
'outline-color',
@@ -1957,6 +1965,7 @@
'picture-in-picture',
'pixel-7',
'pl',
+ 'placeholder-shown',
'platform',
'platform-version',
'play',
@@ -2074,6 +2083,8 @@
'ratechange',
'raw-headers',
'raw-headers-show-more',
+ 'read-only',
+ 'read-write',
'reading-flow',
'readystatechange',
'rec-2020',
@@ -2155,6 +2166,7 @@
'request-headers',
'request-payload',
'request-types',
+ 'required',
'reset',
'reset-children',
'reset-columns',
@@ -2822,11 +2834,14 @@
'user-agent',
'user-flow-name',
'user-handle',
+ 'user-invalid',
'user-select',
'user-shortcuts',
+ 'user-valid',
'user-verification',
'userHandle',
'uz',
+ 'valid',
'value',
'value-1',
'value-2',
diff --git a/test/e2e/helpers/elements-helpers.ts b/test/e2e/helpers/elements-helpers.ts
index fa8b05e..885be6d 100644
--- a/test/e2e/helpers/elements-helpers.ts
+++ b/test/e2e/helpers/elements-helpers.ts
@@ -431,7 +431,6 @@
veImpression('Toggle: focus-within'),
veImpression('Toggle: hover'),
veImpression('Toggle: target'),
- veImpression('Toggle: visited'),
])]),
veChange(`Panel: elements > Pane: styles > Pane: element-states > Toggle: ${
pseudoState === 'Emulate a focused page' ? 'emulate-page-focus' : pseudoState.substr(1)}`),