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)}`),