From 3f300066af954fe36486dbcf08f838d4b97fe246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:33:51 +0100 Subject: [PATCH 01/10] Add `isInert` method to `Element` --- packages/alfa-dom/src/node/element.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/alfa-dom/src/node/element.ts b/packages/alfa-dom/src/node/element.ts index 74646e69e4..dd915f83e7 100644 --- a/packages/alfa-dom/src/node/element.ts +++ b/packages/alfa-dom/src/node/element.ts @@ -268,6 +268,31 @@ export class Element return None; } + /** + * Computes inertness of an element based on the `inert` attribute. + * + * {@link https://html.spec.whatwg.org/#the-inert-attribute} + * + * @privateRemarks + * According to {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert} + * only open dialogs can escape inertness (except when they have the `inert` attribute). + */ + public isInert(): boolean { + if (this._isInert === undefined) { + if (this.attribute("inert").isSome()) { + this._isInert = true; + } else if (this.name === "dialog" && this.attribute("open").isSome()) { + this._isInert = false; + } else { + this._isInert = this.ancestors(Node.flatTree) + .find(Element.isElement) + .map((parent) => parent.isInert()) + .getOr(false); + } + } + return this._isInert; + } + /* * This collects caches for methods that are specific to some kind of elements. * The actual methods are declared in element/augment.ts to de-clutter this @@ -279,6 +304,7 @@ export class Element protected _inputType: helpers.InputType | undefined; protected _displaySize: number | undefined; protected _optionsList: Sequence> | undefined; + private _isInert: boolean | undefined; /* * End of caches for methods specific to some kind of elements. From 7b0dd460736a3f3b4f534374e65c76f47bf555b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:36:47 +0100 Subject: [PATCH 02/10] Update `isInert` predicate to include call to `Element#isInert` --- .../src/element/predicate/is-inert.ts | 16 ++- .../test/element/predicate/is-inert.spec.tsx | 117 ++++++++++++++++++ packages/alfa-style/test/tsconfig.json | 1 + 3 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 packages/alfa-style/test/element/predicate/is-inert.spec.tsx diff --git a/packages/alfa-style/src/element/predicate/is-inert.ts b/packages/alfa-style/src/element/predicate/is-inert.ts index 31784e4f0f..a87ea44bf2 100644 --- a/packages/alfa-style/src/element/predicate/is-inert.ts +++ b/packages/alfa-style/src/element/predicate/is-inert.ts @@ -1,19 +1,25 @@ import type { Device } from "@siteimprove/alfa-device"; -import type { Element } from "@siteimprove/alfa-dom"; -import type { Predicate } from "@siteimprove/alfa-predicate"; +import { Element } from "@siteimprove/alfa-dom"; +import { Predicate } from "@siteimprove/alfa-predicate"; import { Style } from "../../style.js"; +const { or } = Predicate; + /** * {@link https://html.spec.whatwg.org/#inert} * * @public */ -export function isInert(device: Device): Predicate { - return Style.hasComputedStyle( +export const isInert = (device: Device) => + or(isInertFromVisibility(device), isInertFromAttribute); + +const isInertFromVisibility = (device: Device) => + Style.hasComputedStyle( "visibility", (specified) => specified.value === "hidden" || specified.value === "collapse", device, ); -} + +const isInertFromAttribute = (element: Element) => element.isInert(); diff --git a/packages/alfa-style/test/element/predicate/is-inert.spec.tsx b/packages/alfa-style/test/element/predicate/is-inert.spec.tsx new file mode 100644 index 0000000000..774943ade8 --- /dev/null +++ b/packages/alfa-style/test/element/predicate/is-inert.spec.tsx @@ -0,0 +1,117 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Device } from "@siteimprove/alfa-device"; +import { h } from "@siteimprove/alfa-dom/h"; + +import { isInert } from "../../../src/element/predicate/is-inert.js"; + +const device = Device.standard(); +const inert = isInert(device); + +test("isInert() returns false for elements with visibility: visible", (t) => { + t.equal(inert(
), false); +}); + +test("isInert() returns true for elements with visibility: hidden", (t) => { + t.equal(inert(
), true); +}); + +test("isInert() returns true for elements with visibility: collapse", (t) => { + t.equal(inert(
), true); +}); + +test("isInert() returns false by default", (t) => { + t.equal(inert(
), false); +}); + +test("isInert() returns true for elements inheriting visibility: hidden", (t) => { + const child =
; + const parent =
{child}
; + + h.document([parent]); + + t.equal(inert(child), true); +}); + +test("isInert() returns true when stylesheet applies visibility: hidden", (t) => { + const element =