From 560b81ddd7bc60cf3dc72d145d636c17c053b4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:40:09 +0100 Subject: [PATCH 01/36] Skaffold new package --- packages/alfa-painting-order/CHANGELOG.md | 0 packages/alfa-painting-order/README.md | 3 ++ .../config/api-extractor.json | 5 +++ packages/alfa-painting-order/package.json | 31 +++++++++++++++++++ packages/alfa-painting-order/src/index.ts | 0 .../alfa-painting-order/src/tsconfig.json | 6 ++++ .../alfa-painting-order/test/tsconfig.json | 7 +++++ packages/alfa-painting-order/tsconfig.json | 6 ++++ packages/tsconfig.json | 1 + 9 files changed, 59 insertions(+) create mode 100644 packages/alfa-painting-order/CHANGELOG.md create mode 100644 packages/alfa-painting-order/README.md create mode 100644 packages/alfa-painting-order/config/api-extractor.json create mode 100644 packages/alfa-painting-order/package.json create mode 100644 packages/alfa-painting-order/src/index.ts create mode 100644 packages/alfa-painting-order/src/tsconfig.json create mode 100644 packages/alfa-painting-order/test/tsconfig.json create mode 100644 packages/alfa-painting-order/tsconfig.json diff --git a/packages/alfa-painting-order/CHANGELOG.md b/packages/alfa-painting-order/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-painting-order/README.md b/packages/alfa-painting-order/README.md new file mode 100644 index 0000000000..2960a1166f --- /dev/null +++ b/packages/alfa-painting-order/README.md @@ -0,0 +1,3 @@ +# Alfa painting order + +Support for computing painting order of elements. diff --git a/packages/alfa-painting-order/config/api-extractor.json b/packages/alfa-painting-order/config/api-extractor.json new file mode 100644 index 0000000000..7c547d46a6 --- /dev/null +++ b/packages/alfa-painting-order/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../../config/api-extractor.json", + "mainEntryPointFilePath": "/dist/index.d.ts" +} diff --git a/packages/alfa-painting-order/package.json b/packages/alfa-painting-order/package.json new file mode 100644 index 0000000000..1c7f811085 --- /dev/null +++ b/packages/alfa-painting-order/package.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/package", + "name": "@siteimprove/alfa-painting-order", + "homepage": "https://alfa.siteimprove.com", + "version": "0.97.0", + "license": "MIT", + "description": "Support for computing painting order of elements.", + "repository": { + "type": "git", + "url": "github:Siteimprove/alfa", + "directory": "packages/alfa-painting-order" + }, + "bugs": "https://github.com/siteimprove/alfa/issues", + "engines": { + "node": ">=20.0.0" + }, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "devDependencies": { + "@siteimprove/alfa-test": "workspace:^" + }, + "publishConfig": { + "access": "public", + "registry": "https://npm.pkg.github.com/" + } +} diff --git a/packages/alfa-painting-order/src/index.ts b/packages/alfa-painting-order/src/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json new file mode 100644 index 0000000000..9364627c60 --- /dev/null +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "compilerOptions": { "outDir": "../dist" }, + "include": ["./index.ts"] +} diff --git a/packages/alfa-painting-order/test/tsconfig.json b/packages/alfa-painting-order/test/tsconfig.json new file mode 100644 index 0000000000..3390a535e9 --- /dev/null +++ b/packages/alfa-painting-order/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "compilerOptions": { "noEmit": true }, + "files": [], + "references": [{ "path": "../src" }, { "path": "../../alfa-test" }] +} diff --git a/packages/alfa-painting-order/tsconfig.json b/packages/alfa-painting-order/tsconfig.json new file mode 100644 index 0000000000..398e0fba97 --- /dev/null +++ b/packages/alfa-painting-order/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "files": [], + "references": [{ "path": "./src" }, { "path": "./test" }] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 14c550ebb9..2e7b2cddc4 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -48,6 +48,7 @@ { "path": "alfa-monad" }, { "path": "alfa-network" }, { "path": "alfa-option" }, + { "path": "alfa-painting-order" }, { "path": "alfa-parser" }, { "path": "alfa-performance" }, { "path": "alfa-predicate" }, From cf3ffa075fef7cd01db9c8e01ac53fe1902bd976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:11:36 +0100 Subject: [PATCH 02/36] Add predicate for checking if an element creates a stacking context --- packages/alfa-painting-order/package.json | 7 + .../src/predicate/creates-stacking-context.ts | 120 +++++++++ .../alfa-painting-order/src/tsconfig.json | 2 +- .../creates-stacking-context.spec.tsx | 239 ++++++++++++++++++ .../alfa-painting-order/test/tsconfig.json | 8 +- packages/alfa-style/src/element/element.ts | 1 + .../predicate/has-initial-computed-style.ts | 24 ++ packages/alfa-style/src/style.ts | 1 + packages/alfa-style/src/tsconfig.json | 1 + yarn.lock | 13 + 10 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 packages/alfa-painting-order/src/predicate/creates-stacking-context.ts create mode 100644 packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx create mode 100644 packages/alfa-style/src/element/predicate/has-initial-computed-style.ts diff --git a/packages/alfa-painting-order/package.json b/packages/alfa-painting-order/package.json index 1c7f811085..51278ddf7d 100644 --- a/packages/alfa-painting-order/package.json +++ b/packages/alfa-painting-order/package.json @@ -21,6 +21,13 @@ "dist/**/*.js", "dist/**/*.d.ts" ], + "dependencies": { + "@siteimprove/alfa-css": "workspace:^", + "@siteimprove/alfa-device": "workspace:^", + "@siteimprove/alfa-dom": "workspace:^", + "@siteimprove/alfa-refinement": "workspace:^", + "@siteimprove/alfa-style": "workspace:^" + }, "devDependencies": { "@siteimprove/alfa-test": "workspace:^" }, diff --git a/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts b/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts new file mode 100644 index 0000000000..841bbb3748 --- /dev/null +++ b/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts @@ -0,0 +1,120 @@ +import { Keyword } from "@siteimprove/alfa-css"; +import { Device } from "@siteimprove/alfa-device"; +import { Element, Node } from "@siteimprove/alfa-dom"; +import { Refinement } from "@siteimprove/alfa-refinement"; +import { Style } from "@siteimprove/alfa-style"; + +const { and, not, or } = Refinement; +const { hasComputedStyle, hasInitialComputedStyle, isPositioned } = Style; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context} + * + * @remarks + * This doesn't cover the following known cases, either because we haven't + * implemented support for the CSS property or because it's otherwise not + * feasible to cover the case. In the future, new properties may be added that + * also create stacking contexts. We will have to update this predicate as such + * new properties become supported by the browsers. + * + * The properties `filter`, `backdrop-filter` and `mask-border` having + * non-initial values: Support for the properties are not yet implemented in Alfa. + * + * Elements placed into the top layer and its corresponding ::backdrop e.g. + * fullscreen and popover elements: It's unclear how to implement this and it's + * not the most important use case currently. + * + * Element that has had stacking context-creating properties animated using + * @keyframes, with `animation-fill-mode` set to `forwards`: It's unclear how + * to implement this, but it is a valid case, that we should eventually support. + * + * @internal + */ +export function createsStackingContext(device: Device) { + const hasZIndex = not(hasInitialComputedStyle("z-index", device)); + + return or( + // positioned with z-index: + and(isPositioned(device, "absolute", "relative"), hasZIndex), + + // fixed or sticky: + isPositioned(device, "fixed", "sticky"), + + // child of flex or grid: + and(hasZIndex, (element: Element) => + element + .parent(Node.fullTree) + .filter(Element.isElement) + .some((parent) => + hasComputedStyle( + "display", + ({ values: [, inside] }) => + inside?.value === "flex" || inside?.value === "grid", + device, + )(parent), + ), + ), + + // opacity < 1: + hasComputedStyle("opacity", ({ value: opacity }) => opacity < 1, device), + + // non-initial properties: + not(hasInitialComputedStyle("mix-blend-mode", device)), + not(hasInitialComputedStyle("transform", device)), + not(hasInitialComputedStyle("scale", device)), + not(hasInitialComputedStyle("rotate", device)), + not(hasInitialComputedStyle("translate", device)), + not(hasInitialComputedStyle("perspective", device)), + not(hasInitialComputedStyle("clip-path", device)), + not(hasInitialComputedStyle("mask-clip", device)), + not(hasInitialComputedStyle("mask-composite", device)), + not(hasInitialComputedStyle("mask-mode", device)), + not(hasInitialComputedStyle("mask-origin", device)), + not(hasInitialComputedStyle("mask-position", device)), + not(hasInitialComputedStyle("mask-repeat", device)), + not(hasInitialComputedStyle("mask-size", device)), + not(hasInitialComputedStyle("mask-image", device)), + + // isolation: isolate + hasComputedStyle("isolation", ({ value }) => value === "isolate", device), + + // will-change: specifying any property that would create a stacking context + // on non-initial value + hasComputedStyle( + "will-change", + (value) => + !Keyword.isKeyword(value) && + value.values.some(({ value }) => + [ + "position", + "z-index", + "opacity", + "mix-blend-mode", + "transform", + "scale", + "rotate", + "translate", + "filter", + "backdrop-filter", + "perspective", + "clip-path", + "mask", + "isolation", + ].includes(value), + ), + + device, + ), + + // contain: strict | content | layout | paint + hasComputedStyle( + "contain", + (value) => { + return Keyword.isKeyword(value) + ? value.is("strict", "content") + : value.layout || value.paint; + }, + device, + ), + ); +} diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index 9364627c60..7392021ee8 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -2,5 +2,5 @@ "$schema": "http://json.schemastore.org/tsconfig", "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../dist" }, - "include": ["./index.ts"] + "include": ["./index.ts", "./predicate/creates-stacking-context.ts"] } diff --git a/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx b/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx new file mode 100644 index 0000000000..1b2e67c071 --- /dev/null +++ b/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx @@ -0,0 +1,239 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Device } from "@siteimprove/alfa-device"; + +import { createsStackingContext } from "../../dist/predicate/creates-stacking-context.js"; + +test("non positioned element with z-index does not create a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + false, + ); +}); + +test("absolutely positioned element without z-index does not create a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + false, + ); +}); + +test("relatively positioned element without z-index does not create a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + false, + ); +}); + +test("element with opacity equal to 1 does not create a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + false, + ); +}); + +test("absolutely positioned element with z-index creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("relatively positioned element with z-index creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("fixed element creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("sticky element creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("flex child with z-index creates a stacking context", async (t) => { + const child =
; +
{child}
; + + t.equal(createsStackingContext(Device.standard())(child), true); +}); + +test("grid child with z-index creates a stacking context", async (t) => { + const child =
; +
{child}
; + + t.equal(createsStackingContext(Device.standard())(child), true); +}); + +test("element with opacity less than 1 creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with mix-blend-mode equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with transform equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with scale equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with rotate equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with translate equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with perspective equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with clip-path equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with mask equal to non-initial value creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with isolation equal to isolate creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with will-change specifying a property that would create a stacking context on non-initial value, creates a stacking context", async (t) => { + for (const prop of [ + "mix-blend-mode", + "transform", + "scale", + "rotate", + "translate", + "filter", + "backdrop-filter", + "perspective", + "clip-path", + "mask", + ]) { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); + } +}); + +test("element with contain equal to layout creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with contain equal to paint creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with contain equal to strict creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); + +test("element with contain equal to content creates a stacking context", async (t) => { + t.equal( + createsStackingContext(Device.standard())( +
, + ), + true, + ); +}); diff --git a/packages/alfa-painting-order/test/tsconfig.json b/packages/alfa-painting-order/test/tsconfig.json index 3390a535e9..7bf1f920f5 100644 --- a/packages/alfa-painting-order/test/tsconfig.json +++ b/packages/alfa-painting-order/test/tsconfig.json @@ -1,7 +1,11 @@ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "../tsconfig.json", - "compilerOptions": { "noEmit": true }, - "files": [], + "compilerOptions": { + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "@siteimprove/alfa-dom" + }, + "files": ["./predicate/creates-stacking-context.spec.tsx"], "references": [{ "path": "../src" }, { "path": "../../alfa-test" }] } diff --git a/packages/alfa-style/src/element/element.ts b/packages/alfa-style/src/element/element.ts index fb9b267ff8..90fe0e3113 100755 --- a/packages/alfa-style/src/element/element.ts +++ b/packages/alfa-style/src/element/element.ts @@ -4,6 +4,7 @@ export * from "./predicate/has-border.js"; export * from "./predicate/has-box-shadow.js"; export * from "./predicate/has-cascaded-style.js"; export * from "./predicate/has-computed-style.js"; +export * from "./predicate/has-initial-computed-style.js"; export * from "./predicate/has-outline.js"; export * from "./predicate/has-positioning-parent.js"; export * from "./predicate/has-specified-style.js"; diff --git a/packages/alfa-style/src/element/predicate/has-initial-computed-style.ts b/packages/alfa-style/src/element/predicate/has-initial-computed-style.ts new file mode 100644 index 0000000000..125580c720 --- /dev/null +++ b/packages/alfa-style/src/element/predicate/has-initial-computed-style.ts @@ -0,0 +1,24 @@ +import type { Device } from "@siteimprove/alfa-device"; +import type { Text } from "@siteimprove/alfa-dom"; +import { Element } from "@siteimprove/alfa-dom"; +import type { Predicate } from "@siteimprove/alfa-predicate"; +import type { Context } from "@siteimprove/alfa-selector"; + +import { hasComputedStyle } from "./has-computed-style.js"; +import { Longhands } from "../../longhands.js"; + +/** + * @public + */ +export function hasInitialComputedStyle( + name: N, + device: Device, + context?: Context, +): Predicate { + return hasComputedStyle( + name, + (value) => value.equals(Longhands.get(name).initial), + device, + context, + ); +} diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index 7f87a343d1..f1d8848edd 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -483,6 +483,7 @@ export namespace Style { hasBoxShadow, hasCascadedStyle, hasComputedStyle, + hasInitialComputedStyle, hasPositioningParent, hasOutline, hasSpecifiedStyle, diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 80e803580f..f987f817c8 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -10,6 +10,7 @@ "./element/predicate/has-box-shadow.ts", "./element/predicate/has-cascaded-style.ts", "./element/predicate/has-computed-style.ts", + "./element/predicate/has-initial-computed-style.ts", "./element/predicate/has-outline.ts", "./element/predicate/has-positioning-parent.ts", "./element/predicate/has-specified-style.ts", diff --git a/yarn.lock b/yarn.lock index 6e26555c4b..d8b0af07dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1706,6 +1706,19 @@ __metadata: languageName: unknown linkType: soft +"@siteimprove/alfa-painting-order@workspace:packages/alfa-painting-order": + version: 0.0.0-use.local + resolution: "@siteimprove/alfa-painting-order@workspace:packages/alfa-painting-order" + dependencies: + "@siteimprove/alfa-css": "workspace:^" + "@siteimprove/alfa-device": "workspace:^" + "@siteimprove/alfa-dom": "workspace:^" + "@siteimprove/alfa-refinement": "workspace:^" + "@siteimprove/alfa-style": "workspace:^" + "@siteimprove/alfa-test": "workspace:^" + languageName: unknown + linkType: soft + "@siteimprove/alfa-parser@workspace:^, @siteimprove/alfa-parser@workspace:packages/alfa-parser": version: 0.0.0-use.local resolution: "@siteimprove/alfa-parser@workspace:packages/alfa-parser" From 4d9d04df63d13fb9d4e9d788a844a1fcefa9c2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:42:57 +0100 Subject: [PATCH 03/36] Add function to compute the painting order of elements --- packages/alfa-painting-order/package.json | 4 + packages/alfa-painting-order/src/index.ts | 1 + .../alfa-painting-order/src/painting-order.ts | 241 ++++++++++ .../src/predicate/creates-stacking-context.ts | 17 +- .../src/predicate/is-flex-or-grid-child.ts | 16 + .../alfa-painting-order/src/tsconfig.json | 7 +- .../test/painting-order.spec.tsx | 425 ++++++++++++++++++ .../creates-stacking-context.spec.tsx | 50 +-- .../alfa-painting-order/test/tsconfig.json | 5 +- yarn.lock | 4 + 10 files changed, 729 insertions(+), 41 deletions(-) create mode 100644 packages/alfa-painting-order/src/painting-order.ts create mode 100644 packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts create mode 100644 packages/alfa-painting-order/test/painting-order.spec.tsx diff --git a/packages/alfa-painting-order/package.json b/packages/alfa-painting-order/package.json index 51278ddf7d..77efdaf917 100644 --- a/packages/alfa-painting-order/package.json +++ b/packages/alfa-painting-order/package.json @@ -22,10 +22,14 @@ "dist/**/*.d.ts" ], "dependencies": { + "@siteimprove/alfa-array": "workspace:^", + "@siteimprove/alfa-cache": "workspace:^", + "@siteimprove/alfa-comparable": "workspace:^", "@siteimprove/alfa-css": "workspace:^", "@siteimprove/alfa-device": "workspace:^", "@siteimprove/alfa-dom": "workspace:^", "@siteimprove/alfa-refinement": "workspace:^", + "@siteimprove/alfa-sequence": "workspace:^", "@siteimprove/alfa-style": "workspace:^" }, "devDependencies": { diff --git a/packages/alfa-painting-order/src/index.ts b/packages/alfa-painting-order/src/index.ts index e69de29bb2..04bb86635d 100644 --- a/packages/alfa-painting-order/src/index.ts +++ b/packages/alfa-painting-order/src/index.ts @@ -0,0 +1 @@ +export * from "./painting-order.js"; diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts new file mode 100644 index 0000000000..5f3fd1bb61 --- /dev/null +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -0,0 +1,241 @@ +import { Array } from "@siteimprove/alfa-array"; +import { Cache } from "@siteimprove/alfa-cache"; +import { Comparable } from "@siteimprove/alfa-comparable"; +import type { Device } from "@siteimprove/alfa-device"; +import { Element, Node } from "@siteimprove/alfa-dom"; +import { Refinement } from "@siteimprove/alfa-refinement"; +import { Sequence } from "@siteimprove/alfa-sequence"; +import { Style } from "@siteimprove/alfa-style"; + +const { and, not, or } = Refinement; +const { hasComputedStyle, isRendered } = Style; + +import { createsStackingContext } from "./predicate/creates-stacking-context.js"; +import { isFlexOrGridChild } from "./predicate/is-flex-or-grid-child.js"; + +const cache = Cache.empty>>(); + +/** + * Computes the painting order of the element descendants of a root element and + * returns said elements in the order in which they would be painted onto the screen. + * + * {@link https://drafts.csswg.org/css2/#elaborate-stacking-contexts} + * + * @remarks + * The painting order of flex children is different from the usual painting + * order, but we currently do not observe these differences. The assumption is + * that flex children, unless they are positioned or has z-index, should never + * overlap, so it's acceptable that we do not get their relative order right as + * long as the flex container itself is ordered correctly. + * {@link https://www.w3.org/TR/css-flexbox-1/#painting}. + * + * @public + */ +export function computePaintingOrder( + root: Element, + device: Device, +): Sequence { + const isPositioned = hasComputedStyle( + "position", + (position) => position.value !== "static", + device, + ); + const hasAutoZIndex = hasComputedStyle( + "z-index", + ({ value }) => value === "auto", + device, + ); + const isBlockLevel = hasComputedStyle( + "display", + ({ values: [outside, inside, listItem] }) => + outside.value === "block" || + inside?.value === "table" || + inside?.value === "flex" || + inside?.value === "grid" || + listItem?.value === "list-item", + device, + ); + const isFloat = hasComputedStyle( + "float", + ({ value }) => value !== "none", + device, + ); + const createsSC = createsStackingContext(device); + const rendered = isRendered(device); + + const getZLevel = (element: Element) => { + // If the element is not positioned and not a flex child, setting a z-index + // wont affect the z-level. + if (and(not(isPositioned), not(isFlexOrGridChild(device)))(element)) { + return 0; + } + + const { + value: { value }, + } = Style.from(element, device).computed("z-index"); + + return value === "auto" ? 0 : value; + }; + + function paint( + element: Element, + canvas: Array, + options: { defer?: boolean } = { + defer: false, + }, + ): void { + const { defer = false } = options; + const positionedOrStackingContexts: Array = []; + const blockLevels: Array = []; + const floats: Array = []; + const inlines: Array = []; + + /** + * @remarks + * Positioned elements with z-index: auto and floating elements are treated + * as if they create stacking contexts, but their positioned descendants + * and descendants that create stacking contexts should be considered part + * of the parent stacking context, i.e. we need to compute the painting + * order of such subtrees, without recursing into positioned descendants + * and descendants creating stacking contexts, then iterate the result and + * distribute said descendants into layers at this level and add the float + * itself and the other descendants to the floats layer. + */ + function distributeIntoLayers(element: Element) { + if (or(isFlexOrGridChild(device), createsSC)(element)) { + positionedOrStackingContexts.push(element); + } else if (isPositioned(element)) { + if (hasAutoZIndex(element)) { + const temporaryLayer: Array = []; + paint(element, temporaryLayer, { defer: true }); + + for (const descendant of temporaryLayer) { + if (or(isPositioned, createsSC)(descendant)) { + if (or(isPositioned, createsSC)(descendant)) { + positionedOrStackingContexts.push(descendant); + } else if (isFloat(descendant)) { + floats.push(descendant); + } else if (isBlockLevel(descendant)) { + blockLevels.push(descendant); + } else { + inlines.push(descendant); + } + } else { + positionedOrStackingContexts.push(descendant); + } + } + } else { + positionedOrStackingContexts.push(element); + } + } else if (isFloat(element)) { + const temporaryLayer: Array = []; + paint(element, temporaryLayer, { defer: true }); + + for (const descendant of temporaryLayer) { + if (or(isPositioned, createsSC)(descendant)) { + positionedOrStackingContexts.push(descendant); + } else { + floats.push(descendant); + } + } + } else if (isBlockLevel(element)) { + blockLevels.push(element); + } else { + // everything else, this is somewhat crude and might not be accurate, but + // will do for now. + inlines.push(element); + } + } + + // Block-level elements, forming a stacking context, are painted before + // their descendants. Inline-level elements, forming a stacking context, + // are painted in the inline layer before its inline descendants + // (and before stacking-context-creating and positioned descendants with + // stack level greater than or equal to 0), but after positioned descendants + // with negative z-index, block-level descendants and floating descendants. + if (isBlockLevel(element)) { + canvas.push(element); + } else { + inlines.push(element); + } + + function traverse(element: Element) { + for (const child of element + .children(Node.fullTree) + .filter(and(Element.isElement, rendered))) { + distributeIntoLayers(child); + + if (or(isPositioned, isFloat, createsSC)(child)) { + // The child is going to be painted in full or partial isolation, so + // we need to stop descending. + continue; + } + + traverse(child); + } + } + traverse(element); + + positionedOrStackingContexts.sort((a: Element, b: Element) => + Comparable.compare(getZLevel(a), getZLevel(b)), + ); + + // If the defer is true, painting of descendant stacking contexts should + // be deferred i.e. the element should just be added to the canvas, (which + // should be a temporary canvas). + let posDescIndex = 0; + for ( + ; + posDescIndex < positionedOrStackingContexts.length && + getZLevel(positionedOrStackingContexts[posDescIndex]) < 0; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); + } + } + + for (const blockLevel of blockLevels) { + if (!defer && createsSC(blockLevel)) { + paint(blockLevel, canvas); + } else { + canvas.push(blockLevel); + } + } + + for (const float of floats) { + if (!defer && float !== element && createsSC(float)) { + paint(float, canvas); + } else { + canvas.push(float); + } + } + + for (const inline of inlines) { + if (!defer && inline !== element && createsSC(inline)) { + paint(inline, canvas); + } else { + canvas.push(inline); + } + } + + for (; posDescIndex < positionedOrStackingContexts.length; ++posDescIndex) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element && createsSC(posOrSC)) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); + } + } + } + + return cache.get(device, Cache.empty).get(root, () => { + const canvas: Array = []; + paint(root, canvas); + + return Sequence.from(canvas); + }); +} diff --git a/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts b/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts index 841bbb3748..f1926fb430 100644 --- a/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts +++ b/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts @@ -1,12 +1,13 @@ import { Keyword } from "@siteimprove/alfa-css"; import { Device } from "@siteimprove/alfa-device"; -import { Element, Node } from "@siteimprove/alfa-dom"; import { Refinement } from "@siteimprove/alfa-refinement"; import { Style } from "@siteimprove/alfa-style"; const { and, not, or } = Refinement; const { hasComputedStyle, hasInitialComputedStyle, isPositioned } = Style; +import { isFlexOrGridChild } from "./is-flex-or-grid-child.js"; + /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context} * @@ -41,19 +42,7 @@ export function createsStackingContext(device: Device) { isPositioned(device, "fixed", "sticky"), // child of flex or grid: - and(hasZIndex, (element: Element) => - element - .parent(Node.fullTree) - .filter(Element.isElement) - .some((parent) => - hasComputedStyle( - "display", - ({ values: [, inside] }) => - inside?.value === "flex" || inside?.value === "grid", - device, - )(parent), - ), - ), + and(hasZIndex, isFlexOrGridChild(device)), // opacity < 1: hasComputedStyle("opacity", ({ value: opacity }) => opacity < 1, device), diff --git a/packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts b/packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts new file mode 100644 index 0000000000..119e0f2f00 --- /dev/null +++ b/packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts @@ -0,0 +1,16 @@ +import { Device } from "@siteimprove/alfa-device"; +import { Element, Node } from "@siteimprove/alfa-dom"; +import { Style } from "@siteimprove/alfa-style"; + +const { isFlexContainer, isGridContainer } = Style; + +export function isFlexOrGridChild(device: Device) { + return (element: Element) => + element + .parent(Node.fullTree) + .filter(Element.isElement) + .some((parent) => { + const style = Style.from(parent, device); + return isFlexContainer(style) || isGridContainer(style); + }); +} diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index 7392021ee8..5e8cc7c305 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -2,5 +2,10 @@ "$schema": "http://json.schemastore.org/tsconfig", "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../dist" }, - "include": ["./index.ts", "./predicate/creates-stacking-context.ts"] + "include": [ + "./index.ts", + "./painting-order.ts", + "./predicate/creates-stacking-context.ts", + "./predicate/is-flex-or-grid-child.ts" + ] } diff --git a/packages/alfa-painting-order/test/painting-order.spec.tsx b/packages/alfa-painting-order/test/painting-order.spec.tsx new file mode 100644 index 0000000000..f832faadc6 --- /dev/null +++ b/packages/alfa-painting-order/test/painting-order.spec.tsx @@ -0,0 +1,425 @@ +import { Device } from "@siteimprove/alfa-device"; +import { h } from "@siteimprove/alfa-dom"; +import { Sequence } from "@siteimprove/alfa-sequence"; +import { test } from "@siteimprove/alfa-test"; + +import { computePaintingOrder } from "../dist/painting-order.js"; + +const device = Device.standard(); + +test("block-level root element is painted before positioned descendant with negative z-index", (t) => { + const negativeZ =
; + const body = {negativeZ}; + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, negativeZ]).toJSON(), + ); +}); + +test("block-level stacking context element is painted before positioned descendant with negative z-index", (t) => { + const negativeZ =
; + const sc =
{negativeZ}
; + const body = {sc}; + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, sc, negativeZ]).toJSON(), + ); +}); + +test("positioned elements with negative z-index are painted in z-order then tree-order", (t) => { + const negativeZ1 =
; + const negativeZ2 =
; + const negativeZ3 =
; + const body = ( + + {negativeZ1} + {negativeZ2} + {negativeZ3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, negativeZ3, negativeZ1, negativeZ2]).toJSON(), + ); +}); + +test("positioned element with negative z-index is painted before block-level element", (t) => { + const negativeZ =
; + const block =
; + const body = ( + + {block} + {negativeZ} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, negativeZ, block]).toJSON(), + ); +}); + +test("block-level descendants are painted in tree-order", (t) => { + const div3 =
3
; + const div2 =
2
; + const div1 =
{div2}
; + const body = ( + + {div1} + {div3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div1, div2, div3]).toJSON(), + ); +}); + +test("block-level elements are painted before floating elements", (t) => { + const div =
; + const float =
; + const body = ( + + {float} + {div} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div, float]).toJSON(), + ); +}); + +test("floating descendants are painted in tree-order", (t) => { + const div3 =
3
; + const div2 =
2
; + const div1 =
{div2}
; + const body = ( + + {div1} + {div3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div1, div2, div3]).toJSON(), + ); +}); + +test("floating descendants are painted atomically with respect to block-level descendants", (t) => { + // This means that, even though block-level elements are painted before + // floating elements in the same stacking context, block-level descendants of + // floating elements are painted after their floating ancestor and as a + // consequence will be painted after block-level descendants outside the float + // even though they appear earlier in tree-order. + const div2 =
2
; + const div1 =
{div2}
; + const div3 =
3
; + const body = ( + + {div1} + {div3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div3, div1, div2]).toJSON(), + ); +}); + +test("floating descendants are not painted atomically with respect to positioned descendants", (t) => { + const div1 =
1
; + const div3 =
3
; + const div2 =
{div3}
; + const body = ( + + {div1} + {div2} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div2, div1, div3]).toJSON(), + ); +}); + +test("floating descendants are painted before inline-level descendants", (t) => { + const inline =
; + const float =
; + const body = ( + + {inline} + {float} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, float, inline]).toJSON(), + ); +}); + +test("inline-level descendants are painted in tree-order", (t) => { + const span3 = 3; + const span2 = 2; + const span1 = {span2}; + const body = ( + + {span1} + {span3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, span1, span2, span3]).toJSON(), + ); +}); + +test("inline-level descendants are painted before positioned elements", (t) => { + const positioned =
; + const inline =
; + const body = ( + + {positioned} + {inline} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, inline, positioned]).toJSON(), + ); +}); + +test("positioned descendants with z-index: auto and non-positioned elements that create stacking contexts are painted in tree-order", (t) => { + const div2 =
2
; + const div1 =
{div2}
; + const div3 =
3
; + const div4 =
4
; + const body = ( + + {div1} + {div3} + {div4} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div1, div2, div3, div4]).toJSON(), + ); +}); + +test("positioned descendants are painted atomically with respect to block-level descendants", (t) => { + // This means that, even though block-level elements are painted before + // positioned elements in the same stacking context, block-level descendants of + // positioned elements are painted after their positioned ancestor and as a + // consequence will be painted after block-level descendants outside the positioned element + // even though they appear earlier in tree-order. + const div2 =
2
; + const div1 =
{div2}
; + const div3 =
3
; + const body = ( + + {div1} + {div3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div3, div1, div2]).toJSON(), + ); +}); + +test("positioned descendants are not painted atomically with respect to positioned descendants with positive z-indices", (t) => { + const div1 =
1
; + const div3 =
3
; + const div2 =
{div3}
; + const body = ( + + {div1} + {div2} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div2, div1, div3]).toJSON(), + ); +}); + +test("positioned elements without z-index are painted before positioned elements with positve z-index", (t) => { + const zIndex =
; + const positioned =
; + const body = ( + + {zIndex} + {positioned} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, positioned, zIndex]).toJSON(), + ); +}); + +test("positioned elements with positive z-index are painted in z-order then tree-order", (t) => { + const positiveZ1 =
; + const positiveZ2 =
; + const positiveZ3 =
; + const body = ( + + {positiveZ1} + {positiveZ2} + {positiveZ3} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, positiveZ2, positiveZ3, positiveZ1]).toJSON(), + ); +}); + +test("inline-level stacking context element is painted after floating descendants and before inline-level descendants", (t) => { + const float =
; + const inlineChild =
; + const inlineStackingContext = ( +
+ {float} + {inlineChild} +
+ ); + const body = {inlineStackingContext}; + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, float, inlineStackingContext, inlineChild]).toJSON(), + ); +}); + +test("stacking context creating elements are painted atomically", (t) => { + // The element with the lower z-index is painted after the element with the + // higher z-index because the element with opacity creates a stacking context + // and therefore is painted atomically before the element with z-index: 2. + const div1 =
; + const div3 =
; + const div2 =
{div3}
; + const body = ( + + {div1} + {div2} + + ); + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div2, div3, div1]).toJSON(), + ); +}); + +test("non-positioned elements are not affected by z-index", (t) => { + const div2 =
; + const div3 =
; + const div1 = ( +
+ {div2} + {div3} +
+ ); + const body = {div1}; + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div1, div2, div3]).toJSON(), + ); +}); + +test("flex children are affected by z-index", (t) => { + const div2 =
; + const div3 =
; + const div1 = ( +
+ {div2} + {div3} +
+ ); + const body = {div1}; + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div1, div3, div2]).toJSON(), + ); +}); + +test("grid children are affected by z-index", (t) => { + const div2 =
; + const div3 =
; + const div1 = ( +
+ {div2} + {div3} +
+ ); + const body = {div1}; + + h.document([body]); + + t.deepEqual( + computePaintingOrder(body, device).toJSON(), + Sequence.from([body, div1, div3, div2]).toJSON(), + ); +}); diff --git a/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx b/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx index 1b2e67c071..320cdb1315 100644 --- a/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx +++ b/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx @@ -4,7 +4,7 @@ import { Device } from "@siteimprove/alfa-device"; import { createsStackingContext } from "../../dist/predicate/creates-stacking-context.js"; -test("non positioned element with z-index does not create a stacking context", async (t) => { +test("non positioned element with z-index does not create a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -13,7 +13,7 @@ test("non positioned element with z-index does not create a stacking context", a ); }); -test("absolutely positioned element without z-index does not create a stacking context", async (t) => { +test("absolutely positioned element without z-index does not create a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -22,7 +22,7 @@ test("absolutely positioned element without z-index does not create a stacking c ); }); -test("relatively positioned element without z-index does not create a stacking context", async (t) => { +test("relatively positioned element without z-index does not create a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -31,7 +31,7 @@ test("relatively positioned element without z-index does not create a stacking c ); }); -test("element with opacity equal to 1 does not create a stacking context", async (t) => { +test("element with opacity equal to 1 does not create a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -40,7 +40,7 @@ test("element with opacity equal to 1 does not create a stacking context", async ); }); -test("absolutely positioned element with z-index creates a stacking context", async (t) => { +test("absolutely positioned element with z-index creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -49,7 +49,7 @@ test("absolutely positioned element with z-index creates a stacking context", as ); }); -test("relatively positioned element with z-index creates a stacking context", async (t) => { +test("relatively positioned element with z-index creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -58,7 +58,7 @@ test("relatively positioned element with z-index creates a stacking context", as ); }); -test("fixed element creates a stacking context", async (t) => { +test("fixed element creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -67,7 +67,7 @@ test("fixed element creates a stacking context", async (t) => { ); }); -test("sticky element creates a stacking context", async (t) => { +test("sticky element creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -76,21 +76,21 @@ test("sticky element creates a stacking context", async (t) => { ); }); -test("flex child with z-index creates a stacking context", async (t) => { +test("flex child with z-index creates a stacking context", (t) => { const child =
;
{child}
; t.equal(createsStackingContext(Device.standard())(child), true); }); -test("grid child with z-index creates a stacking context", async (t) => { +test("grid child with z-index creates a stacking context", (t) => { const child =
;
{child}
; t.equal(createsStackingContext(Device.standard())(child), true); }); -test("element with opacity less than 1 creates a stacking context", async (t) => { +test("element with opacity less than 1 creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -99,7 +99,7 @@ test("element with opacity less than 1 creates a stacking context", async (t) => ); }); -test("element with mix-blend-mode equal to non-initial value creates a stacking context", async (t) => { +test("element with mix-blend-mode equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -108,7 +108,7 @@ test("element with mix-blend-mode equal to non-initial value creates a stacking ); }); -test("element with transform equal to non-initial value creates a stacking context", async (t) => { +test("element with transform equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -117,7 +117,7 @@ test("element with transform equal to non-initial value creates a stacking conte ); }); -test("element with scale equal to non-initial value creates a stacking context", async (t) => { +test("element with scale equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -126,7 +126,7 @@ test("element with scale equal to non-initial value creates a stacking context", ); }); -test("element with rotate equal to non-initial value creates a stacking context", async (t) => { +test("element with rotate equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -135,7 +135,7 @@ test("element with rotate equal to non-initial value creates a stacking context" ); }); -test("element with translate equal to non-initial value creates a stacking context", async (t) => { +test("element with translate equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -144,7 +144,7 @@ test("element with translate equal to non-initial value creates a stacking conte ); }); -test("element with perspective equal to non-initial value creates a stacking context", async (t) => { +test("element with perspective equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -153,7 +153,7 @@ test("element with perspective equal to non-initial value creates a stacking con ); }); -test("element with clip-path equal to non-initial value creates a stacking context", async (t) => { +test("element with clip-path equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -162,7 +162,7 @@ test("element with clip-path equal to non-initial value creates a stacking conte ); }); -test("element with mask equal to non-initial value creates a stacking context", async (t) => { +test("element with mask equal to non-initial value creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -171,7 +171,7 @@ test("element with mask equal to non-initial value creates a stacking context", ); }); -test("element with isolation equal to isolate creates a stacking context", async (t) => { +test("element with isolation equal to isolate creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -180,7 +180,7 @@ test("element with isolation equal to isolate creates a stacking context", async ); }); -test("element with will-change specifying a property that would create a stacking context on non-initial value, creates a stacking context", async (t) => { +test("element with will-change specifying a property that would create a stacking context on non-initial value, creates a stacking context", (t) => { for (const prop of [ "mix-blend-mode", "transform", @@ -202,7 +202,7 @@ test("element with will-change specifying a property that would create a stackin } }); -test("element with contain equal to layout creates a stacking context", async (t) => { +test("element with contain equal to layout creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -211,7 +211,7 @@ test("element with contain equal to layout creates a stacking context", async (t ); }); -test("element with contain equal to paint creates a stacking context", async (t) => { +test("element with contain equal to paint creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -220,7 +220,7 @@ test("element with contain equal to paint creates a stacking context", async (t) ); }); -test("element with contain equal to strict creates a stacking context", async (t) => { +test("element with contain equal to strict creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, @@ -229,7 +229,7 @@ test("element with contain equal to strict creates a stacking context", async (t ); }); -test("element with contain equal to content creates a stacking context", async (t) => { +test("element with contain equal to content creates a stacking context", (t) => { t.equal( createsStackingContext(Device.standard())(
, diff --git a/packages/alfa-painting-order/test/tsconfig.json b/packages/alfa-painting-order/test/tsconfig.json index 7bf1f920f5..8a8c8ecc2a 100644 --- a/packages/alfa-painting-order/test/tsconfig.json +++ b/packages/alfa-painting-order/test/tsconfig.json @@ -6,6 +6,9 @@ "jsx": "react-jsx", "jsxImportSource": "@siteimprove/alfa-dom" }, - "files": ["./predicate/creates-stacking-context.spec.tsx"], + "files": [ + "./predicate/creates-stacking-context.spec.tsx", + "./painting-order.spec.tsx" + ], "references": [{ "path": "../src" }, { "path": "../../alfa-test" }] } diff --git a/yarn.lock b/yarn.lock index d8b0af07dd..15945be283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1710,10 +1710,14 @@ __metadata: version: 0.0.0-use.local resolution: "@siteimprove/alfa-painting-order@workspace:packages/alfa-painting-order" dependencies: + "@siteimprove/alfa-array": "workspace:^" + "@siteimprove/alfa-cache": "workspace:^" + "@siteimprove/alfa-comparable": "workspace:^" "@siteimprove/alfa-css": "workspace:^" "@siteimprove/alfa-device": "workspace:^" "@siteimprove/alfa-dom": "workspace:^" "@siteimprove/alfa-refinement": "workspace:^" + "@siteimprove/alfa-sequence": "workspace:^" "@siteimprove/alfa-style": "workspace:^" "@siteimprove/alfa-test": "workspace:^" languageName: unknown From dd45af17d57b5090393b3040327787055682b544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:47:13 +0100 Subject: [PATCH 04/36] Add references to src/tsconfig.json --- packages/alfa-painting-order/src/tsconfig.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index 5e8cc7c305..ccbad3f3ed 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -7,5 +7,16 @@ "./painting-order.ts", "./predicate/creates-stacking-context.ts", "./predicate/is-flex-or-grid-child.ts" + ], + "references": [ + { "path": "../../alfa-array" }, + { "path": "../../alfa-cache" }, + { "path": "../../alfa-comparable" }, + { "path": "../../alfa-css" }, + { "path": "../../alfa-device" }, + { "path": "../../alfa-dom" }, + { "path": "../../alfa-refinement" }, + { "path": "../../alfa-sequence" }, + { "path": "../../alfa-style" } ] } From 5e2e7a6083a61258e82033d565bb1ff8f693bca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:48:15 +0100 Subject: [PATCH 05/36] Extract API --- docs/review/api/alfa-painting-order.api.md | 16 ++++++++++++++++ docs/review/api/alfa-style.api.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 docs/review/api/alfa-painting-order.api.md diff --git a/docs/review/api/alfa-painting-order.api.md b/docs/review/api/alfa-painting-order.api.md new file mode 100644 index 0000000000..1ba57aff36 --- /dev/null +++ b/docs/review/api/alfa-painting-order.api.md @@ -0,0 +1,16 @@ +## API Report File for "@siteimprove/alfa-painting-order" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { Device } from '@siteimprove/alfa-device'; +import { Element } from '@siteimprove/alfa-dom'; +import { Sequence } from '@siteimprove/alfa-sequence'; + +// @public +export function computePaintingOrder(root: Element, device: Device): Sequence; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 81086cd74d..7f25d7d0a8 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -440,6 +440,7 @@ export namespace Style { hasBoxShadow: typeof element.hasBoxShadow, // (undocumented) hasCascadedStyle: typeof element.hasCascadedStyle, // (undocumented) hasComputedStyle: typeof element.hasComputedStyle, // (undocumented) + hasInitialComputedStyle: typeof element.hasInitialComputedStyle, // (undocumented) hasPositioningParent: typeof element.hasPositioningParent, // (undocumented) hasOutline: typeof element.hasOutline, // (undocumented) hasSpecifiedStyle: typeof element.hasSpecifiedStyle, // (undocumented) From 9ae4f0250ccbb0153086378ad09b185b4b619a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:49:56 +0100 Subject: [PATCH 06/36] Add changeset --- .changeset/proud-apples-promise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/proud-apples-promise.md diff --git a/.changeset/proud-apples-promise.md b/.changeset/proud-apples-promise.md new file mode 100644 index 0000000000..d123ab456d --- /dev/null +++ b/.changeset/proud-apples-promise.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-painting-order": minor +--- + +**Added:** A new package has been added for computing the painting order of HTML elements. From e4a6bbf21095ad1819ba1d4a29bddf054aef655f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:06:32 +0100 Subject: [PATCH 07/36] Clean up tests --- .../test/painting-order.spec.tsx | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/alfa-painting-order/test/painting-order.spec.tsx b/packages/alfa-painting-order/test/painting-order.spec.tsx index f832faadc6..1e775278e9 100644 --- a/packages/alfa-painting-order/test/painting-order.spec.tsx +++ b/packages/alfa-painting-order/test/painting-order.spec.tsx @@ -7,40 +7,40 @@ import { computePaintingOrder } from "../dist/painting-order.js"; const device = Device.standard(); -test("block-level root element is painted before positioned descendant with negative z-index", (t) => { - const negativeZ =
; - const body = {negativeZ}; +test("block-level element is painted before positioned descendant with negative z-index", (t) => { + const div =
; + const body = {div}; h.document([body]); t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, negativeZ]).toJSON(), + Sequence.from([body, div]).toJSON(), ); }); test("block-level stacking context element is painted before positioned descendant with negative z-index", (t) => { - const negativeZ =
; - const sc =
{negativeZ}
; - const body = {sc}; + const div2 =
; + const div1 =
{div2}
; + const body = {div1}; h.document([body]); t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, sc, negativeZ]).toJSON(), + Sequence.from([body, div1, div2]).toJSON(), ); }); test("positioned elements with negative z-index are painted in z-order then tree-order", (t) => { - const negativeZ1 =
; - const negativeZ2 =
; - const negativeZ3 =
; + const div1 =
; + const div2 =
; + const div3 =
; const body = ( - {negativeZ1} - {negativeZ2} - {negativeZ3} + {div1} + {div2} + {div3} ); @@ -48,17 +48,17 @@ test("positioned elements with negative z-index are painted in z-order then tree t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, negativeZ3, negativeZ1, negativeZ2]).toJSON(), + Sequence.from([body, div3, div1, div2]).toJSON(), ); }); test("positioned element with negative z-index is painted before block-level element", (t) => { - const negativeZ =
; - const block =
; + const div2 =
; + const div1 =
; const body = ( - {block} - {negativeZ} + {div1} + {div2} ); @@ -66,7 +66,7 @@ test("positioned element with negative z-index is painted before block-level ele t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, negativeZ, block]).toJSON(), + Sequence.from([body, div2, div1]).toJSON(), ); }); @@ -90,12 +90,12 @@ test("block-level descendants are painted in tree-order", (t) => { }); test("block-level elements are painted before floating elements", (t) => { - const div =
; - const float =
; + const div2 =
; + const div1 =
; const body = ( - {float} - {div} + {div1} + {div2} ); @@ -103,7 +103,7 @@ test("block-level elements are painted before floating elements", (t) => { t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div, float]).toJSON(), + Sequence.from([body, div2, div1]).toJSON(), ); }); @@ -170,12 +170,12 @@ test("floating descendants are not painted atomically with respect to positioned }); test("floating descendants are painted before inline-level descendants", (t) => { - const inline =
; - const float =
; + const div1 =
; + const div2 =
; const body = ( - {inline} - {float} + {div1} + {div2} ); @@ -183,7 +183,7 @@ test("floating descendants are painted before inline-level descendants", (t) => t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, float, inline]).toJSON(), + Sequence.from([body, div2, div1]).toJSON(), ); }); @@ -207,12 +207,12 @@ test("inline-level descendants are painted in tree-order", (t) => { }); test("inline-level descendants are painted before positioned elements", (t) => { - const positioned =
; - const inline =
; + const div1 =
; + const div2 =
; const body = ( - {positioned} - {inline} + {div1} + {div2} ); @@ -220,7 +220,7 @@ test("inline-level descendants are painted before positioned elements", (t) => { t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, inline, positioned]).toJSON(), + Sequence.from([body, div2, div1]).toJSON(), ); }); @@ -289,12 +289,12 @@ test("positioned descendants are not painted atomically with respect to position }); test("positioned elements without z-index are painted before positioned elements with positve z-index", (t) => { - const zIndex =
; - const positioned =
; + const div1 =
; + const div2 =
; const body = ( - {zIndex} - {positioned} + {div1} + {div2} ); @@ -302,19 +302,19 @@ test("positioned elements without z-index are painted before positioned elements t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, positioned, zIndex]).toJSON(), + Sequence.from([body, div2, div1]).toJSON(), ); }); test("positioned elements with positive z-index are painted in z-order then tree-order", (t) => { - const positiveZ1 =
; - const positiveZ2 =
; - const positiveZ3 =
; + const div1 =
; + const div2 =
; + const div3 =
; const body = ( - {positiveZ1} - {positiveZ2} - {positiveZ3} + {div1} + {div2} + {div3} ); @@ -322,26 +322,26 @@ test("positioned elements with positive z-index are painted in z-order then tree t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, positiveZ2, positiveZ3, positiveZ1]).toJSON(), + Sequence.from([body, div2, div3, div1]).toJSON(), ); }); test("inline-level stacking context element is painted after floating descendants and before inline-level descendants", (t) => { - const float =
; - const inlineChild =
; - const inlineStackingContext = ( + const div2 =
; + const div3 =
; + const div1 = (
- {float} - {inlineChild} + {div2} + {div3}
); - const body = {inlineStackingContext}; + const body = {div1}; h.document([body]); t.deepEqual( computePaintingOrder(body, device).toJSON(), - Sequence.from([body, float, inlineStackingContext, inlineChild]).toJSON(), + Sequence.from([body, div2, div1, div3]).toJSON(), ); }); From 3e5fa27742296d7b850d350830e591c908fc9327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:03:34 +0100 Subject: [PATCH 08/36] Add `List.some` in alfa-css --- packages/alfa-css/src/value/collection/list.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/alfa-css/src/value/collection/list.ts b/packages/alfa-css/src/value/collection/list.ts index 60f554a1e1..b3f87ae161 100644 --- a/packages/alfa-css/src/value/collection/list.ts +++ b/packages/alfa-css/src/value/collection/list.ts @@ -1,8 +1,9 @@ import type { Hash } from "@siteimprove/alfa-hash"; -import type { Iterable } from "@siteimprove/alfa-iterable"; +import { Iterable } from "@siteimprove/alfa-iterable"; import type { Serializable } from "@siteimprove/alfa-json"; import type { Mapper } from "@siteimprove/alfa-mapper"; import { Parser } from "@siteimprove/alfa-parser"; +import type { Predicate } from "@siteimprove/alfa-predicate"; import { type Parser as CSSParser, Token } from "../../syntax/index.js"; @@ -48,6 +49,10 @@ export class List return this._values.length; } + public some(predicate: Predicate) { + return Iterable.some(this._values, predicate); + } + public resolve( resolver?: Resolvable.Resolver, ): List> { From ecc81e0dbafd461d55c21da7af0fbf91b4601fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:04:28 +0100 Subject: [PATCH 09/36] Add `Device.isDevice` --- packages/alfa-device/src/device.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/alfa-device/src/device.ts b/packages/alfa-device/src/device.ts index 66e3b091dd..b6dbb88335 100644 --- a/packages/alfa-device/src/device.ts +++ b/packages/alfa-device/src/device.ts @@ -164,4 +164,8 @@ export namespace Device { export function standard(): Device { return Device.of(Type.Screen, Viewport.standard(), Display.standard()); } + + export function isDevice(value: unknown): value is Device { + return value instanceof Device; + } } From 8bcc9dd1dbc4c111bf1d6796d461eba79463e2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:08:12 +0100 Subject: [PATCH 10/36] Add `isFlexOrGridChild` predicate in alfa-style --- packages/alfa-style/src/element/element.ts | 1 + .../predicate/is-flex-or-grid-child.ts | 22 +++++++++++++++ .../src/predicate/is-flex-container.ts | 27 +++++++++++++++++-- .../src/predicate/is-grid-container.ts | 27 +++++++++++++++++-- packages/alfa-style/src/style.ts | 1 + packages/alfa-style/src/tsconfig.json | 1 + 6 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts diff --git a/packages/alfa-style/src/element/element.ts b/packages/alfa-style/src/element/element.ts index 90fe0e3113..968a1d05fd 100755 --- a/packages/alfa-style/src/element/element.ts +++ b/packages/alfa-style/src/element/element.ts @@ -11,6 +11,7 @@ export * from "./predicate/has-specified-style.js"; export * from "./predicate/has-text-decoration.js"; export * from "./predicate/has-transparent-background.js"; export * from "./predicate/has-used-style.js"; +export * from "./predicate/is-flex-or-grid-child.js"; export * from "./predicate/is-focusable.js"; export * from "./predicate/is-important.js"; export * from "./predicate/is-inert.js"; diff --git a/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts b/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts new file mode 100644 index 0000000000..b31b1c7a88 --- /dev/null +++ b/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts @@ -0,0 +1,22 @@ +import type { Device } from "@siteimprove/alfa-device"; +import { Element, Node } from "@siteimprove/alfa-dom"; +import { Predicate } from "@siteimprove/alfa-predicate"; +import type { Context } from "@siteimprove/alfa-selector"; + +import { isFlexContainer } from "../../predicate/is-flex-container.js"; +import { isGridContainer } from "../../predicate/is-grid-container.js"; + +const { or } = Predicate; + +export function isFlexOrGridChild( + device: Device, + context?: Context, +): Predicate { + return (element) => + element + .parent(Node.fullTree) + .filter(Element.isElement) + .some( + or(isGridContainer(device, context), isFlexContainer(device, context)), + ); +} diff --git a/packages/alfa-style/src/predicate/is-flex-container.ts b/packages/alfa-style/src/predicate/is-flex-container.ts index 11bcdab204..587fab3ea1 100644 --- a/packages/alfa-style/src/predicate/is-flex-container.ts +++ b/packages/alfa-style/src/predicate/is-flex-container.ts @@ -1,12 +1,35 @@ +import { Device } from "@siteimprove/alfa-device"; +import type { Element } from "@siteimprove/alfa-dom"; +import type { Predicate } from "@siteimprove/alfa-predicate"; +import type { Context } from "@siteimprove/alfa-selector"; + import type { Style } from "../style.js"; +import { hasComputedStyle } from "../element/element.js"; + /** * {@link https://drafts.csswg.org/css-flexbox-1/#flex-container} * * @internal */ -export function isFlexContainer(style: Style): boolean { - const [_, inside] = style.computed("display").value.values; +export function isFlexContainer( + device: Device, + context?: Context, +): Predicate; +export function isFlexContainer(style: Style): boolean; +export function isFlexContainer( + deviceOrStyle: Device | Style, + context?: Context, +): Predicate | boolean { + if (Device.isDevice(deviceOrStyle)) { + return hasComputedStyle( + "display", + ({ values: [_, inside] }) => inside?.value === "flex", + deviceOrStyle, + context, + ); + } + const [_, inside] = deviceOrStyle.computed("display").value.values; return inside?.value === "flex"; } diff --git a/packages/alfa-style/src/predicate/is-grid-container.ts b/packages/alfa-style/src/predicate/is-grid-container.ts index 2d365bb126..fb835cb37d 100644 --- a/packages/alfa-style/src/predicate/is-grid-container.ts +++ b/packages/alfa-style/src/predicate/is-grid-container.ts @@ -1,12 +1,35 @@ +import { Device } from "@siteimprove/alfa-device"; +import type { Element } from "@siteimprove/alfa-dom"; +import type { Predicate } from "@siteimprove/alfa-predicate"; +import type { Context } from "@siteimprove/alfa-selector"; + import type { Style } from "../style.js"; +import { hasComputedStyle } from "../element/element.js"; + /** * {@link https://www.w3.org/TR/css-grid-2/#grid-container} * * @internal */ -export function isGridContainer(style: Style): boolean { - const [_, inside] = style.computed("display").value.values; +export function isGridContainer( + device: Device, + context?: Context, +): Predicate; +export function isGridContainer(style: Style): boolean; +export function isGridContainer( + deviceOrStyle: Device | Style, + context?: Context, +): Predicate | boolean { + if (Device.isDevice(deviceOrStyle)) { + return hasComputedStyle( + "display", + ({ values: [_, inside] }) => inside?.value === "grid", + deviceOrStyle, + context, + ); + } + const [_, inside] = deviceOrStyle.computed("display").value.values; return inside?.value === "grid"; } diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index f1d8848edd..07a06eb5af 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -490,6 +490,7 @@ export namespace Style { hasTextDecoration, hasTransparentBackground, hasUsedStyle, + isFlexOrGridChild, isFocusable, isImportant, isInert, diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index f987f817c8..211f21a1d2 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -17,6 +17,7 @@ "./element/predicate/has-text-decoration.ts", "./element/predicate/has-transparent-background.ts", "./element/predicate/has-used-style.ts", + "./element/predicate/is-flex-or-grid-child.ts", "./element/predicate/is-focusable.ts", "./element/predicate/is-important.ts", "./element/predicate/is-inert.ts", From a594733627c0aadc835ac3c78d2ee901b8a5ffa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:09:37 +0100 Subject: [PATCH 11/36] Use `isFlexOrGridChild` from alfa-style --- .../alfa-painting-order/src/painting-order.ts | 3 +- .../src/predicate/creates-stacking-context.ts | 42 ++++++++++--------- .../src/predicate/is-flex-or-grid-child.ts | 16 ------- .../alfa-painting-order/src/tsconfig.json | 3 +- 4 files changed, 25 insertions(+), 39 deletions(-) delete mode 100644 packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 5f3fd1bb61..a8991a1160 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -8,10 +8,9 @@ import { Sequence } from "@siteimprove/alfa-sequence"; import { Style } from "@siteimprove/alfa-style"; const { and, not, or } = Refinement; -const { hasComputedStyle, isRendered } = Style; +const { hasComputedStyle, isRendered, isFlexOrGridChild } = Style; import { createsStackingContext } from "./predicate/creates-stacking-context.js"; -import { isFlexOrGridChild } from "./predicate/is-flex-or-grid-child.js"; const cache = Cache.empty>>(); diff --git a/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts b/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts index f1926fb430..90e25de4b5 100644 --- a/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts +++ b/packages/alfa-painting-order/src/predicate/creates-stacking-context.ts @@ -1,12 +1,17 @@ -import { Keyword } from "@siteimprove/alfa-css"; +import { type Ident, Keyword, List } from "@siteimprove/alfa-css"; import { Device } from "@siteimprove/alfa-device"; +import { Element } from "@siteimprove/alfa-dom"; +import type { Predicate } from "@siteimprove/alfa-predicate"; import { Refinement } from "@siteimprove/alfa-refinement"; import { Style } from "@siteimprove/alfa-style"; const { and, not, or } = Refinement; -const { hasComputedStyle, hasInitialComputedStyle, isPositioned } = Style; - -import { isFlexOrGridChild } from "./is-flex-or-grid-child.js"; +const { + hasComputedStyle, + hasInitialComputedStyle, + isPositioned, + isFlexOrGridChild, +} = Style; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context} @@ -18,20 +23,20 @@ import { isFlexOrGridChild } from "./is-flex-or-grid-child.js"; * also create stacking contexts. We will have to update this predicate as such * new properties become supported by the browsers. * - * The properties `filter`, `backdrop-filter` and `mask-border` having - * non-initial values: Support for the properties are not yet implemented in Alfa. + * * The properties `filter`, `backdrop-filter` and `mask-border` having + * non-initial values: Support for the properties are not yet implemented in Alfa. * - * Elements placed into the top layer and its corresponding ::backdrop e.g. - * fullscreen and popover elements: It's unclear how to implement this and it's - * not the most important use case currently. + * * Elements placed into the top layer and its corresponding ::backdrop e.g. + * fullscreen and popover elements: It's unclear how to implement this and it's + * not the most important use case currently. * - * Element that has had stacking context-creating properties animated using - * @keyframes, with `animation-fill-mode` set to `forwards`: It's unclear how - * to implement this, but it is a valid case, that we should eventually support. + * * Element that has had stacking context-creating properties animated using + * @keyframes, with `animation-fill-mode` set to `forwards`: It's unclear how + * to implement this, but it is a valid case, that we should eventually support. * * @internal */ -export function createsStackingContext(device: Device) { +export function createsStackingContext(device: Device): Predicate { const hasZIndex = not(hasInitialComputedStyle("z-index", device)); return or( @@ -71,10 +76,9 @@ export function createsStackingContext(device: Device) { // on non-initial value hasComputedStyle( "will-change", - (value) => - !Keyword.isKeyword(value) && - value.values.some(({ value }) => - [ + and(List.isList, (list) => + list.some((ident) => + ident.is( "position", "z-index", "opacity", @@ -89,9 +93,9 @@ export function createsStackingContext(device: Device) { "clip-path", "mask", "isolation", - ].includes(value), + ), ), - + ), device, ), diff --git a/packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts b/packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts deleted file mode 100644 index 119e0f2f00..0000000000 --- a/packages/alfa-painting-order/src/predicate/is-flex-or-grid-child.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Device } from "@siteimprove/alfa-device"; -import { Element, Node } from "@siteimprove/alfa-dom"; -import { Style } from "@siteimprove/alfa-style"; - -const { isFlexContainer, isGridContainer } = Style; - -export function isFlexOrGridChild(device: Device) { - return (element: Element) => - element - .parent(Node.fullTree) - .filter(Element.isElement) - .some((parent) => { - const style = Style.from(parent, device); - return isFlexContainer(style) || isGridContainer(style); - }); -} diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index ccbad3f3ed..09879b8b75 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -5,8 +5,7 @@ "include": [ "./index.ts", "./painting-order.ts", - "./predicate/creates-stacking-context.ts", - "./predicate/is-flex-or-grid-child.ts" + "./predicate/creates-stacking-context.ts" ], "references": [ { "path": "../../alfa-array" }, From 0ade4349727cc324f40f51a0eb90e72b57d08276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:28:45 +0100 Subject: [PATCH 12/36] Revert changes to the is*Container functions --- .../predicate/is-flex-or-grid-child.ts | 9 ++++--- .../src/predicate/is-flex-container.ts | 27 ++----------------- .../src/predicate/is-grid-container.ts | 27 ++----------------- 3 files changed, 10 insertions(+), 53 deletions(-) diff --git a/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts b/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts index b31b1c7a88..0cf532f4f2 100644 --- a/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts +++ b/packages/alfa-style/src/element/predicate/is-flex-or-grid-child.ts @@ -2,9 +2,9 @@ import type { Device } from "@siteimprove/alfa-device"; import { Element, Node } from "@siteimprove/alfa-dom"; import { Predicate } from "@siteimprove/alfa-predicate"; import type { Context } from "@siteimprove/alfa-selector"; - import { isFlexContainer } from "../../predicate/is-flex-container.js"; import { isGridContainer } from "../../predicate/is-grid-container.js"; +import { Style } from "../../style.js"; const { or } = Predicate; @@ -16,7 +16,10 @@ export function isFlexOrGridChild( element .parent(Node.fullTree) .filter(Element.isElement) - .some( - or(isGridContainer(device, context), isFlexContainer(device, context)), + .some((parent) => + or( + isFlexContainer, + isGridContainer, + )(Style.from(parent, device, context)), ); } diff --git a/packages/alfa-style/src/predicate/is-flex-container.ts b/packages/alfa-style/src/predicate/is-flex-container.ts index 587fab3ea1..11bcdab204 100644 --- a/packages/alfa-style/src/predicate/is-flex-container.ts +++ b/packages/alfa-style/src/predicate/is-flex-container.ts @@ -1,35 +1,12 @@ -import { Device } from "@siteimprove/alfa-device"; -import type { Element } from "@siteimprove/alfa-dom"; -import type { Predicate } from "@siteimprove/alfa-predicate"; -import type { Context } from "@siteimprove/alfa-selector"; - import type { Style } from "../style.js"; -import { hasComputedStyle } from "../element/element.js"; - /** * {@link https://drafts.csswg.org/css-flexbox-1/#flex-container} * * @internal */ -export function isFlexContainer( - device: Device, - context?: Context, -): Predicate; -export function isFlexContainer(style: Style): boolean; -export function isFlexContainer( - deviceOrStyle: Device | Style, - context?: Context, -): Predicate | boolean { - if (Device.isDevice(deviceOrStyle)) { - return hasComputedStyle( - "display", - ({ values: [_, inside] }) => inside?.value === "flex", - deviceOrStyle, - context, - ); - } +export function isFlexContainer(style: Style): boolean { + const [_, inside] = style.computed("display").value.values; - const [_, inside] = deviceOrStyle.computed("display").value.values; return inside?.value === "flex"; } diff --git a/packages/alfa-style/src/predicate/is-grid-container.ts b/packages/alfa-style/src/predicate/is-grid-container.ts index fb835cb37d..2d365bb126 100644 --- a/packages/alfa-style/src/predicate/is-grid-container.ts +++ b/packages/alfa-style/src/predicate/is-grid-container.ts @@ -1,35 +1,12 @@ -import { Device } from "@siteimprove/alfa-device"; -import type { Element } from "@siteimprove/alfa-dom"; -import type { Predicate } from "@siteimprove/alfa-predicate"; -import type { Context } from "@siteimprove/alfa-selector"; - import type { Style } from "../style.js"; -import { hasComputedStyle } from "../element/element.js"; - /** * {@link https://www.w3.org/TR/css-grid-2/#grid-container} * * @internal */ -export function isGridContainer( - device: Device, - context?: Context, -): Predicate; -export function isGridContainer(style: Style): boolean; -export function isGridContainer( - deviceOrStyle: Device | Style, - context?: Context, -): Predicate | boolean { - if (Device.isDevice(deviceOrStyle)) { - return hasComputedStyle( - "display", - ({ values: [_, inside] }) => inside?.value === "grid", - deviceOrStyle, - context, - ); - } +export function isGridContainer(style: Style): boolean { + const [_, inside] = style.computed("display").value.values; - const [_, inside] = deviceOrStyle.computed("display").value.values; return inside?.value === "grid"; } From 03b368cbadc7d1d5992ce3dc4a35f9080210357f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:28:55 +0100 Subject: [PATCH 13/36] Extract API --- docs/review/api/alfa-css.api.md | 2 ++ docs/review/api/alfa-device.api.md | 2 ++ docs/review/api/alfa-style.api.md | 57 +++++++++++++++--------------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/docs/review/api/alfa-css.api.md b/docs/review/api/alfa-css.api.md index e7b5f5af47..a4551d36e8 100644 --- a/docs/review/api/alfa-css.api.md +++ b/docs/review/api/alfa-css.api.md @@ -1298,6 +1298,8 @@ export class List extends Value<"list", Value.HasCalculation<[V // (undocumented) get size(): number; // (undocumented) + some(predicate: Predicate): boolean; + // (undocumented) toJSON(): List.JSON; // (undocumented) toString(): string; diff --git a/docs/review/api/alfa-device.api.md b/docs/review/api/alfa-device.api.md index 9ef2b202a7..afdc3d20f5 100644 --- a/docs/review/api/alfa-device.api.md +++ b/docs/review/api/alfa-device.api.md @@ -42,6 +42,8 @@ export namespace Device { // (undocumented) export function from(json: JSON): Device; // (undocumented) + export function isDevice(value: unknown): value is Device; + // (undocumented) export interface JSON { // (undocumented) [key: string]: json.JSON; diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 7f25d7d0a8..b54470c343 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -160,42 +160,42 @@ export namespace Longhands { readonly "background-repeat-y": Longhand, List>; readonly "background-size": Longhand, List, LengthPercentage | Keyword<"auto">]> | Keyword<"cover"> | Keyword<"contain">>>; readonly "border-block-end-color": Longhand; - readonly "border-block-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-block-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-block-end-width": Longhand; readonly "border-block-start-color": Longhand; - readonly "border-block-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-block-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-block-start-width": Longhand; readonly "border-bottom-color": Longhand; readonly "border-bottom-left-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-bottom-right-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; - readonly "border-bottom-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-bottom-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-bottom-width": Longhand; readonly "border-collapse": Longhand, Keyword.ToKeywords<"separate" | "collapse">>; readonly "border-end-end-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-end-start-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-image-outset": Longhand>; readonly "border-image-repeat": Longhand; - readonly "border-image-slice": Longhand | Tuple<[top: Number_2.Fixed | Percentage.Canonical, right: Number_2.Fixed | Percentage.Canonical, bottom: Number_2.Fixed | Percentage.Canonical, left: Number_2.Fixed | Percentage.Canonical, fill: Keyword<"fill">]>>; + readonly "border-image-slice": Longhand | Tuple<[top: Percentage.Canonical | Number_2.Fixed, right: Percentage.Canonical | Number_2.Fixed, bottom: Percentage.Canonical | Number_2.Fixed, left: Percentage.Canonical | Number_2.Fixed, fill: Keyword<"fill">]>>; readonly "border-image-source": Longhand | Image.PartiallyResolved>; readonly "border-image-width": Longhand, right: Number_2.Fixed | LengthPercentage | Keyword<"auto">, bottom: Number_2.Fixed | LengthPercentage | Keyword<"auto">, left: Number_2.Fixed | LengthPercentage | Keyword<"auto">]>>; readonly "border-inline-end-color": Longhand; - readonly "border-inline-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-inline-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-inline-end-width": Longhand; readonly "border-inline-start-color": Longhand; - readonly "border-inline-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-inline-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-inline-start-width": Longhand; readonly "border-left-color": Longhand; - readonly "border-left-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-left-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-left-width": Longhand; readonly "border-right-color": Longhand; - readonly "border-right-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-right-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-right-width": Longhand; readonly "border-start-end-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-start-start-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-top-color": Longhand; readonly "border-top-left-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-top-right-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; - readonly "border-top-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "border-top-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "border-top-width": Longhand; readonly bottom: Longhand, LengthPercentage | Keyword<"auto">>; readonly "box-shadow": Longhand | List>, Keyword<"none"> | List>; @@ -203,13 +203,13 @@ export namespace Longhands { readonly clip: Longhand | Shape, Keyword<"border-box">>, Keyword<"auto"> | Shape, Keyword<"border-box">>>; readonly color: Longhand; readonly contain: Longhand; - readonly "container-type": Longhand, Keyword.ToKeywords<"size" | "normal" | "inline-size">>; + readonly "container-type": Longhand, Keyword.ToKeywords<"normal" | "size" | "inline-size">>; readonly cursor: Longhand>, Keyword<"none"> | Keyword<"auto"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">]>, Tuple<[List>, Keyword<"none"> | Keyword<"auto"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">]>>; readonly display: Longhand | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">, listitem: Keyword<"list-item">]> | Tuple<[outside: Keyword<"table-row-group"> | Keyword<"table-header-group"> | Keyword<"table-footer-group"> | Keyword<"table-row"> | Keyword<"table-cell"> | Keyword<"table-column-group"> | Keyword<"table-column"> | Keyword<"table-caption"> | Keyword<"ruby-base"> | Keyword<"ruby-text"> | Keyword<"ruby-base-container"> | Keyword<"ruby-text-container">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[Keyword<"none"> | Keyword<"contents">]>, Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">, listitem: Keyword<"list-item">]> | Tuple<[outside: Keyword<"table-row-group"> | Keyword<"table-header-group"> | Keyword<"table-footer-group"> | Keyword<"table-row"> | Keyword<"table-cell"> | Keyword<"table-column-group"> | Keyword<"table-column"> | Keyword<"table-caption"> | Keyword<"ruby-base"> | Keyword<"ruby-text"> | Keyword<"ruby-base-container"> | Keyword<"ruby-text-container">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[Keyword<"none"> | Keyword<"contents">]>>; readonly "flex-direction": Longhand, Keyword.ToKeywords<"row" | "row-reverse" | "column" | "column-reverse">>; readonly "flex-wrap": Longhand, Keyword.ToKeywords<"nowrap" | "wrap" | "wrap-reverse">>; readonly float: Longhand, Keyword.ToKeywords<"none" | "left" | "right">>; - readonly "font-family": Longhand | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace">>, List | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace">>>; + readonly "font-family": Longhand | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace"> | String_2>, List | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace"> | String_2>>; readonly "font-size": Longhand | Keyword<"xx-small"> | Keyword<"x-small"> | Keyword<"small"> | Keyword<"large"> | Keyword<"x-large"> | Keyword<"xx-large"> | Keyword<"xxx-large"> | Keyword<"larger"> | Keyword<"smaller">, Length>; readonly "font-stretch": Longhand | Percentage.Fixed>; readonly "font-style": Longhand, Keyword.ToKeywords<"normal" | "italic" | "oblique">>; @@ -217,7 +217,7 @@ export namespace Longhands { readonly "font-variant-east-asian": Longhand; readonly "font-variant-ligatures": Longhand; readonly "font-variant-numeric": Longhand; - readonly "font-variant-position": Longhand, Keyword.ToKeywords<"sub" | "normal" | "super">>; + readonly "font-variant-position": Longhand, Keyword.ToKeywords<"normal" | "sub" | "super">>; readonly "font-weight": Longhand | Keyword<"bold"> | Keyword<"bolder"> | Keyword<"lighter">, Number_2.Fixed>; readonly height: Longhand, LengthPercentage | Keyword<"auto">>; readonly "inset-block-end": Longhand, LengthPercentage | Keyword<"auto">>; @@ -227,11 +227,11 @@ export namespace Longhands { readonly isolation: Longhand, Keyword.ToKeywords<"auto" | "isolate">>; readonly left: Longhand, LengthPercentage | Keyword<"auto">>; readonly "letter-spacing": Longhand, Length>; - readonly "line-height": Longhand, Computed>; - readonly "margin-bottom": Longhand, Length | Percentage | Keyword<"auto">>; - readonly "margin-left": Longhand, Length | Percentage | Keyword<"auto">>; - readonly "margin-right": Longhand, Length | Percentage | Keyword<"auto">>; - readonly "margin-top": Longhand, Length | Percentage | Keyword<"auto">>; + readonly "line-height": Longhand, Computed>; + readonly "margin-bottom": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; + readonly "margin-left": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; + readonly "margin-right": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; + readonly "margin-top": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; readonly "mask-clip": Longhand, List>; readonly "mask-composite": Longhand, List>; readonly "mask-image": Longhand, List>; @@ -240,19 +240,19 @@ export namespace Longhands { readonly "mask-position": Longhand, List>; readonly "mask-repeat": Longhand, List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">, Keyword<"repeat"> | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">]>>>; readonly "mask-size": Longhand, List>; - readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; - readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; - readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"screen" | "color" | "hue" | "saturation" | "normal" | "multiply" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "luminosity" | "plus-darker" | "plus-lighter">>; + readonly "min-height": Longhand | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Keyword<"auto"> | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; + readonly "min-width": Longhand | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Keyword<"auto"> | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; + readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"color" | "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "luminosity" | "plus-darker" | "plus-lighter">>; readonly opacity: Longhand, Number_2.Fixed>; readonly "outline-color": Longhand, Color.Canonical | Keyword<"invert">>; readonly "outline-offset": Longhand, Length>; - readonly "outline-style": Longhand, Keyword.ToKeywords<"none" | "inset" | "auto" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; + readonly "outline-style": Longhand, Keyword.ToKeywords<"none" | "auto" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; readonly "outline-width": Longhand | Keyword<"medium"> | Keyword<"thick">, Length>; - readonly "overflow-x": Longhand, Keyword<"auto"> | Keyword<"scroll"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; - readonly "overflow-y": Longhand, Keyword<"auto"> | Keyword<"scroll"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; + readonly "overflow-x": Longhand, Keyword<"scroll"> | Keyword<"auto"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; + readonly "overflow-y": Longhand, Keyword<"scroll"> | Keyword<"auto"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; readonly perspective: Longhand | Perspective, Keyword<"none"> | Perspective>; readonly "pointer-events": Longhand, Keyword.ToKeywords<"none" | "auto">>; - readonly position: Longhand, Keyword.ToKeywords<"fixed" | "relative" | "static" | "absolute" | "sticky">>; + readonly position: Longhand, Keyword.ToKeywords<"fixed" | "static" | "relative" | "absolute" | "sticky">>; readonly right: Longhand, LengthPercentage | Keyword<"auto">>; readonly rotate: Longhand | Rotate, Keyword<"none"> | Rotate>; readonly scale: Longhand | Scale, Number_2.Fixed | Percentage.Fixed<"percentage">, Number_2.Fixed | Percentage.Fixed<"percentage">>, Keyword<"none"> | Scale, Number_2.Fixed | Percentage.Fixed<"percentage">, Number_2.Fixed | Percentage.Fixed<"percentage">>>; @@ -261,7 +261,7 @@ export namespace Longhands { readonly "text-decoration-line": Longhand | List | Keyword<"overline"> | Keyword<"line-through"> | Keyword<"blink">>, Keyword<"none"> | List | Keyword<"overline"> | Keyword<"line-through"> | Keyword<"blink">>>; readonly "text-decoration-style": Longhand, Keyword.ToKeywords<"dotted" | "dashed" | "solid" | "double" | "wavy">>; readonly "text-decoration-thickness": Longhand | Keyword<"from-font">, Length | Keyword<"auto"> | Keyword<"from-font">>; - readonly "text-indent": Longhand | LengthPercentage | Percentage.Calculated<"length"> | Percentage.Fixed<"length">, LengthPercentage>; + readonly "text-indent": Longhand | Length | Percentage.Calculated<"length"> | Length, LengthPercentage>; readonly "text-overflow": Longhand, Keyword.ToKeywords<"clip" | "ellipsis">>; readonly "text-shadow": Longhand | List>, Keyword<"none"> | List>; readonly "text-transform": Longhand, Keyword.ToKeywords<"none" | "capitalize" | "uppercase" | "lowercase">>; @@ -272,9 +272,9 @@ export namespace Longhands { readonly visibility: Longhand, Keyword.ToKeywords<"hidden" | "collapse" | "visible">>; readonly "white-space": Longhand, Keyword.ToKeywords<"normal" | "nowrap" | "pre" | "pre-wrap" | "break-spaces" | "pre-line">>; readonly width: Longhand, LengthPercentage | Keyword<"auto">>; - readonly "will-change": Longhand | List | Keyword<"scroll-position">>, Keyword<"auto"> | List | Keyword<"scroll-position">>>; + readonly "will-change": Longhand | List | Keyword<"scroll-position"> | CustomIdent>, Keyword<"auto"> | List | Keyword<"scroll-position"> | CustomIdent>>; readonly "word-spacing": Longhand, Length>; - readonly "z-index": Longhand, Integer.Fixed | Keyword<"auto">>; + readonly "z-index": Longhand | Integer, Integer.Fixed | Keyword<"auto">>; }; // (undocumented) export type Property = typeof longHands; @@ -353,7 +353,7 @@ export namespace Shorthands { readonly "font-variant": Shorthand<"font-variant-caps" | "font-variant-east-asian" | "font-variant-ligatures" | "font-variant-numeric">; readonly "inset-block": Shorthand<"inset-block-end" | "inset-block-start">; readonly "inset-inline": Shorthand<"inset-inline-end" | "inset-inline-start">; - readonly inset: Shorthand<"top" | "bottom" | "left" | "right">; + readonly inset: Shorthand<"left" | "right" | "top" | "bottom">; readonly margin: Shorthand<"margin-bottom" | "margin-left" | "margin-right" | "margin-top">; readonly mask: Shorthand<"mask-image" | "mask-clip" | "mask-composite" | "mask-mode" | "mask-origin" | "mask-position" | "mask-repeat" | "mask-size">; readonly outline: Shorthand<"outline-color" | "outline-style" | "outline-width">; @@ -447,6 +447,7 @@ export namespace Style { hasTextDecoration: typeof element.hasTextDecoration, // (undocumented) hasTransparentBackground: typeof element.hasTransparentBackground, // (undocumented) hasUsedStyle: typeof element.hasUsedStyle, // (undocumented) + isFlexOrGridChild: typeof element.isFlexOrGridChild, // (undocumented) isFocusable: typeof element.isFocusable, // (undocumented) isImportant: typeof element.isImportant, // (undocumented) isInert: typeof element.isInert, // (undocumented) From 7b541846ab11828f50602bbd9c260d6a2c2e959c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:34:44 +0100 Subject: [PATCH 14/36] Add missing references --- packages/alfa-painting-order/package.json | 1 + packages/alfa-painting-order/src/tsconfig.json | 1 + yarn.lock | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/alfa-painting-order/package.json b/packages/alfa-painting-order/package.json index 77efdaf917..ddf3e063ec 100644 --- a/packages/alfa-painting-order/package.json +++ b/packages/alfa-painting-order/package.json @@ -28,6 +28,7 @@ "@siteimprove/alfa-css": "workspace:^", "@siteimprove/alfa-device": "workspace:^", "@siteimprove/alfa-dom": "workspace:^", + "@siteimprove/alfa-predicate": "workspace:^", "@siteimprove/alfa-refinement": "workspace:^", "@siteimprove/alfa-sequence": "workspace:^", "@siteimprove/alfa-style": "workspace:^" diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index 09879b8b75..5c19811fa8 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -14,6 +14,7 @@ { "path": "../../alfa-css" }, { "path": "../../alfa-device" }, { "path": "../../alfa-dom" }, + { "path": "../../alfa-predicate" }, { "path": "../../alfa-refinement" }, { "path": "../../alfa-sequence" }, { "path": "../../alfa-style" } diff --git a/yarn.lock b/yarn.lock index 4f5375ffa8..cb360f3646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1716,6 +1716,7 @@ __metadata: "@siteimprove/alfa-css": "workspace:^" "@siteimprove/alfa-device": "workspace:^" "@siteimprove/alfa-dom": "workspace:^" + "@siteimprove/alfa-predicate": "workspace:^" "@siteimprove/alfa-refinement": "workspace:^" "@siteimprove/alfa-sequence": "workspace:^" "@siteimprove/alfa-style": "workspace:^" From b4ee48a9d664fce5b2567b79ed12b9a5f9033df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:38:21 +0100 Subject: [PATCH 15/36] Add changesets --- .changeset/five-rocks-join.md | 5 +++++ .changeset/friendly-pots-destroy.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/five-rocks-join.md create mode 100644 .changeset/friendly-pots-destroy.md diff --git a/.changeset/five-rocks-join.md b/.changeset/five-rocks-join.md new file mode 100644 index 0000000000..25df7ff2e1 --- /dev/null +++ b/.changeset/five-rocks-join.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-style": minor +--- + +**Added:** A new predicate `isFlexOrGridChild` is available. diff --git a/.changeset/friendly-pots-destroy.md b/.changeset/friendly-pots-destroy.md new file mode 100644 index 0000000000..a6b0068d2d --- /dev/null +++ b/.changeset/friendly-pots-destroy.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-device": minor +--- + +**Added:** `Device.isDevice` is now available. From db98af5147b78b7729234e13f6febd3b27833147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:39:43 +0100 Subject: [PATCH 16/36] Add changeset for alfa-css --- .changeset/eight-experts-know.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-experts-know.md diff --git a/.changeset/eight-experts-know.md b/.changeset/eight-experts-know.md new file mode 100644 index 0000000000..4f408cd2eb --- /dev/null +++ b/.changeset/eight-experts-know.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-css": minor +--- + +**Added:** `List.some` is now available. From 63ad89e65944dec2dfdbc50974fd691374a9e975 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:50:54 +0000 Subject: [PATCH 17/36] Extract API --- docs/review/api/alfa-style.api.md | 56 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index b54470c343..4897ba71fd 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -160,42 +160,42 @@ export namespace Longhands { readonly "background-repeat-y": Longhand, List>; readonly "background-size": Longhand, List, LengthPercentage | Keyword<"auto">]> | Keyword<"cover"> | Keyword<"contain">>>; readonly "border-block-end-color": Longhand; - readonly "border-block-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-block-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-block-end-width": Longhand; readonly "border-block-start-color": Longhand; - readonly "border-block-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-block-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-block-start-width": Longhand; readonly "border-bottom-color": Longhand; readonly "border-bottom-left-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-bottom-right-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; - readonly "border-bottom-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-bottom-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-bottom-width": Longhand; readonly "border-collapse": Longhand, Keyword.ToKeywords<"separate" | "collapse">>; readonly "border-end-end-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-end-start-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-image-outset": Longhand>; readonly "border-image-repeat": Longhand; - readonly "border-image-slice": Longhand | Tuple<[top: Percentage.Canonical | Number_2.Fixed, right: Percentage.Canonical | Number_2.Fixed, bottom: Percentage.Canonical | Number_2.Fixed, left: Percentage.Canonical | Number_2.Fixed, fill: Keyword<"fill">]>>; + readonly "border-image-slice": Longhand | Tuple<[top: Number_2.Fixed | Percentage.Canonical, right: Number_2.Fixed | Percentage.Canonical, bottom: Number_2.Fixed | Percentage.Canonical, left: Number_2.Fixed | Percentage.Canonical, fill: Keyword<"fill">]>>; readonly "border-image-source": Longhand | Image.PartiallyResolved>; readonly "border-image-width": Longhand, right: Number_2.Fixed | LengthPercentage | Keyword<"auto">, bottom: Number_2.Fixed | LengthPercentage | Keyword<"auto">, left: Number_2.Fixed | LengthPercentage | Keyword<"auto">]>>; readonly "border-inline-end-color": Longhand; - readonly "border-inline-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-inline-end-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-inline-end-width": Longhand; readonly "border-inline-start-color": Longhand; - readonly "border-inline-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-inline-start-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-inline-start-width": Longhand; readonly "border-left-color": Longhand; - readonly "border-left-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-left-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-left-width": Longhand; readonly "border-right-color": Longhand; - readonly "border-right-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-right-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-right-width": Longhand; readonly "border-start-end-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-start-start-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-top-color": Longhand; readonly "border-top-left-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; readonly "border-top-right-radius": Longhand, Tuple<[horizontal: LengthPercentage, vertical: LengthPercentage]>>; - readonly "border-top-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "border-top-style": Longhand, Keyword.ToKeywords<"none" | "hidden" | "inset" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "border-top-width": Longhand; readonly bottom: Longhand, LengthPercentage | Keyword<"auto">>; readonly "box-shadow": Longhand | List>, Keyword<"none"> | List>; @@ -203,13 +203,13 @@ export namespace Longhands { readonly clip: Longhand | Shape, Keyword<"border-box">>, Keyword<"auto"> | Shape, Keyword<"border-box">>>; readonly color: Longhand; readonly contain: Longhand; - readonly "container-type": Longhand, Keyword.ToKeywords<"normal" | "size" | "inline-size">>; + readonly "container-type": Longhand, Keyword.ToKeywords<"size" | "normal" | "inline-size">>; readonly cursor: Longhand>, Keyword<"none"> | Keyword<"auto"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">]>, Tuple<[List>, Keyword<"none"> | Keyword<"auto"> | Keyword<"default"> | Keyword<"context-menu"> | Keyword<"help"> | Keyword<"pointer"> | Keyword<"progress"> | Keyword<"wait"> | Keyword<"cell"> | Keyword<"crosshair"> | Keyword<"text"> | Keyword<"vertical-text"> | Keyword<"alias"> | Keyword<"copy"> | Keyword<"move"> | Keyword<"no-drop"> | Keyword<"not-allowed"> | Keyword<"grab"> | Keyword<"grabbing"> | Keyword<"e-resize"> | Keyword<"n-resize"> | Keyword<"ne-resize"> | Keyword<"nw-resize"> | Keyword<"s-resize"> | Keyword<"se-resize"> | Keyword<"sw-resize"> | Keyword<"w-resize"> | Keyword<"ew-resize"> | Keyword<"ns-resize"> | Keyword<"nesw-resize"> | Keyword<"nwse-resize"> | Keyword<"col-resize"> | Keyword<"row-resize"> | Keyword<"all-scroll"> | Keyword<"zoom-in"> | Keyword<"zoom-out">]>>; readonly display: Longhand | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">, listitem: Keyword<"list-item">]> | Tuple<[outside: Keyword<"table-row-group"> | Keyword<"table-header-group"> | Keyword<"table-footer-group"> | Keyword<"table-row"> | Keyword<"table-cell"> | Keyword<"table-column-group"> | Keyword<"table-column"> | Keyword<"table-caption"> | Keyword<"ruby-base"> | Keyword<"ruby-text"> | Keyword<"ruby-base-container"> | Keyword<"ruby-text-container">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[Keyword<"none"> | Keyword<"contents">]>, Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[outside: Keyword<"block"> | Keyword<"inline"> | Keyword<"run-in">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">, listitem: Keyword<"list-item">]> | Tuple<[outside: Keyword<"table-row-group"> | Keyword<"table-header-group"> | Keyword<"table-footer-group"> | Keyword<"table-row"> | Keyword<"table-cell"> | Keyword<"table-column-group"> | Keyword<"table-column"> | Keyword<"table-caption"> | Keyword<"ruby-base"> | Keyword<"ruby-text"> | Keyword<"ruby-base-container"> | Keyword<"ruby-text-container">, inside: Keyword<"flow"> | Keyword<"flow-root"> | Keyword<"table"> | Keyword<"flex"> | Keyword<"grid"> | Keyword<"ruby">]> | Tuple<[Keyword<"none"> | Keyword<"contents">]>>; readonly "flex-direction": Longhand, Keyword.ToKeywords<"row" | "row-reverse" | "column" | "column-reverse">>; readonly "flex-wrap": Longhand, Keyword.ToKeywords<"nowrap" | "wrap" | "wrap-reverse">>; readonly float: Longhand, Keyword.ToKeywords<"none" | "left" | "right">>; - readonly "font-family": Longhand | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace"> | String_2>, List | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace"> | String_2>>; + readonly "font-family": Longhand | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace">>, List | Keyword<"sans-serif"> | Keyword<"cursive"> | Keyword<"fantasy"> | Keyword<"monospace">>>; readonly "font-size": Longhand | Keyword<"xx-small"> | Keyword<"x-small"> | Keyword<"small"> | Keyword<"large"> | Keyword<"x-large"> | Keyword<"xx-large"> | Keyword<"xxx-large"> | Keyword<"larger"> | Keyword<"smaller">, Length>; readonly "font-stretch": Longhand | Percentage.Fixed>; readonly "font-style": Longhand, Keyword.ToKeywords<"normal" | "italic" | "oblique">>; @@ -217,7 +217,7 @@ export namespace Longhands { readonly "font-variant-east-asian": Longhand; readonly "font-variant-ligatures": Longhand; readonly "font-variant-numeric": Longhand; - readonly "font-variant-position": Longhand, Keyword.ToKeywords<"normal" | "sub" | "super">>; + readonly "font-variant-position": Longhand, Keyword.ToKeywords<"sub" | "normal" | "super">>; readonly "font-weight": Longhand | Keyword<"bold"> | Keyword<"bolder"> | Keyword<"lighter">, Number_2.Fixed>; readonly height: Longhand, LengthPercentage | Keyword<"auto">>; readonly "inset-block-end": Longhand, LengthPercentage | Keyword<"auto">>; @@ -227,11 +227,11 @@ export namespace Longhands { readonly isolation: Longhand, Keyword.ToKeywords<"auto" | "isolate">>; readonly left: Longhand, LengthPercentage | Keyword<"auto">>; readonly "letter-spacing": Longhand, Length>; - readonly "line-height": Longhand, Computed>; - readonly "margin-bottom": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; - readonly "margin-left": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; - readonly "margin-right": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; - readonly "margin-top": Longhand | Percentage, Length | Keyword<"auto"> | Percentage>; + readonly "line-height": Longhand, Computed>; + readonly "margin-bottom": Longhand, Length | Percentage | Keyword<"auto">>; + readonly "margin-left": Longhand, Length | Percentage | Keyword<"auto">>; + readonly "margin-right": Longhand, Length | Percentage | Keyword<"auto">>; + readonly "margin-top": Longhand, Length | Percentage | Keyword<"auto">>; readonly "mask-clip": Longhand, List>; readonly "mask-composite": Longhand, List>; readonly "mask-image": Longhand, List>; @@ -240,19 +240,19 @@ export namespace Longhands { readonly "mask-position": Longhand, List>; readonly "mask-repeat": Longhand, List | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">, Keyword<"repeat"> | Keyword<"space"> | Keyword<"round"> | Keyword<"no-repeat">]>>>; readonly "mask-size": Longhand, List>; - readonly "min-height": Longhand | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Keyword<"auto"> | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; - readonly "min-width": Longhand | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Keyword<"auto"> | Percentage | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; - readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"color" | "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "luminosity" | "plus-darker" | "plus-lighter">>; + readonly "min-height": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; + readonly "min-width": Longhand | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">, Length | Percentage | Keyword<"auto"> | Keyword<"fit-content"> | Keyword<"max-content"> | Keyword<"min-content">>; + readonly "mix-blend-mode": Longhand, Keyword.ToKeywords<"screen" | "color" | "hue" | "saturation" | "normal" | "multiply" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "luminosity" | "plus-darker" | "plus-lighter">>; readonly opacity: Longhand, Number_2.Fixed>; readonly "outline-color": Longhand, Color.Canonical | Keyword<"invert">>; readonly "outline-offset": Longhand, Length>; - readonly "outline-style": Longhand, Keyword.ToKeywords<"none" | "auto" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset">>; + readonly "outline-style": Longhand, Keyword.ToKeywords<"none" | "inset" | "auto" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "outset">>; readonly "outline-width": Longhand | Keyword<"medium"> | Keyword<"thick">, Length>; - readonly "overflow-x": Longhand, Keyword<"scroll"> | Keyword<"auto"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; - readonly "overflow-y": Longhand, Keyword<"scroll"> | Keyword<"auto"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; + readonly "overflow-x": Longhand, Keyword<"auto"> | Keyword<"scroll"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; + readonly "overflow-y": Longhand, Keyword<"auto"> | Keyword<"scroll"> | Keyword<"hidden"> | Keyword<"visible"> | Keyword<"clip">>; readonly perspective: Longhand | Perspective, Keyword<"none"> | Perspective>; readonly "pointer-events": Longhand, Keyword.ToKeywords<"none" | "auto">>; - readonly position: Longhand, Keyword.ToKeywords<"fixed" | "static" | "relative" | "absolute" | "sticky">>; + readonly position: Longhand, Keyword.ToKeywords<"fixed" | "relative" | "static" | "absolute" | "sticky">>; readonly right: Longhand, LengthPercentage | Keyword<"auto">>; readonly rotate: Longhand | Rotate, Keyword<"none"> | Rotate>; readonly scale: Longhand | Scale, Number_2.Fixed | Percentage.Fixed<"percentage">, Number_2.Fixed | Percentage.Fixed<"percentage">>, Keyword<"none"> | Scale, Number_2.Fixed | Percentage.Fixed<"percentage">, Number_2.Fixed | Percentage.Fixed<"percentage">>>; @@ -261,7 +261,7 @@ export namespace Longhands { readonly "text-decoration-line": Longhand | List | Keyword<"overline"> | Keyword<"line-through"> | Keyword<"blink">>, Keyword<"none"> | List | Keyword<"overline"> | Keyword<"line-through"> | Keyword<"blink">>>; readonly "text-decoration-style": Longhand, Keyword.ToKeywords<"dotted" | "dashed" | "solid" | "double" | "wavy">>; readonly "text-decoration-thickness": Longhand | Keyword<"from-font">, Length | Keyword<"auto"> | Keyword<"from-font">>; - readonly "text-indent": Longhand | Length | Percentage.Calculated<"length"> | Length, LengthPercentage>; + readonly "text-indent": Longhand | LengthPercentage | Percentage.Calculated<"length"> | Percentage.Fixed<"length">, LengthPercentage>; readonly "text-overflow": Longhand, Keyword.ToKeywords<"clip" | "ellipsis">>; readonly "text-shadow": Longhand | List>, Keyword<"none"> | List>; readonly "text-transform": Longhand, Keyword.ToKeywords<"none" | "capitalize" | "uppercase" | "lowercase">>; @@ -272,9 +272,9 @@ export namespace Longhands { readonly visibility: Longhand, Keyword.ToKeywords<"hidden" | "collapse" | "visible">>; readonly "white-space": Longhand, Keyword.ToKeywords<"normal" | "nowrap" | "pre" | "pre-wrap" | "break-spaces" | "pre-line">>; readonly width: Longhand, LengthPercentage | Keyword<"auto">>; - readonly "will-change": Longhand | List | Keyword<"scroll-position"> | CustomIdent>, Keyword<"auto"> | List | Keyword<"scroll-position"> | CustomIdent>>; + readonly "will-change": Longhand | List | Keyword<"scroll-position">>, Keyword<"auto"> | List | Keyword<"scroll-position">>>; readonly "word-spacing": Longhand, Length>; - readonly "z-index": Longhand | Integer, Integer.Fixed | Keyword<"auto">>; + readonly "z-index": Longhand, Integer.Fixed | Keyword<"auto">>; }; // (undocumented) export type Property = typeof longHands; @@ -353,7 +353,7 @@ export namespace Shorthands { readonly "font-variant": Shorthand<"font-variant-caps" | "font-variant-east-asian" | "font-variant-ligatures" | "font-variant-numeric">; readonly "inset-block": Shorthand<"inset-block-end" | "inset-block-start">; readonly "inset-inline": Shorthand<"inset-inline-end" | "inset-inline-start">; - readonly inset: Shorthand<"left" | "right" | "top" | "bottom">; + readonly inset: Shorthand<"top" | "bottom" | "left" | "right">; readonly margin: Shorthand<"margin-bottom" | "margin-left" | "margin-right" | "margin-top">; readonly mask: Shorthand<"mask-image" | "mask-clip" | "mask-composite" | "mask-mode" | "mask-origin" | "mask-position" | "mask-repeat" | "mask-size">; readonly outline: Shorthand<"outline-color" | "outline-style" | "outline-width">; From 8e34ce8665e0df86df5c6f5b0abb941b65154f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:25:36 +0100 Subject: [PATCH 18/36] Use `Cache.memoize` --- .../alfa-painting-order/src/painting-order.ts | 349 +++++++++--------- 1 file changed, 174 insertions(+), 175 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index a8991a1160..c0537a32b4 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -12,8 +12,6 @@ const { hasComputedStyle, isRendered, isFlexOrGridChild } = Style; import { createsStackingContext } from "./predicate/creates-stacking-context.js"; -const cache = Cache.empty>>(); - /** * Computes the painting order of the element descendants of a root element and * returns said elements in the order in which they would be painted onto the screen. @@ -30,211 +28,212 @@ const cache = Cache.empty>>(); * * @public */ -export function computePaintingOrder( - root: Element, - device: Device, -): Sequence { - const isPositioned = hasComputedStyle( - "position", - (position) => position.value !== "static", - device, - ); - const hasAutoZIndex = hasComputedStyle( - "z-index", - ({ value }) => value === "auto", - device, - ); - const isBlockLevel = hasComputedStyle( - "display", - ({ values: [outside, inside, listItem] }) => - outside.value === "block" || - inside?.value === "table" || - inside?.value === "flex" || - inside?.value === "grid" || - listItem?.value === "list-item", - device, - ); - const isFloat = hasComputedStyle( - "float", - ({ value }) => value !== "none", - device, - ); - const createsSC = createsStackingContext(device); - const rendered = isRendered(device); - - const getZLevel = (element: Element) => { - // If the element is not positioned and not a flex child, setting a z-index - // wont affect the z-level. - if (and(not(isPositioned), not(isFlexOrGridChild(device)))(element)) { - return 0; - } +export const computePaintingOrder = Cache.memoize( + (root: Element, device: Device): Sequence => { + const isPositioned = hasComputedStyle( + "position", + (position) => position.value !== "static", + device, + ); + const hasAutoZIndex = hasComputedStyle( + "z-index", + ({ value }) => value === "auto", + device, + ); + const isBlockLevel = hasComputedStyle( + "display", + ({ values: [outside, inside, listItem] }) => + outside.value === "block" || + inside?.value === "table" || + inside?.value === "flex" || + inside?.value === "grid" || + listItem?.value === "list-item", + device, + ); + const isFloat = hasComputedStyle( + "float", + ({ value }) => value !== "none", + device, + ); + const createsSC = createsStackingContext(device); + const rendered = isRendered(device); + + const getZLevel = (element: Element) => { + // If the element is not positioned and not a flex child, setting a z-index + // wont affect the z-level. + if (and(not(isPositioned), not(isFlexOrGridChild(device)))(element)) { + return 0; + } - const { - value: { value }, - } = Style.from(element, device).computed("z-index"); - - return value === "auto" ? 0 : value; - }; - - function paint( - element: Element, - canvas: Array, - options: { defer?: boolean } = { - defer: false, - }, - ): void { - const { defer = false } = options; - const positionedOrStackingContexts: Array = []; - const blockLevels: Array = []; - const floats: Array = []; - const inlines: Array = []; - - /** - * @remarks - * Positioned elements with z-index: auto and floating elements are treated - * as if they create stacking contexts, but their positioned descendants - * and descendants that create stacking contexts should be considered part - * of the parent stacking context, i.e. we need to compute the painting - * order of such subtrees, without recursing into positioned descendants - * and descendants creating stacking contexts, then iterate the result and - * distribute said descendants into layers at this level and add the float - * itself and the other descendants to the floats layer. - */ - function distributeIntoLayers(element: Element) { - if (or(isFlexOrGridChild(device), createsSC)(element)) { - positionedOrStackingContexts.push(element); - } else if (isPositioned(element)) { - if (hasAutoZIndex(element)) { + const { + value: { value }, + } = Style.from(element, device).computed("z-index"); + + return value === "auto" ? 0 : value; + }; + + function paint( + element: Element, + canvas: Array, + options: { defer?: boolean } = { + defer: false, + }, + ): void { + const { defer = false } = options; + const positionedOrStackingContexts: Array = []; + const blockLevels: Array = []; + const floats: Array = []; + const inlines: Array = []; + + /** + * @remarks + * Positioned elements with z-index: auto and floating elements are treated + * as if they create stacking contexts, but their positioned descendants + * and descendants that create stacking contexts should be considered part + * of the parent stacking context, i.e. we need to compute the painting + * order of such subtrees, without recursing into positioned descendants + * and descendants creating stacking contexts, then iterate the result and + * distribute said descendants into layers at this level and add the float + * itself and the other descendants to the floats layer. + */ + function distributeIntoLayers(element: Element) { + if (or(isFlexOrGridChild(device), createsSC)(element)) { + positionedOrStackingContexts.push(element); + } else if (isPositioned(element)) { + if (hasAutoZIndex(element)) { + const temporaryLayer: Array = []; + paint(element, temporaryLayer, { defer: true }); + + for (const descendant of temporaryLayer) { + if (or(isPositioned, createsSC)(descendant)) { + if (or(isPositioned, createsSC)(descendant)) { + positionedOrStackingContexts.push(descendant); + } else if (isFloat(descendant)) { + floats.push(descendant); + } else if (isBlockLevel(descendant)) { + blockLevels.push(descendant); + } else { + inlines.push(descendant); + } + } else { + positionedOrStackingContexts.push(descendant); + } + } + } else { + positionedOrStackingContexts.push(element); + } + } else if (isFloat(element)) { const temporaryLayer: Array = []; paint(element, temporaryLayer, { defer: true }); for (const descendant of temporaryLayer) { if (or(isPositioned, createsSC)(descendant)) { - if (or(isPositioned, createsSC)(descendant)) { - positionedOrStackingContexts.push(descendant); - } else if (isFloat(descendant)) { - floats.push(descendant); - } else if (isBlockLevel(descendant)) { - blockLevels.push(descendant); - } else { - inlines.push(descendant); - } - } else { positionedOrStackingContexts.push(descendant); + } else { + floats.push(descendant); } } + } else if (isBlockLevel(element)) { + blockLevels.push(element); } else { - positionedOrStackingContexts.push(element); + // everything else, this is somewhat crude and might not be accurate, but + // will do for now. + inlines.push(element); } - } else if (isFloat(element)) { - const temporaryLayer: Array = []; - paint(element, temporaryLayer, { defer: true }); + } - for (const descendant of temporaryLayer) { - if (or(isPositioned, createsSC)(descendant)) { - positionedOrStackingContexts.push(descendant); - } else { - floats.push(descendant); - } - } - } else if (isBlockLevel(element)) { - blockLevels.push(element); + // Block-level elements, forming a stacking context, are painted before + // their descendants. Inline-level elements, forming a stacking context, + // are painted in the inline layer before its inline descendants + // (and before stacking-context-creating and positioned descendants with + // stack level greater than or equal to 0), but after positioned descendants + // with negative z-index, block-level descendants and floating descendants. + if (isBlockLevel(element)) { + canvas.push(element); } else { - // everything else, this is somewhat crude and might not be accurate, but - // will do for now. inlines.push(element); } - } - // Block-level elements, forming a stacking context, are painted before - // their descendants. Inline-level elements, forming a stacking context, - // are painted in the inline layer before its inline descendants - // (and before stacking-context-creating and positioned descendants with - // stack level greater than or equal to 0), but after positioned descendants - // with negative z-index, block-level descendants and floating descendants. - if (isBlockLevel(element)) { - canvas.push(element); - } else { - inlines.push(element); - } + function traverse(element: Element) { + for (const child of element + .children(Node.fullTree) + .filter(and(Element.isElement, rendered))) { + distributeIntoLayers(child); - function traverse(element: Element) { - for (const child of element - .children(Node.fullTree) - .filter(and(Element.isElement, rendered))) { - distributeIntoLayers(child); + if (or(isPositioned, isFloat, createsSC)(child)) { + // The child is going to be painted in full or partial isolation, so + // we need to stop descending. + continue; + } - if (or(isPositioned, isFloat, createsSC)(child)) { - // The child is going to be painted in full or partial isolation, so - // we need to stop descending. - continue; + traverse(child); } - - traverse(child); } - } - traverse(element); - - positionedOrStackingContexts.sort((a: Element, b: Element) => - Comparable.compare(getZLevel(a), getZLevel(b)), - ); - - // If the defer is true, painting of descendant stacking contexts should - // be deferred i.e. the element should just be added to the canvas, (which - // should be a temporary canvas). - let posDescIndex = 0; - for ( - ; - posDescIndex < positionedOrStackingContexts.length && - getZLevel(positionedOrStackingContexts[posDescIndex]) < 0; - ++posDescIndex - ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element) { - paint(posOrSC, canvas); - } else { - canvas.push(posOrSC); + traverse(element); + + positionedOrStackingContexts.sort((a: Element, b: Element) => + Comparable.compare(getZLevel(a), getZLevel(b)), + ); + + // If the defer is true, painting of descendant stacking contexts should + // be deferred i.e. the element should just be added to the canvas, (which + // should be a temporary canvas). + let posDescIndex = 0; + for ( + ; + posDescIndex < positionedOrStackingContexts.length && + getZLevel(positionedOrStackingContexts[posDescIndex]) < 0; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); + } } - } - for (const blockLevel of blockLevels) { - if (!defer && createsSC(blockLevel)) { - paint(blockLevel, canvas); - } else { - canvas.push(blockLevel); + for (const blockLevel of blockLevels) { + if (!defer && createsSC(blockLevel)) { + paint(blockLevel, canvas); + } else { + canvas.push(blockLevel); + } } - } - for (const float of floats) { - if (!defer && float !== element && createsSC(float)) { - paint(float, canvas); - } else { - canvas.push(float); + for (const float of floats) { + if (!defer && float !== element && createsSC(float)) { + paint(float, canvas); + } else { + canvas.push(float); + } } - } - for (const inline of inlines) { - if (!defer && inline !== element && createsSC(inline)) { - paint(inline, canvas); - } else { - canvas.push(inline); + for (const inline of inlines) { + if (!defer && inline !== element && createsSC(inline)) { + paint(inline, canvas); + } else { + canvas.push(inline); + } } - } - for (; posDescIndex < positionedOrStackingContexts.length; ++posDescIndex) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element && createsSC(posOrSC)) { - paint(posOrSC, canvas); - } else { - canvas.push(posOrSC); + for ( + ; + posDescIndex < positionedOrStackingContexts.length; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element && createsSC(posOrSC)) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); + } } } - } - return cache.get(device, Cache.empty).get(root, () => { const canvas: Array = []; paint(root, canvas); return Sequence.from(canvas); - }); -} + }, +); From 9a1d55edd595a756fdc77f7dfa697d6b56621e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:15:57 +0100 Subject: [PATCH 19/36] Add `PaintingOrder` class --- .../alfa-painting-order/src/painting-order.ts | 429 ++++++++++-------- .../test/painting-order.spec.tsx | 175 ++----- 2 files changed, 279 insertions(+), 325 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index c0537a32b4..6fd24751a4 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -3,237 +3,288 @@ import { Cache } from "@siteimprove/alfa-cache"; import { Comparable } from "@siteimprove/alfa-comparable"; import type { Device } from "@siteimprove/alfa-device"; import { Element, Node } from "@siteimprove/alfa-dom"; +import type { Equatable } from "@siteimprove/alfa-equatable"; +import type { Hash, Hashable } from "@siteimprove/alfa-hash"; +import { Iterable } from "@siteimprove/alfa-iterable"; +import type { Serializable } from "@siteimprove/alfa-json"; import { Refinement } from "@siteimprove/alfa-refinement"; -import { Sequence } from "@siteimprove/alfa-sequence"; import { Style } from "@siteimprove/alfa-style"; +import * as json from "@siteimprove/alfa-json"; + const { and, not, or } = Refinement; const { hasComputedStyle, isRendered, isFlexOrGridChild } = Style; import { createsStackingContext } from "./predicate/creates-stacking-context.js"; -/** - * Computes the painting order of the element descendants of a root element and - * returns said elements in the order in which they would be painted onto the screen. - * - * {@link https://drafts.csswg.org/css2/#elaborate-stacking-contexts} - * - * @remarks - * The painting order of flex children is different from the usual painting - * order, but we currently do not observe these differences. The assumption is - * that flex children, unless they are positioned or has z-index, should never - * overlap, so it's acceptable that we do not get their relative order right as - * long as the flex container itself is ordered correctly. - * {@link https://www.w3.org/TR/css-flexbox-1/#painting}. - * - * @public - */ -export const computePaintingOrder = Cache.memoize( - (root: Element, device: Device): Sequence => { - const isPositioned = hasComputedStyle( - "position", - (position) => position.value !== "static", - device, - ); - const hasAutoZIndex = hasComputedStyle( - "z-index", - ({ value }) => value === "auto", - device, - ); - const isBlockLevel = hasComputedStyle( - "display", - ({ values: [outside, inside, listItem] }) => - outside.value === "block" || - inside?.value === "table" || - inside?.value === "flex" || - inside?.value === "grid" || - listItem?.value === "list-item", - device, - ); - const isFloat = hasComputedStyle( - "float", - ({ value }) => value !== "none", - device, - ); - const createsSC = createsStackingContext(device); - const rendered = isRendered(device); - - const getZLevel = (element: Element) => { - // If the element is not positioned and not a flex child, setting a z-index - // wont affect the z-level. - if (and(not(isPositioned), not(isFlexOrGridChild(device)))(element)) { - return 0; - } +export class PaintingOrder + implements Equatable, Hashable, Serializable +{ + public static of(elements: Iterable): PaintingOrder { + return new PaintingOrder(Array.from(elements)); + } + + private readonly _elements: Array; - const { - value: { value }, - } = Style.from(element, device).computed("z-index"); + protected constructor(elements: Array) { + this._elements = elements; + } + + public equals(value: this): boolean; + public equals(value: unknown): value is this; + public equals(value: unknown): boolean { + return ( + value === this || + (PaintingOrder.isPaintingOrder(value) && + Array.equals(value._elements, this._elements)) + ); + } + public hash(hash: Hash): void { + Array.hash(this._elements, hash); + } - return value === "auto" ? 0 : value; + public toJSON(options?: Serializable.Options): PaintingOrder.JSON { + return { + type: "painting-order", + elements: Array.toJSON(this._elements, options), }; + } +} - function paint( - element: Element, - canvas: Array, - options: { defer?: boolean } = { - defer: false, - }, - ): void { - const { defer = false } = options; - const positionedOrStackingContexts: Array = []; - const blockLevels: Array = []; - const floats: Array = []; - const inlines: Array = []; - - /** - * @remarks - * Positioned elements with z-index: auto and floating elements are treated - * as if they create stacking contexts, but their positioned descendants - * and descendants that create stacking contexts should be considered part - * of the parent stacking context, i.e. we need to compute the painting - * order of such subtrees, without recursing into positioned descendants - * and descendants creating stacking contexts, then iterate the result and - * distribute said descendants into layers at this level and add the float - * itself and the other descendants to the floats layer. - */ - function distributeIntoLayers(element: Element) { - if (or(isFlexOrGridChild(device), createsSC)(element)) { - positionedOrStackingContexts.push(element); - } else if (isPositioned(element)) { - if (hasAutoZIndex(element)) { +export namespace PaintingOrder { + export type JSON = { + [key: string]: json.JSON; + type: "painting-order"; + elements: Array; + }; + + export function isPaintingOrder(value: unknown): value is PaintingOrder { + return value instanceof PaintingOrder; + } + + /** + * Computes the painting order of the element descendants of a root element and + * returns said elements in the order in which they would be painted onto the screen. + * + * {@link https://drafts.csswg.org/css2/#elaborate-stacking-contexts} + * + * @remarks + * The painting order of flex children is different from the usual painting + * order, but we currently do not observe these differences. The assumption is + * that flex children, unless they are positioned or has z-index, should never + * overlap, so it's acceptable that we do not get their relative order right as + * long as the flex container itself is ordered correctly. + * {@link https://www.w3.org/TR/css-flexbox-1/#painting}. + * + * @public + */ + export const from = Cache.memoize( + (root: Element, device: Device): PaintingOrder => { + const isPositioned = hasComputedStyle( + "position", + (position) => position.value !== "static", + device, + ); + const hasAutoZIndex = hasComputedStyle( + "z-index", + ({ value }) => value === "auto", + device, + ); + const isBlockLevel = hasComputedStyle( + "display", + ({ values: [outside, inside, listItem] }) => + outside.value === "block" || + inside?.value === "table" || + inside?.value === "flex" || + inside?.value === "grid" || + listItem?.value === "list-item", + device, + ); + const isFloat = hasComputedStyle( + "float", + ({ value }) => value !== "none", + device, + ); + const createsSC = createsStackingContext(device); + const rendered = isRendered(device); + + const getZLevel = (element: Element) => { + // If the element is not positioned and not a flex child, setting a z-index + // wont affect the z-level. + if (and(not(isPositioned), not(isFlexOrGridChild(device)))(element)) { + return 0; + } + + const { + value: { value }, + } = Style.from(element, device).computed("z-index"); + + return value === "auto" ? 0 : value; + }; + + function paint( + element: Element, + canvas: Array, + options: { defer?: boolean } = { + defer: false, + }, + ): void { + const { defer = false } = options; + const positionedOrStackingContexts: Array = []; + const blockLevels: Array = []; + const floats: Array = []; + const inlines: Array = []; + + /** + * @remarks + * Positioned elements with z-index: auto and floating elements are treated + * as if they create stacking contexts, but their positioned descendants + * and descendants that create stacking contexts should be considered part + * of the parent stacking context, i.e. we need to compute the painting + * order of such subtrees, without recursing into positioned descendants + * and descendants creating stacking contexts, then iterate the result and + * distribute said descendants into layers at this level and add the float + * itself and the other descendants to the floats layer. + */ + function distributeIntoLayers(element: Element) { + if (or(isFlexOrGridChild(device), createsSC)(element)) { + positionedOrStackingContexts.push(element); + } else if (isPositioned(element)) { + if (hasAutoZIndex(element)) { + const temporaryLayer: Array = []; + paint(element, temporaryLayer, { defer: true }); + + for (const descendant of temporaryLayer) { + if (or(isPositioned, createsSC)(descendant)) { + if (or(isPositioned, createsSC)(descendant)) { + positionedOrStackingContexts.push(descendant); + } else if (isFloat(descendant)) { + floats.push(descendant); + } else if (isBlockLevel(descendant)) { + blockLevels.push(descendant); + } else { + inlines.push(descendant); + } + } else { + positionedOrStackingContexts.push(descendant); + } + } + } else { + positionedOrStackingContexts.push(element); + } + } else if (isFloat(element)) { const temporaryLayer: Array = []; paint(element, temporaryLayer, { defer: true }); for (const descendant of temporaryLayer) { if (or(isPositioned, createsSC)(descendant)) { - if (or(isPositioned, createsSC)(descendant)) { - positionedOrStackingContexts.push(descendant); - } else if (isFloat(descendant)) { - floats.push(descendant); - } else if (isBlockLevel(descendant)) { - blockLevels.push(descendant); - } else { - inlines.push(descendant); - } - } else { positionedOrStackingContexts.push(descendant); + } else { + floats.push(descendant); } } + } else if (isBlockLevel(element)) { + blockLevels.push(element); } else { - positionedOrStackingContexts.push(element); + // everything else, this is somewhat crude and might not be accurate, but + // will do for now. + inlines.push(element); } - } else if (isFloat(element)) { - const temporaryLayer: Array = []; - paint(element, temporaryLayer, { defer: true }); + } - for (const descendant of temporaryLayer) { - if (or(isPositioned, createsSC)(descendant)) { - positionedOrStackingContexts.push(descendant); - } else { - floats.push(descendant); - } - } - } else if (isBlockLevel(element)) { - blockLevels.push(element); + // Block-level elements, forming a stacking context, are painted before + // their descendants. Inline-level elements, forming a stacking context, + // are painted in the inline layer before its inline descendants + // (and before stacking-context-creating and positioned descendants with + // stack level greater than or equal to 0), but after positioned descendants + // with negative z-index, block-level descendants and floating descendants. + if (isBlockLevel(element)) { + canvas.push(element); } else { - // everything else, this is somewhat crude and might not be accurate, but - // will do for now. inlines.push(element); } - } - // Block-level elements, forming a stacking context, are painted before - // their descendants. Inline-level elements, forming a stacking context, - // are painted in the inline layer before its inline descendants - // (and before stacking-context-creating and positioned descendants with - // stack level greater than or equal to 0), but after positioned descendants - // with negative z-index, block-level descendants and floating descendants. - if (isBlockLevel(element)) { - canvas.push(element); - } else { - inlines.push(element); - } + function traverse(element: Element) { + for (const child of element + .children(Node.fullTree) + .filter(and(Element.isElement, rendered))) { + distributeIntoLayers(child); - function traverse(element: Element) { - for (const child of element - .children(Node.fullTree) - .filter(and(Element.isElement, rendered))) { - distributeIntoLayers(child); + if (or(isPositioned, isFloat, createsSC)(child)) { + // The child is going to be painted in full or partial isolation, so + // we need to stop descending. + continue; + } - if (or(isPositioned, isFloat, createsSC)(child)) { - // The child is going to be painted in full or partial isolation, so - // we need to stop descending. - continue; + traverse(child); } - - traverse(child); } - } - traverse(element); + traverse(element); - positionedOrStackingContexts.sort((a: Element, b: Element) => - Comparable.compare(getZLevel(a), getZLevel(b)), - ); + positionedOrStackingContexts.sort((a: Element, b: Element) => + Comparable.compare(getZLevel(a), getZLevel(b)), + ); - // If the defer is true, painting of descendant stacking contexts should - // be deferred i.e. the element should just be added to the canvas, (which - // should be a temporary canvas). - let posDescIndex = 0; - for ( - ; - posDescIndex < positionedOrStackingContexts.length && - getZLevel(positionedOrStackingContexts[posDescIndex]) < 0; - ++posDescIndex - ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element) { - paint(posOrSC, canvas); - } else { - canvas.push(posOrSC); + // If the defer is true, painting of descendant stacking contexts should + // be deferred i.e. the element should just be added to the canvas, (which + // should be a temporary canvas). + let posDescIndex = 0; + for ( + ; + posDescIndex < positionedOrStackingContexts.length && + getZLevel(positionedOrStackingContexts[posDescIndex]) < 0; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); + } } - } - for (const blockLevel of blockLevels) { - if (!defer && createsSC(blockLevel)) { - paint(blockLevel, canvas); - } else { - canvas.push(blockLevel); + for (const blockLevel of blockLevels) { + if (!defer && createsSC(blockLevel)) { + paint(blockLevel, canvas); + } else { + canvas.push(blockLevel); + } } - } - for (const float of floats) { - if (!defer && float !== element && createsSC(float)) { - paint(float, canvas); - } else { - canvas.push(float); + for (const float of floats) { + if (!defer && float !== element && createsSC(float)) { + paint(float, canvas); + } else { + canvas.push(float); + } } - } - for (const inline of inlines) { - if (!defer && inline !== element && createsSC(inline)) { - paint(inline, canvas); - } else { - canvas.push(inline); + for (const inline of inlines) { + if (!defer && inline !== element && createsSC(inline)) { + paint(inline, canvas); + } else { + canvas.push(inline); + } } - } - for ( - ; - posDescIndex < positionedOrStackingContexts.length; - ++posDescIndex - ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element && createsSC(posOrSC)) { - paint(posOrSC, canvas); - } else { - canvas.push(posOrSC); + for ( + ; + posDescIndex < positionedOrStackingContexts.length; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element && createsSC(posOrSC)) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); + } } } - } - const canvas: Array = []; - paint(root, canvas); + const canvas: Array = []; + paint(root, canvas); - return Sequence.from(canvas); - }, -); + return PaintingOrder.of(canvas); + }, + ); +} diff --git a/packages/alfa-painting-order/test/painting-order.spec.tsx b/packages/alfa-painting-order/test/painting-order.spec.tsx index 1e775278e9..f57d0552bd 100644 --- a/packages/alfa-painting-order/test/painting-order.spec.tsx +++ b/packages/alfa-painting-order/test/painting-order.spec.tsx @@ -1,22 +1,30 @@ -import { Device } from "@siteimprove/alfa-device"; import { h } from "@siteimprove/alfa-dom"; -import { Sequence } from "@siteimprove/alfa-sequence"; -import { test } from "@siteimprove/alfa-test"; +import { test, type Assertions } from "@siteimprove/alfa-test"; + +import { Array } from "@siteimprove/alfa-array"; +import { Device } from "@siteimprove/alfa-device"; +import type { Element } from "@siteimprove/alfa-dom"; +import { Serializable } from "@siteimprove/alfa-json"; -import { computePaintingOrder } from "../dist/painting-order.js"; +import { PaintingOrder } from "../dist/painting-order.js"; const device = Device.standard(); +const options = { verbosity: Serializable.Verbosity.Low }; + +function testOrder(t: Assertions, root: Element, expected: Array) { + h.document([root]); + + t.deepEqual(PaintingOrder.from(root, device).toJSON(options), { + type: "painting-order", + elements: Array.toJSON(expected, options), + }); +} test("block-level element is painted before positioned descendant with negative z-index", (t) => { const div =
; const body = {div}; - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div]).toJSON(), - ); + testOrder(t, body, [body, div]); }); test("block-level stacking context element is painted before positioned descendant with negative z-index", (t) => { @@ -24,12 +32,7 @@ test("block-level stacking context element is painted before positioned descenda const div1 =
{div2}
; const body = {div1}; - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div2]).toJSON(), - ); + testOrder(t, body, [body, div1, div2]); }); test("positioned elements with negative z-index are painted in z-order then tree-order", (t) => { @@ -44,12 +47,7 @@ test("positioned elements with negative z-index are painted in z-order then tree ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div3, div1, div2]).toJSON(), - ); + testOrder(t, body, [body, div3, div1, div2]); }); test("positioned element with negative z-index is painted before block-level element", (t) => { @@ -62,12 +60,7 @@ test("positioned element with negative z-index is painted before block-level ele ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div1]); }); test("block-level descendants are painted in tree-order", (t) => { @@ -81,12 +74,7 @@ test("block-level descendants are painted in tree-order", (t) => { ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div2, div3]).toJSON(), - ); + testOrder(t, body, [body, div1, div2, div3]); }); test("block-level elements are painted before floating elements", (t) => { @@ -99,12 +87,7 @@ test("block-level elements are painted before floating elements", (t) => { ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div1]); }); test("floating descendants are painted in tree-order", (t) => { @@ -118,12 +101,7 @@ test("floating descendants are painted in tree-order", (t) => { ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div2, div3]).toJSON(), - ); + testOrder(t, body, [body, div1, div2, div3]); }); test("floating descendants are painted atomically with respect to block-level descendants", (t) => { @@ -142,12 +120,7 @@ test("floating descendants are painted atomically with respect to block-level de ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div3, div1, div2]).toJSON(), - ); + testOrder(t, body, [body, div3, div1, div2]); }); test("floating descendants are not painted atomically with respect to positioned descendants", (t) => { @@ -161,12 +134,7 @@ test("floating descendants are not painted atomically with respect to positioned ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1, div3]).toJSON(), - ); + testOrder(t, body, [body, div2, div1, div3]); }); test("floating descendants are painted before inline-level descendants", (t) => { @@ -179,12 +147,7 @@ test("floating descendants are painted before inline-level descendants", (t) => ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div1]); }); test("inline-level descendants are painted in tree-order", (t) => { @@ -198,12 +161,7 @@ test("inline-level descendants are painted in tree-order", (t) => { ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, span1, span2, span3]).toJSON(), - ); + testOrder(t, body, [body, span1, span2, span3]); }); test("inline-level descendants are painted before positioned elements", (t) => { @@ -216,12 +174,7 @@ test("inline-level descendants are painted before positioned elements", (t) => { ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div1]); }); test("positioned descendants with z-index: auto and non-positioned elements that create stacking contexts are painted in tree-order", (t) => { @@ -237,12 +190,7 @@ test("positioned descendants with z-index: auto and non-positioned elements that ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div2, div3, div4]).toJSON(), - ); + testOrder(t, body, [body, div1, div2, div3, div4]); }); test("positioned descendants are painted atomically with respect to block-level descendants", (t) => { @@ -261,12 +209,7 @@ test("positioned descendants are painted atomically with respect to block-level ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div3, div1, div2]).toJSON(), - ); + testOrder(t, body, [body, div3, div1, div2]); }); test("positioned descendants are not painted atomically with respect to positioned descendants with positive z-indices", (t) => { @@ -280,12 +223,7 @@ test("positioned descendants are not painted atomically with respect to position ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1, div3]).toJSON(), - ); + testOrder(t, body, [body, div2, div1, div3]); }); test("positioned elements without z-index are painted before positioned elements with positve z-index", (t) => { @@ -298,12 +236,7 @@ test("positioned elements without z-index are painted before positioned elements ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div1]); }); test("positioned elements with positive z-index are painted in z-order then tree-order", (t) => { @@ -318,12 +251,7 @@ test("positioned elements with positive z-index are painted in z-order then tree ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div3, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div3, div1]); }); test("inline-level stacking context element is painted after floating descendants and before inline-level descendants", (t) => { @@ -337,12 +265,7 @@ test("inline-level stacking context element is painted after floating descendant ); const body = {div1}; - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div1, div3]).toJSON(), - ); + testOrder(t, body, [body, div2, div1, div3]); }); test("stacking context creating elements are painted atomically", (t) => { @@ -359,12 +282,7 @@ test("stacking context creating elements are painted atomically", (t) => { ); - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div2, div3, div1]).toJSON(), - ); + testOrder(t, body, [body, div2, div3, div1]); }); test("non-positioned elements are not affected by z-index", (t) => { @@ -378,12 +296,7 @@ test("non-positioned elements are not affected by z-index", (t) => { ); const body = {div1}; - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div2, div3]).toJSON(), - ); + testOrder(t, body, [body, div1, div2, div3]); }); test("flex children are affected by z-index", (t) => { @@ -397,12 +310,7 @@ test("flex children are affected by z-index", (t) => { ); const body = {div1}; - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div3, div2]).toJSON(), - ); + testOrder(t, body, [body, div1, div3, div2]); }); test("grid children are affected by z-index", (t) => { @@ -416,10 +324,5 @@ test("grid children are affected by z-index", (t) => { ); const body = {div1}; - h.document([body]); - - t.deepEqual( - computePaintingOrder(body, device).toJSON(), - Sequence.from([body, div1, div3, div2]).toJSON(), - ); + testOrder(t, body, [body, div1, div3, div2]); }); From 2fce5331e388f822e2cd9116d2d32f390921367e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:30:27 +0100 Subject: [PATCH 20/36] Extract local functions --- .../alfa-painting-order/src/painting-order.ts | 125 +++++++++--------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 6fd24751a4..fd47632de9 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -80,48 +80,6 @@ export namespace PaintingOrder { */ export const from = Cache.memoize( (root: Element, device: Device): PaintingOrder => { - const isPositioned = hasComputedStyle( - "position", - (position) => position.value !== "static", - device, - ); - const hasAutoZIndex = hasComputedStyle( - "z-index", - ({ value }) => value === "auto", - device, - ); - const isBlockLevel = hasComputedStyle( - "display", - ({ values: [outside, inside, listItem] }) => - outside.value === "block" || - inside?.value === "table" || - inside?.value === "flex" || - inside?.value === "grid" || - listItem?.value === "list-item", - device, - ); - const isFloat = hasComputedStyle( - "float", - ({ value }) => value !== "none", - device, - ); - const createsSC = createsStackingContext(device); - const rendered = isRendered(device); - - const getZLevel = (element: Element) => { - // If the element is not positioned and not a flex child, setting a z-index - // wont affect the z-level. - if (and(not(isPositioned), not(isFlexOrGridChild(device)))(element)) { - return 0; - } - - const { - value: { value }, - } = Style.from(element, device).computed("z-index"); - - return value === "auto" ? 0 : value; - }; - function paint( element: Element, canvas: Array, @@ -147,20 +105,20 @@ export namespace PaintingOrder { * itself and the other descendants to the floats layer. */ function distributeIntoLayers(element: Element) { - if (or(isFlexOrGridChild(device), createsSC)(element)) { + if (or(isFlexOrGridChild(device), createsSC(device))(element)) { positionedOrStackingContexts.push(element); - } else if (isPositioned(element)) { - if (hasAutoZIndex(element)) { + } else if (isPositioned(device)(element)) { + if (hasAutoZIndex(device)(element)) { const temporaryLayer: Array = []; paint(element, temporaryLayer, { defer: true }); for (const descendant of temporaryLayer) { - if (or(isPositioned, createsSC)(descendant)) { - if (or(isPositioned, createsSC)(descendant)) { + if (or(isPositioned(device), createsSC(device))(descendant)) { + if (or(isPositioned(device), createsSC(device))(descendant)) { positionedOrStackingContexts.push(descendant); - } else if (isFloat(descendant)) { + } else if (isFloat(device)(descendant)) { floats.push(descendant); - } else if (isBlockLevel(descendant)) { + } else if (isBlockLevel(device)(descendant)) { blockLevels.push(descendant); } else { inlines.push(descendant); @@ -172,18 +130,18 @@ export namespace PaintingOrder { } else { positionedOrStackingContexts.push(element); } - } else if (isFloat(element)) { + } else if (isFloat(device)(element)) { const temporaryLayer: Array = []; paint(element, temporaryLayer, { defer: true }); for (const descendant of temporaryLayer) { - if (or(isPositioned, createsSC)(descendant)) { + if (or(isPositioned(device), createsSC(device))(descendant)) { positionedOrStackingContexts.push(descendant); } else { floats.push(descendant); } } - } else if (isBlockLevel(element)) { + } else if (isBlockLevel(device)(element)) { blockLevels.push(element); } else { // everything else, this is somewhat crude and might not be accurate, but @@ -198,7 +156,7 @@ export namespace PaintingOrder { // (and before stacking-context-creating and positioned descendants with // stack level greater than or equal to 0), but after positioned descendants // with negative z-index, block-level descendants and floating descendants. - if (isBlockLevel(element)) { + if (isBlockLevel(device)(element)) { canvas.push(element); } else { inlines.push(element); @@ -207,10 +165,16 @@ export namespace PaintingOrder { function traverse(element: Element) { for (const child of element .children(Node.fullTree) - .filter(and(Element.isElement, rendered))) { + .filter(and(Element.isElement, rendered(device)))) { distributeIntoLayers(child); - if (or(isPositioned, isFloat, createsSC)(child)) { + if ( + or( + isPositioned(device), + isFloat(device), + createsSC(device), + )(child) + ) { // The child is going to be painted in full or partial isolation, so // we need to stop descending. continue; @@ -222,7 +186,7 @@ export namespace PaintingOrder { traverse(element); positionedOrStackingContexts.sort((a: Element, b: Element) => - Comparable.compare(getZLevel(a), getZLevel(b)), + Comparable.compare(getZLevel(device, a), getZLevel(device, b)), ); // If the defer is true, painting of descendant stacking contexts should @@ -232,7 +196,7 @@ export namespace PaintingOrder { for ( ; posDescIndex < positionedOrStackingContexts.length && - getZLevel(positionedOrStackingContexts[posDescIndex]) < 0; + getZLevel(device, positionedOrStackingContexts[posDescIndex]) < 0; ++posDescIndex ) { const posOrSC = positionedOrStackingContexts[posDescIndex]; @@ -244,7 +208,7 @@ export namespace PaintingOrder { } for (const blockLevel of blockLevels) { - if (!defer && createsSC(blockLevel)) { + if (!defer && createsSC(device)(blockLevel)) { paint(blockLevel, canvas); } else { canvas.push(blockLevel); @@ -252,7 +216,7 @@ export namespace PaintingOrder { } for (const float of floats) { - if (!defer && float !== element && createsSC(float)) { + if (!defer && float !== element && createsSC(device)(float)) { paint(float, canvas); } else { canvas.push(float); @@ -260,7 +224,7 @@ export namespace PaintingOrder { } for (const inline of inlines) { - if (!defer && inline !== element && createsSC(inline)) { + if (!defer && inline !== element && createsSC(device)(inline)) { paint(inline, canvas); } else { canvas.push(inline); @@ -273,7 +237,7 @@ export namespace PaintingOrder { ++posDescIndex ) { const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element && createsSC(posOrSC)) { + if (!defer && posOrSC !== element && createsSC(device)(posOrSC)) { paint(posOrSC, canvas); } else { canvas.push(posOrSC); @@ -287,4 +251,43 @@ export namespace PaintingOrder { return PaintingOrder.of(canvas); }, ); + const isPositioned = (device: Device) => + hasComputedStyle( + "position", + (position) => position.value !== "static", + device, + ); + const hasAutoZIndex = (device: Device) => + hasComputedStyle("z-index", ({ value }) => value === "auto", device); + const isBlockLevel = (device: Device) => + hasComputedStyle( + "display", + ({ values: [outside, inside, listItem] }) => + outside.value === "block" || + inside?.value === "table" || + inside?.value === "flex" || + inside?.value === "grid" || + listItem?.value === "list-item", + device, + ); + const isFloat = (device: Device) => + hasComputedStyle("float", ({ value }) => value !== "none", device); + const createsSC = (device: Device) => createsStackingContext(device); + const rendered = (device: Device) => isRendered(device); + + const getZLevel = (device: Device, element: Element) => { + // If the element is not positioned and not a flex child, setting a z-index + // wont affect the z-level. + if ( + and(not(isPositioned(device)), not(isFlexOrGridChild(device)))(element) + ) { + return 0; + } + + const { + value: { value }, + } = Style.from(element, device).computed("z-index"); + + return value === "auto" ? 0 : value; + }; } From ef476f965e2850bd39f4a04f9fcdb52ba50f1c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:54:50 +0100 Subject: [PATCH 21/36] Use alfa-style predicates --- .../alfa-painting-order/src/painting-order.ts | 350 +++++++++--------- .../src/element/predicate/is-positioned.ts | 3 +- packages/alfa-style/src/property/position.ts | 20 +- 3 files changed, 199 insertions(+), 174 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index fd47632de9..72aa4ebfb5 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -13,7 +13,13 @@ import { Style } from "@siteimprove/alfa-style"; import * as json from "@siteimprove/alfa-json"; const { and, not, or } = Refinement; -const { hasComputedStyle, isRendered, isFlexOrGridChild } = Style; +const { + hasInitialComputedStyle, + isBlockContainer, + isFlexOrGridChild, + isPositioned, + isRendered, +} = Style; import { createsStackingContext } from "./predicate/creates-stacking-context.js"; @@ -78,208 +84,220 @@ export namespace PaintingOrder { * * @public */ - export const from = Cache.memoize( - (root: Element, device: Device): PaintingOrder => { - function paint( - element: Element, - canvas: Array, - options: { defer?: boolean } = { - defer: false, - }, - ): void { - const { defer = false } = options; - const positionedOrStackingContexts: Array = []; - const blockLevels: Array = []; - const floats: Array = []; - const inlines: Array = []; + export const from = Cache.memoize(function ( + root: Element, + device: Device, + ): PaintingOrder { + function paint( + element: Element, + canvas: Array, + options: { defer?: boolean } = { + defer: false, + }, + ): void { + const { defer = false } = options; + const positionedOrStackingContexts: Array = []; + const blockLevels: Array = []; + const floats: Array = []; + const inlines: Array = []; - /** - * @remarks - * Positioned elements with z-index: auto and floating elements are treated - * as if they create stacking contexts, but their positioned descendants - * and descendants that create stacking contexts should be considered part - * of the parent stacking context, i.e. we need to compute the painting - * order of such subtrees, without recursing into positioned descendants - * and descendants creating stacking contexts, then iterate the result and - * distribute said descendants into layers at this level and add the float - * itself and the other descendants to the floats layer. - */ - function distributeIntoLayers(element: Element) { - if (or(isFlexOrGridChild(device), createsSC(device))(element)) { - positionedOrStackingContexts.push(element); - } else if (isPositioned(device)(element)) { - if (hasAutoZIndex(device)(element)) { - const temporaryLayer: Array = []; - paint(element, temporaryLayer, { defer: true }); - - for (const descendant of temporaryLayer) { - if (or(isPositioned(device), createsSC(device))(descendant)) { - if (or(isPositioned(device), createsSC(device))(descendant)) { - positionedOrStackingContexts.push(descendant); - } else if (isFloat(device)(descendant)) { - floats.push(descendant); - } else if (isBlockLevel(device)(descendant)) { - blockLevels.push(descendant); - } else { - inlines.push(descendant); - } - } else { - positionedOrStackingContexts.push(descendant); - } - } - } else { - positionedOrStackingContexts.push(element); - } - } else if (isFloat(device)(element)) { + /** + * @remarks + * Positioned elements with z-index: auto and floating elements are treated + * as if they create stacking contexts, but their positioned descendants + * and descendants that create stacking contexts should be considered part + * of the parent stacking context, i.e. we need to compute the painting + * order of such subtrees, without recursing into positioned descendants + * and descendants creating stacking contexts, then iterate the result and + * distribute said descendants into layers at this level and add the float + * itself and the other descendants to the floats layer. + */ + function distributeIntoLayers(element: Element) { + if ( + or(isFlexOrGridChild(device), createsStackingContext(device))(element) + ) { + positionedOrStackingContexts.push(element); + } else if (not(isPositioned(device, "static"))(element)) { + if (hasInitialComputedStyle("z-index", device)(element)) { const temporaryLayer: Array = []; paint(element, temporaryLayer, { defer: true }); for (const descendant of temporaryLayer) { - if (or(isPositioned(device), createsSC(device))(descendant)) { - positionedOrStackingContexts.push(descendant); + if ( + or( + not(isPositioned(device, "static")), + createsStackingContext(device), + )(descendant) + ) { + if ( + or( + not(isPositioned(device, "static")), + createsStackingContext(device), + )(descendant) + ) { + positionedOrStackingContexts.push(descendant); + } else if ( + not(hasInitialComputedStyle("float", device))(descendant) + ) { + floats.push(descendant); + } else if (isBlockContainer(Style.from(descendant, device))) { + blockLevels.push(descendant); + } else { + inlines.push(descendant); + } } else { - floats.push(descendant); + positionedOrStackingContexts.push(descendant); } } - } else if (isBlockLevel(device)(element)) { - blockLevels.push(element); } else { - // everything else, this is somewhat crude and might not be accurate, but - // will do for now. - inlines.push(element); + positionedOrStackingContexts.push(element); } - } + } else if (not(hasInitialComputedStyle("float", device))(element)) { + const temporaryLayer: Array = []; + paint(element, temporaryLayer, { defer: true }); - // Block-level elements, forming a stacking context, are painted before - // their descendants. Inline-level elements, forming a stacking context, - // are painted in the inline layer before its inline descendants - // (and before stacking-context-creating and positioned descendants with - // stack level greater than or equal to 0), but after positioned descendants - // with negative z-index, block-level descendants and floating descendants. - if (isBlockLevel(device)(element)) { - canvas.push(element); + for (const descendant of temporaryLayer) { + if ( + or( + not(isPositioned(device, "static")), + createsStackingContext(device), + )(descendant) + ) { + positionedOrStackingContexts.push(descendant); + } else { + floats.push(descendant); + } + } + } else if (isBlockContainer(Style.from(element, device))) { + blockLevels.push(element); } else { + // everything else, this is somewhat crude and might not be accurate, but + // will do for now. inlines.push(element); } + } - function traverse(element: Element) { - for (const child of element - .children(Node.fullTree) - .filter(and(Element.isElement, rendered(device)))) { - distributeIntoLayers(child); + // Block-level elements, forming a stacking context, are painted before + // their descendants. Inline-level elements, forming a stacking context, + // are painted in the inline layer before its inline descendants + // (and before stacking-context-creating and positioned descendants with + // stack level greater than or equal to 0), but after positioned descendants + // with negative z-index, block-level descendants and floating descendants. + if (isBlockContainer(Style.from(element, device))) { + canvas.push(element); + } else { + inlines.push(element); + } - if ( - or( - isPositioned(device), - isFloat(device), - createsSC(device), - )(child) - ) { - // The child is going to be painted in full or partial isolation, so - // we need to stop descending. - continue; - } + function traverse(element: Element) { + for (const child of element + .children(Node.fullTree) + .filter(and(Element.isElement, isRendered(device)))) { + distributeIntoLayers(child); - traverse(child); + if ( + or( + not(isPositioned(device, "static")), + not(hasInitialComputedStyle("float", device)), + createsStackingContext(device), + )(child) + ) { + // The child is going to be painted in full or partial isolation, so + // we need to stop descending. + continue; } + + traverse(child); } - traverse(element); + } + traverse(element); - positionedOrStackingContexts.sort((a: Element, b: Element) => - Comparable.compare(getZLevel(device, a), getZLevel(device, b)), - ); + positionedOrStackingContexts.sort((a: Element, b: Element) => + Comparable.compare(getZLevel(device, a), getZLevel(device, b)), + ); - // If the defer is true, painting of descendant stacking contexts should - // be deferred i.e. the element should just be added to the canvas, (which - // should be a temporary canvas). - let posDescIndex = 0; - for ( - ; - posDescIndex < positionedOrStackingContexts.length && - getZLevel(device, positionedOrStackingContexts[posDescIndex]) < 0; - ++posDescIndex - ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element) { - paint(posOrSC, canvas); - } else { - canvas.push(posOrSC); - } + // If the defer is true, painting of descendant stacking contexts should + // be deferred i.e. the element should just be added to the canvas, (which + // should be a temporary canvas). + let posDescIndex = 0; + for ( + ; + posDescIndex < positionedOrStackingContexts.length && + getZLevel(device, positionedOrStackingContexts[posDescIndex]) < 0; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if (!defer && posOrSC !== element) { + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); } + } - for (const blockLevel of blockLevels) { - if (!defer && createsSC(device)(blockLevel)) { - paint(blockLevel, canvas); - } else { - canvas.push(blockLevel); - } + for (const blockLevel of blockLevels) { + if (!defer && createsStackingContext(device)(blockLevel)) { + paint(blockLevel, canvas); + } else { + canvas.push(blockLevel); } + } - for (const float of floats) { - if (!defer && float !== element && createsSC(device)(float)) { - paint(float, canvas); - } else { - canvas.push(float); - } + for (const float of floats) { + if ( + !defer && + float !== element && + createsStackingContext(device)(float) + ) { + paint(float, canvas); + } else { + canvas.push(float); } + } - for (const inline of inlines) { - if (!defer && inline !== element && createsSC(device)(inline)) { - paint(inline, canvas); - } else { - canvas.push(inline); - } + for (const inline of inlines) { + if ( + !defer && + inline !== element && + createsStackingContext(device)(inline) + ) { + paint(inline, canvas); + } else { + canvas.push(inline); } + } - for ( - ; - posDescIndex < positionedOrStackingContexts.length; - ++posDescIndex + for ( + ; + posDescIndex < positionedOrStackingContexts.length; + ++posDescIndex + ) { + const posOrSC = positionedOrStackingContexts[posDescIndex]; + if ( + !defer && + posOrSC !== element && + createsStackingContext(device)(posOrSC) ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element && createsSC(device)(posOrSC)) { - paint(posOrSC, canvas); - } else { - canvas.push(posOrSC); - } + paint(posOrSC, canvas); + } else { + canvas.push(posOrSC); } } + } - const canvas: Array = []; - paint(root, canvas); + const canvas: Array = []; + paint(root, canvas); - return PaintingOrder.of(canvas); - }, - ); - const isPositioned = (device: Device) => - hasComputedStyle( - "position", - (position) => position.value !== "static", - device, - ); - const hasAutoZIndex = (device: Device) => - hasComputedStyle("z-index", ({ value }) => value === "auto", device); - const isBlockLevel = (device: Device) => - hasComputedStyle( - "display", - ({ values: [outside, inside, listItem] }) => - outside.value === "block" || - inside?.value === "table" || - inside?.value === "flex" || - inside?.value === "grid" || - listItem?.value === "list-item", - device, - ); - const isFloat = (device: Device) => - hasComputedStyle("float", ({ value }) => value !== "none", device); - const createsSC = (device: Device) => createsStackingContext(device); - const rendered = (device: Device) => isRendered(device); + return PaintingOrder.of(canvas); + }); - const getZLevel = (device: Device, element: Element) => { + function getZLevel(device: Device, element: Element) { // If the element is not positioned and not a flex child, setting a z-index // wont affect the z-level. if ( - and(not(isPositioned(device)), not(isFlexOrGridChild(device)))(element) + and( + isPositioned(device, "static"), + not(isFlexOrGridChild(device)), + )(element) ) { return 0; } @@ -289,5 +307,5 @@ export namespace PaintingOrder { } = Style.from(element, device).computed("z-index"); return value === "auto" ? 0 : value; - }; + } } diff --git a/packages/alfa-style/src/element/predicate/is-positioned.ts b/packages/alfa-style/src/element/predicate/is-positioned.ts index a8d695ed31..0db18c0828 100644 --- a/packages/alfa-style/src/element/predicate/is-positioned.ts +++ b/packages/alfa-style/src/element/predicate/is-positioned.ts @@ -2,6 +2,7 @@ import type { Device } from "@siteimprove/alfa-device"; import type { Element } from "@siteimprove/alfa-dom"; import { Predicate } from "@siteimprove/alfa-predicate"; +import type { PositionKeyword } from "../../property/position.js"; import { hasComputedStyle } from "./has-computed-style.js"; const { equals } = Predicate; @@ -11,7 +12,7 @@ const { equals } = Predicate; */ export function isPositioned( device: Device, - ...positions: Array + ...positions: Array ): Predicate { return hasComputedStyle( "position", diff --git a/packages/alfa-style/src/property/position.ts b/packages/alfa-style/src/property/position.ts index 44934fb7f9..f931074ab5 100644 --- a/packages/alfa-style/src/property/position.ts +++ b/packages/alfa-style/src/property/position.ts @@ -1,14 +1,20 @@ import { Longhand } from "../longhand.js"; -/** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/position} - * @internal - */ -export default Longhand.fromKeywords( - { inherits: false }, +const positionKeywords = [ "static", "relative", "absolute", "sticky", "fixed", -); +] as const; + +/** + * @internal + */ +export type PositionKeyword = (typeof positionKeywords)[number]; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/position} + * @internal + */ +export default Longhand.fromKeywords({ inherits: false }, ...positionKeywords); From 57bcb801d6a45416b6a9a9e0e46f238c0df3a7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:05:45 +0100 Subject: [PATCH 22/36] Update references --- packages/alfa-painting-order/package.json | 5 ++++- packages/alfa-painting-order/src/tsconfig.json | 5 ++++- yarn.lock | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/alfa-painting-order/package.json b/packages/alfa-painting-order/package.json index ddf3e063ec..4c6cc89170 100644 --- a/packages/alfa-painting-order/package.json +++ b/packages/alfa-painting-order/package.json @@ -28,9 +28,12 @@ "@siteimprove/alfa-css": "workspace:^", "@siteimprove/alfa-device": "workspace:^", "@siteimprove/alfa-dom": "workspace:^", + "@siteimprove/alfa-equatable": "workspace:^", + "@siteimprove/alfa-hash": "workspace:^", + "@siteimprove/alfa-iterable": "workspace:^", + "@siteimprove/alfa-json": "workspace:^", "@siteimprove/alfa-predicate": "workspace:^", "@siteimprove/alfa-refinement": "workspace:^", - "@siteimprove/alfa-sequence": "workspace:^", "@siteimprove/alfa-style": "workspace:^" }, "devDependencies": { diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index 5c19811fa8..cb37d3712f 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -14,9 +14,12 @@ { "path": "../../alfa-css" }, { "path": "../../alfa-device" }, { "path": "../../alfa-dom" }, + { "path": "../../alfa-equatable" }, + { "path": "../../alfa-hash" }, + { "path": "../../alfa-iterable" }, + { "path": "../../alfa-json" }, { "path": "../../alfa-predicate" }, { "path": "../../alfa-refinement" }, - { "path": "../../alfa-sequence" }, { "path": "../../alfa-style" } ] } diff --git a/yarn.lock b/yarn.lock index cb360f3646..2ae4e82492 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1716,9 +1716,12 @@ __metadata: "@siteimprove/alfa-css": "workspace:^" "@siteimprove/alfa-device": "workspace:^" "@siteimprove/alfa-dom": "workspace:^" + "@siteimprove/alfa-equatable": "workspace:^" + "@siteimprove/alfa-hash": "workspace:^" + "@siteimprove/alfa-iterable": "workspace:^" + "@siteimprove/alfa-json": "workspace:^" "@siteimprove/alfa-predicate": "workspace:^" "@siteimprove/alfa-refinement": "workspace:^" - "@siteimprove/alfa-sequence": "workspace:^" "@siteimprove/alfa-style": "workspace:^" "@siteimprove/alfa-test": "workspace:^" languageName: unknown From 7769f2773c458f1c2472a6725c3c7a668654df0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:21:05 +0100 Subject: [PATCH 23/36] Tag `PaintingOrder` as public --- packages/alfa-painting-order/src/painting-order.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 72aa4ebfb5..7057ccd34c 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -23,6 +23,9 @@ const { import { createsStackingContext } from "./predicate/creates-stacking-context.js"; +/** + * @public + */ export class PaintingOrder implements Equatable, Hashable, Serializable { @@ -57,6 +60,9 @@ export class PaintingOrder } } +/** + * @public + */ export namespace PaintingOrder { export type JSON = { [key: string]: json.JSON; From 36171a336c11f2de5497ee5cb93d7f730b81586c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:10:12 +0000 Subject: [PATCH 24/36] Extract API --- docs/review/api/alfa-painting-order.api.md | 37 ++++++++++++++++++++-- docs/review/api/alfa-style.api.md | 2 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/review/api/alfa-painting-order.api.md b/docs/review/api/alfa-painting-order.api.md index 1ba57aff36..1f76a6db50 100644 --- a/docs/review/api/alfa-painting-order.api.md +++ b/docs/review/api/alfa-painting-order.api.md @@ -4,12 +4,43 @@ ```ts +import { Array as Array_2 } from '@siteimprove/alfa-array'; import type { Device } from '@siteimprove/alfa-device'; import { Element } from '@siteimprove/alfa-dom'; -import { Sequence } from '@siteimprove/alfa-sequence'; +import type { Equatable } from '@siteimprove/alfa-equatable'; +import type { Hash } from '@siteimprove/alfa-hash'; +import type { Hashable } from '@siteimprove/alfa-hash'; +import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; +import * as json from '@siteimprove/alfa-json'; +import type { Serializable } from '@siteimprove/alfa-json'; -// @public -export function computePaintingOrder(root: Element, device: Device): Sequence; +// @public (undocumented) +export class PaintingOrder implements Equatable, Hashable, Serializable { + protected constructor(elements: Array_2); + // (undocumented) + equals(value: this): boolean; + // (undocumented) + equals(value: unknown): value is this; + // (undocumented) + hash(hash: Hash): void; + // (undocumented) + static of(elements: Iterable_2): PaintingOrder; + // (undocumented) + toJSON(options?: Serializable.Options): PaintingOrder.JSON; +} + +// @public (undocumented) +export namespace PaintingOrder { + // (undocumented) + export function isPaintingOrder(value: unknown): value is PaintingOrder; + // (undocumented) + export type JSON = { + [key: string]: json.JSON; + type: "painting-order"; + elements: Array_2; + }; + const from: (this: unknown, root: Element, device: Device) => PaintingOrder; +} // (No @packageDocumentation comment for this package) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 4897ba71fd..d27b651cbf 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -349,7 +349,7 @@ export namespace Shorthands { readonly border: Shorthand<"border-bottom-color" | "border-bottom-style" | "border-bottom-width" | "border-left-color" | "border-left-style" | "border-left-width" | "border-right-color" | "border-right-style" | "border-right-width" | "border-top-color" | "border-top-style" | "border-top-width">; readonly "border-width": Shorthand<"border-bottom-width" | "border-left-width" | "border-right-width" | "border-top-width">; readonly "flex-flow": Shorthand<"flex-direction" | "flex-wrap">; - readonly font: Shorthand<"font-family" | "font-size" | "font-stretch" | "font-style" | "font-variant-caps" | "font-variant-east-asian" | "font-variant-ligatures" | "font-variant-numeric" | "font-variant-position" | "font-weight" | "line-height">; + readonly font: Shorthand<"font-size" | "font-family" | "font-stretch" | "font-style" | "font-variant-caps" | "font-variant-east-asian" | "font-variant-ligatures" | "font-variant-numeric" | "font-variant-position" | "font-weight" | "line-height">; readonly "font-variant": Shorthand<"font-variant-caps" | "font-variant-east-asian" | "font-variant-ligatures" | "font-variant-numeric">; readonly "inset-block": Shorthand<"inset-block-end" | "inset-block-start">; readonly "inset-inline": Shorthand<"inset-inline-end" | "inset-inline-start">; From 01349118f5018a34ff7845ec79494ae176fcce50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:13:25 +0100 Subject: [PATCH 25/36] Refactor function to not update array in-place --- .../alfa-painting-order/src/painting-order.ts | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 7057ccd34c..b61ee26cf8 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -21,6 +21,7 @@ const { isRendered, } = Style; +import { Sequence } from "@siteimprove/alfa-sequence"; import { createsStackingContext } from "./predicate/creates-stacking-context.js"; /** @@ -96,11 +97,11 @@ export namespace PaintingOrder { ): PaintingOrder { function paint( element: Element, - canvas: Array, + canvas: Sequence, options: { defer?: boolean } = { defer: false, }, - ): void { + ): Sequence { const { defer = false } = options; const positionedOrStackingContexts: Array = []; const blockLevels: Array = []; @@ -125,8 +126,9 @@ export namespace PaintingOrder { positionedOrStackingContexts.push(element); } else if (not(isPositioned(device, "static"))(element)) { if (hasInitialComputedStyle("z-index", device)(element)) { - const temporaryLayer: Array = []; - paint(element, temporaryLayer, { defer: true }); + const temporaryLayer = paint(element, Sequence.empty(), { + defer: true, + }); for (const descendant of temporaryLayer) { if ( @@ -159,8 +161,9 @@ export namespace PaintingOrder { positionedOrStackingContexts.push(element); } } else if (not(hasInitialComputedStyle("float", device))(element)) { - const temporaryLayer: Array = []; - paint(element, temporaryLayer, { defer: true }); + const temporaryLayer = paint(element, Sequence.empty(), { + defer: true, + }); for (const descendant of temporaryLayer) { if ( @@ -190,7 +193,7 @@ export namespace PaintingOrder { // stack level greater than or equal to 0), but after positioned descendants // with negative z-index, block-level descendants and floating descendants. if (isBlockContainer(Style.from(element, device))) { - canvas.push(element); + canvas = canvas.append(element); } else { inlines.push(element); } @@ -234,17 +237,17 @@ export namespace PaintingOrder { ) { const posOrSC = positionedOrStackingContexts[posDescIndex]; if (!defer && posOrSC !== element) { - paint(posOrSC, canvas); + canvas = paint(posOrSC, canvas); } else { - canvas.push(posOrSC); + canvas = canvas.append(posOrSC); } } for (const blockLevel of blockLevels) { if (!defer && createsStackingContext(device)(blockLevel)) { - paint(blockLevel, canvas); + canvas = paint(blockLevel, canvas); } else { - canvas.push(blockLevel); + canvas = canvas.append(blockLevel); } } @@ -254,9 +257,9 @@ export namespace PaintingOrder { float !== element && createsStackingContext(device)(float) ) { - paint(float, canvas); + canvas = paint(float, canvas); } else { - canvas.push(float); + canvas = canvas.append(float); } } @@ -266,9 +269,9 @@ export namespace PaintingOrder { inline !== element && createsStackingContext(device)(inline) ) { - paint(inline, canvas); + canvas = paint(inline, canvas); } else { - canvas.push(inline); + canvas = canvas.append(inline); } } @@ -283,17 +286,16 @@ export namespace PaintingOrder { posOrSC !== element && createsStackingContext(device)(posOrSC) ) { - paint(posOrSC, canvas); + canvas = paint(posOrSC, canvas); } else { - canvas.push(posOrSC); + canvas = canvas.append(posOrSC); } } - } - const canvas: Array = []; - paint(root, canvas); + return canvas; + } - return PaintingOrder.of(canvas); + return PaintingOrder.of(paint(root, Sequence.empty())); }); function getZLevel(device: Device, element: Element) { From 7102bb3e4cf43f3a6be6c41f478cbe66c27b3216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:20:18 +0100 Subject: [PATCH 26/36] Remove redundant code --- .../alfa-painting-order/src/painting-order.ts | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index b61ee26cf8..19a4436217 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -126,37 +126,11 @@ export namespace PaintingOrder { positionedOrStackingContexts.push(element); } else if (not(isPositioned(device, "static"))(element)) { if (hasInitialComputedStyle("z-index", device)(element)) { - const temporaryLayer = paint(element, Sequence.empty(), { - defer: true, - }); - - for (const descendant of temporaryLayer) { - if ( - or( - not(isPositioned(device, "static")), - createsStackingContext(device), - )(descendant) - ) { - if ( - or( - not(isPositioned(device, "static")), - createsStackingContext(device), - )(descendant) - ) { - positionedOrStackingContexts.push(descendant); - } else if ( - not(hasInitialComputedStyle("float", device))(descendant) - ) { - floats.push(descendant); - } else if (isBlockContainer(Style.from(descendant, device))) { - blockLevels.push(descendant); - } else { - inlines.push(descendant); - } - } else { - positionedOrStackingContexts.push(descendant); - } - } + positionedOrStackingContexts.push( + ...paint(element, Sequence.empty(), { + defer: true, + }), + ); } else { positionedOrStackingContexts.push(element); } From 2d5832b427cb7bd22780af4c8c301c6f562fe74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:48:43 +0100 Subject: [PATCH 27/36] Add missing reference --- packages/alfa-painting-order/package.json | 1 + packages/alfa-painting-order/src/tsconfig.json | 1 + yarn.lock | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/alfa-painting-order/package.json b/packages/alfa-painting-order/package.json index 4c6cc89170..10e636b2a8 100644 --- a/packages/alfa-painting-order/package.json +++ b/packages/alfa-painting-order/package.json @@ -34,6 +34,7 @@ "@siteimprove/alfa-json": "workspace:^", "@siteimprove/alfa-predicate": "workspace:^", "@siteimprove/alfa-refinement": "workspace:^", + "@siteimprove/alfa-sequence": "workspace:^", "@siteimprove/alfa-style": "workspace:^" }, "devDependencies": { diff --git a/packages/alfa-painting-order/src/tsconfig.json b/packages/alfa-painting-order/src/tsconfig.json index cb37d3712f..d6fe07d80a 100644 --- a/packages/alfa-painting-order/src/tsconfig.json +++ b/packages/alfa-painting-order/src/tsconfig.json @@ -20,6 +20,7 @@ { "path": "../../alfa-json" }, { "path": "../../alfa-predicate" }, { "path": "../../alfa-refinement" }, + { "path": "../../alfa-sequence" }, { "path": "../../alfa-style" } ] } diff --git a/yarn.lock b/yarn.lock index 2ae4e82492..edcf155a96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,6 +1722,7 @@ __metadata: "@siteimprove/alfa-json": "workspace:^" "@siteimprove/alfa-predicate": "workspace:^" "@siteimprove/alfa-refinement": "workspace:^" + "@siteimprove/alfa-sequence": "workspace:^" "@siteimprove/alfa-style": "workspace:^" "@siteimprove/alfa-test": "workspace:^" languageName: unknown From 59d275209e18a847e414ffc8bc339b2d0e66dee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:49:23 +0100 Subject: [PATCH 28/36] Refactor layer painting --- .../alfa-painting-order/src/painting-order.ts | 325 +++++++++--------- 1 file changed, 159 insertions(+), 166 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 19a4436217..8501249e5c 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -95,199 +95,192 @@ export namespace PaintingOrder { root: Element, device: Device, ): PaintingOrder { - function paint( - element: Element, - canvas: Sequence, - options: { defer?: boolean } = { - defer: false, - }, - ): Sequence { - const { defer = false } = options; - const positionedOrStackingContexts: Array = []; - const blockLevels: Array = []; - const floats: Array = []; - const inlines: Array = []; + return PaintingOrder.of(paint(device, root, Sequence.empty())); + }); - /** - * @remarks - * Positioned elements with z-index: auto and floating elements are treated - * as if they create stacking contexts, but their positioned descendants - * and descendants that create stacking contexts should be considered part - * of the parent stacking context, i.e. we need to compute the painting - * order of such subtrees, without recursing into positioned descendants - * and descendants creating stacking contexts, then iterate the result and - * distribute said descendants into layers at this level and add the float - * itself and the other descendants to the floats layer. - */ - function distributeIntoLayers(element: Element) { - if ( - or(isFlexOrGridChild(device), createsStackingContext(device))(element) - ) { - positionedOrStackingContexts.push(element); - } else if (not(isPositioned(device, "static"))(element)) { - if (hasInitialComputedStyle("z-index", device)(element)) { - positionedOrStackingContexts.push( - ...paint(element, Sequence.empty(), { - defer: true, - }), - ); - } else { - positionedOrStackingContexts.push(element); - } - } else if (not(hasInitialComputedStyle("float", device))(element)) { - const temporaryLayer = paint(element, Sequence.empty(), { - defer: true, - }); + const getZLevel = Cache.memoize(function (device: Device, element: Element) { + // If the element is not positioned and not a flex child, setting a z-index + // wont affect the z-level. + if ( + and( + isPositioned(device, "static"), + not(isFlexOrGridChild(device)), + )(element) + ) { + return 0; + } - for (const descendant of temporaryLayer) { - if ( - or( - not(isPositioned(device, "static")), - createsStackingContext(device), - )(descendant) - ) { - positionedOrStackingContexts.push(descendant); - } else { - floats.push(descendant); - } - } - } else if (isBlockContainer(Style.from(element, device))) { - blockLevels.push(element); - } else { - // everything else, this is somewhat crude and might not be accurate, but - // will do for now. - inlines.push(element); - } - } + const { + value: { value }, + } = Style.from(element, device).computed("z-index"); - // Block-level elements, forming a stacking context, are painted before - // their descendants. Inline-level elements, forming a stacking context, - // are painted in the inline layer before its inline descendants - // (and before stacking-context-creating and positioned descendants with - // stack level greater than or equal to 0), but after positioned descendants - // with negative z-index, block-level descendants and floating descendants. - if (isBlockContainer(Style.from(element, device))) { - canvas = canvas.append(element); - } else { - inlines.push(element); - } + return value === "auto" ? 0 : value; + }); + + function sortAndSplitByZLevel( + device: Device, + elements: Iterable, + ): [Array, Array] { + const sorted = Array.from( + Iterable.sortWith(elements, (a: Element, b: Element) => + Comparable.compare(getZLevel(device, a), getZLevel(device, b)), + ), + ); + + const splitIndex = sorted.findIndex( + (element) => getZLevel(device, element) >= 0, + ); + + if (splitIndex < 0) { + return [sorted, []]; + } - function traverse(element: Element) { - for (const child of element - .children(Node.fullTree) - .filter(and(Element.isElement, isRendered(device)))) { - distributeIntoLayers(child); + return [sorted.slice(0, splitIndex), sorted.slice(splitIndex)]; + } + + function paint( + device: Device, + element: Element, + canvas: Sequence, + options: { defer?: boolean } = { + defer: false, + }, + ): Sequence { + const { defer = false } = options; + const positionedOrStackingContexts: Array = []; + const blockLevels: Array = []; + const floats: Array = []; + const inlines: Array = []; + + /** + * @remarks + * Positioned elements with z-index: auto and floating elements are treated + * as if they create stacking contexts, but their positioned descendants + * and descendants that create stacking contexts should be considered part + * of the parent stacking context, i.e. we need to compute the painting + * order of such subtrees, without recursing into positioned descendants + * and descendants creating stacking contexts, then iterate the result and + * distribute said descendants into layers at this level and add the float + * itself and the other descendants to the floats layer. + */ + function distributeIntoLayers(element: Element) { + if ( + or(isFlexOrGridChild(device), createsStackingContext(device))(element) + ) { + positionedOrStackingContexts.push(element); + } else if (not(isPositioned(device, "static"))(element)) { + if (hasInitialComputedStyle("z-index", device)(element)) { + positionedOrStackingContexts.push( + ...paint(device, element, Sequence.empty(), { + defer: true, + }), + ); + } else { + positionedOrStackingContexts.push(element); + } + } else if (not(hasInitialComputedStyle("float", device))(element)) { + const temporaryLayer = paint(device, element, Sequence.empty(), { + defer: true, + }); + for (const descendant of temporaryLayer) { if ( or( not(isPositioned(device, "static")), - not(hasInitialComputedStyle("float", device)), createsStackingContext(device), - )(child) + )(descendant) ) { - // The child is going to be painted in full or partial isolation, so - // we need to stop descending. - continue; + positionedOrStackingContexts.push(descendant); + } else { + floats.push(descendant); } - - traverse(child); } + } else if (isBlockContainer(Style.from(element, device))) { + blockLevels.push(element); + } else { + // everything else, this is somewhat crude and might not be accurate, but + // will do for now. + inlines.push(element); } - traverse(element); - - positionedOrStackingContexts.sort((a: Element, b: Element) => - Comparable.compare(getZLevel(device, a), getZLevel(device, b)), - ); + } - // If the defer is true, painting of descendant stacking contexts should - // be deferred i.e. the element should just be added to the canvas, (which - // should be a temporary canvas). - let posDescIndex = 0; - for ( - ; - posDescIndex < positionedOrStackingContexts.length && - getZLevel(device, positionedOrStackingContexts[posDescIndex]) < 0; - ++posDescIndex - ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if (!defer && posOrSC !== element) { - canvas = paint(posOrSC, canvas); - } else { - canvas = canvas.append(posOrSC); - } - } + // Block-level elements, forming a stacking context, are painted before + // their descendants. Inline-level elements, forming a stacking context, + // are painted in the inline layer before its inline descendants + // (and before stacking-context-creating and positioned descendants with + // stack level greater than or equal to 0), but after positioned descendants + // with negative z-index, block-level descendants and floating descendants. + if (isBlockContainer(Style.from(element, device))) { + canvas = canvas.append(element); + } else { + inlines.push(element); + } - for (const blockLevel of blockLevels) { - if (!defer && createsStackingContext(device)(blockLevel)) { - canvas = paint(blockLevel, canvas); - } else { - canvas = canvas.append(blockLevel); - } - } + function traverse(element: Element) { + for (const child of element + .children(Node.fullTree) + .filter(and(Element.isElement, isRendered(device)))) { + distributeIntoLayers(child); - for (const float of floats) { if ( - !defer && - float !== element && - createsStackingContext(device)(float) + or( + not(isPositioned(device, "static")), + not(hasInitialComputedStyle("float", device)), + createsStackingContext(device), + )(child) ) { - canvas = paint(float, canvas); - } else { - canvas = canvas.append(float); + // The child is going to be painted in full or partial isolation, so + // we need to stop descending. + continue; } - } - for (const inline of inlines) { - if ( - !defer && - inline !== element && - createsStackingContext(device)(inline) - ) { - canvas = paint(inline, canvas); - } else { - canvas = canvas.append(inline); - } + traverse(child); } + } + traverse(element); - for ( - ; - posDescIndex < positionedOrStackingContexts.length; - ++posDescIndex - ) { - const posOrSC = positionedOrStackingContexts[posDescIndex]; - if ( - !defer && - posOrSC !== element && - createsStackingContext(device)(posOrSC) - ) { - canvas = paint(posOrSC, canvas); - } else { - canvas = canvas.append(posOrSC); - } - } + const [negatives, nonNegatives] = sortAndSplitByZLevel( + device, + positionedOrStackingContexts, + ); - return canvas; - } + canvas = paintLayer(device, canvas, negatives, element, defer); + canvas = paintLayer(device, canvas, blockLevels, element, defer); + canvas = paintLayer(device, canvas, floats, element, defer); + canvas = paintLayer(device, canvas, inlines, element, defer); + canvas = paintLayer(device, canvas, nonNegatives, element, defer); - return PaintingOrder.of(paint(root, Sequence.empty())); - }); + return canvas; + } - function getZLevel(device: Device, element: Element) { - // If the element is not positioned and not a flex child, setting a z-index - // wont affect the z-level. - if ( - and( - isPositioned(device, "static"), - not(isFlexOrGridChild(device)), - )(element) - ) { - return 0; + /** + * If the defer is true, painting of descendant stacking contexts should + * be deferred i.e. the layer should just be concatenated to the (temporary) canvas. + * + * For some layers, the parent element, i.e. the element that initated the parent paint procedure, + * might not have been painted first and therefore might appear somewhere in the layer. For that reason, + * to avoid going into infinite recursion, we need to check that each element in the layer is not the + * parent and if it is, it should just be added to the canvas without recursing. + */ + function paintLayer( + device: Device, + canvas: Sequence, + layer: Array, + parent: Element, + defer: boolean, + ): Sequence { + if (defer) { + return canvas.concat(layer); } - const { - value: { value }, - } = Style.from(element, device).computed("z-index"); + for (const element of layer) { + if (element !== parent && createsStackingContext(device)(element)) { + canvas = paint(device, element, canvas); + } else { + canvas = canvas.append(element); + } + } - return value === "auto" ? 0 : value; + return canvas; } } From 2b2bdccecd503c0924e16fe6141c7d48a749cd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:57:47 +0100 Subject: [PATCH 29/36] Add test helper --- .../creates-stacking-context.spec.tsx | 90 +++++++------------ 1 file changed, 33 insertions(+), 57 deletions(-) diff --git a/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx b/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx index 320cdb1315..37be551fa6 100644 --- a/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx +++ b/packages/alfa-painting-order/test/predicate/creates-stacking-context.spec.tsx @@ -1,48 +1,48 @@ +import { h, type Element } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; import { Device } from "@siteimprove/alfa-device"; import { createsStackingContext } from "../../dist/predicate/creates-stacking-context.js"; +const device = Device.standard(); + +function createsStackingContextHelper(element: Element) { + h.document([element]); + return createsStackingContext(device)(element); +} + test("non positioned element with z-index does not create a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), false, ); }); test("absolutely positioned element without z-index does not create a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), false, ); }); test("relatively positioned element without z-index does not create a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), false, ); }); test("element with opacity equal to 1 does not create a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), false, ); }); test("absolutely positioned element with z-index creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -51,7 +51,7 @@ test("absolutely positioned element with z-index creates a stacking context", (t test("relatively positioned element with z-index creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -60,18 +60,14 @@ test("relatively positioned element with z-index creates a stacking context", (t test("fixed element creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("sticky element creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); @@ -80,28 +76,26 @@ test("flex child with z-index creates a stacking context", (t) => { const child =
;
{child}
; - t.equal(createsStackingContext(Device.standard())(child), true); + t.equal(createsStackingContextHelper(child), true); }); test("grid child with z-index creates a stacking context", (t) => { const child =
;
{child}
; - t.equal(createsStackingContext(Device.standard())(child), true); + t.equal(createsStackingContextHelper(child), true); }); test("element with opacity less than 1 creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with mix-blend-mode equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -110,7 +104,7 @@ test("element with mix-blend-mode equal to non-initial value creates a stacking test("element with transform equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -119,25 +113,21 @@ test("element with transform equal to non-initial value creates a stacking conte test("element with scale equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with rotate equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with translate equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -146,16 +136,14 @@ test("element with translate equal to non-initial value creates a stacking conte test("element with perspective equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with clip-path equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -164,7 +152,7 @@ test("element with clip-path equal to non-initial value creates a stacking conte test("element with mask equal to non-initial value creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( + createsStackingContextHelper(
, ), true, @@ -173,9 +161,7 @@ test("element with mask equal to non-initial value creates a stacking context", test("element with isolation equal to isolate creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); @@ -194,9 +180,7 @@ test("element with will-change specifying a property that would create a stackin "mask", ]) { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); } @@ -204,36 +188,28 @@ test("element with will-change specifying a property that would create a stackin test("element with contain equal to layout creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with contain equal to paint creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with contain equal to strict creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); test("element with contain equal to content creates a stacking context", (t) => { t.equal( - createsStackingContext(Device.standard())( -
, - ), + createsStackingContextHelper(
), true, ); }); From a3379f51901f2fee30dd580529f7f780c3cb5291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:21:49 +0100 Subject: [PATCH 30/36] Update function description --- packages/alfa-painting-order/src/painting-order.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 8501249e5c..f726b5f067 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -257,9 +257,9 @@ export namespace PaintingOrder { * If the defer is true, painting of descendant stacking contexts should * be deferred i.e. the layer should just be concatenated to the (temporary) canvas. * - * For some layers, the parent element, i.e. the element that initated the parent paint procedure, - * might not have been painted first and therefore might appear somewhere in the layer. For that reason, - * to avoid going into infinite recursion, we need to check that each element in the layer is not the + * For some layers, the element that initiated the paint procedure, + * might not have been painted yet and therefore might appear somewhere in one of the layers. + * For that reason, to avoid going into infinite recursion, we need to check that each element in the layer is not the * parent and if it is, it should just be added to the canvas without recursing. */ function paintLayer( From 54ad974c381504981536df1a0c0bd74a7825b36a 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, 19 Feb 2025 12:51:19 +0100 Subject: [PATCH 31/36] Update packages/alfa-painting-order/src/painting-order.ts Co-authored-by: Jean-Yves Moyen --- packages/alfa-painting-order/src/painting-order.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index f726b5f067..37d0f6dadc 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -40,6 +40,10 @@ export class PaintingOrder this._elements = elements; } +public get elements(): Iterable { + return this._elements; +} + public equals(value: this): boolean; public equals(value: unknown): value is this; public equals(value: unknown): boolean { From ba4c1ef69a7e1cf8a4d592b49e64a58c3a91a676 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, 19 Feb 2025 12:52:24 +0100 Subject: [PATCH 32/36] Update packages/alfa-painting-order/src/painting-order.ts Co-authored-by: Jean-Yves Moyen --- packages/alfa-painting-order/src/painting-order.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 37d0f6dadc..9368b5dcee 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -53,6 +53,7 @@ public get elements(): Iterable { Array.equals(value._elements, this._elements)) ); } + public hash(hash: Hash): void { Array.hash(this._elements, hash); } From 0b62578ed2c8e22493e1f7e069321947bd37d13e 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, 19 Feb 2025 12:52:33 +0100 Subject: [PATCH 33/36] Update packages/alfa-painting-order/src/painting-order.ts Co-authored-by: Jean-Yves Moyen --- packages/alfa-painting-order/src/painting-order.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 9368b5dcee..9b0831f1f5 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -48,7 +48,6 @@ public get elements(): Iterable { public equals(value: unknown): value is this; public equals(value: unknown): boolean { return ( - value === this || (PaintingOrder.isPaintingOrder(value) && Array.equals(value._elements, this._elements)) ); From bce1f292105fcab17e82a6239cad70d44163af2a 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, 19 Feb 2025 12:56:30 +0100 Subject: [PATCH 34/36] Remove unnecessary memoization --- .../alfa-painting-order/src/painting-order.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 9b0831f1f5..145a46b3b5 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -40,19 +40,19 @@ export class PaintingOrder this._elements = elements; } -public get elements(): Iterable { - return this._elements; -} + public get elements(): Iterable { + return this._elements; + } public equals(value: this): boolean; public equals(value: unknown): value is this; public equals(value: unknown): boolean { return ( - (PaintingOrder.isPaintingOrder(value) && - Array.equals(value._elements, this._elements)) + PaintingOrder.isPaintingOrder(value) && + Array.equals(value._elements, this._elements) ); } - + public hash(hash: Hash): void { Array.hash(this._elements, hash); } @@ -102,7 +102,7 @@ export namespace PaintingOrder { return PaintingOrder.of(paint(device, root, Sequence.empty())); }); - const getZLevel = Cache.memoize(function (device: Device, element: Element) { + function getZLevel(device: Device, element: Element) { // If the element is not positioned and not a flex child, setting a z-index // wont affect the z-level. if ( @@ -119,7 +119,7 @@ export namespace PaintingOrder { } = Style.from(element, device).computed("z-index"); return value === "auto" ? 0 : value; - }); + } function sortAndSplitByZLevel( device: Device, From 0d7a7ea77ec12ad9a3650ef6086d932d8ea11193 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, 19 Feb 2025 13:27:28 +0100 Subject: [PATCH 35/36] Simplify default parameter `options` --- .../alfa-painting-order/src/painting-order.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/alfa-painting-order/src/painting-order.ts b/packages/alfa-painting-order/src/painting-order.ts index 145a46b3b5..cca6980557 100644 --- a/packages/alfa-painting-order/src/painting-order.ts +++ b/packages/alfa-painting-order/src/painting-order.ts @@ -146,11 +146,10 @@ export namespace PaintingOrder { device: Device, element: Element, canvas: Sequence, - options: { defer?: boolean } = { + options: { defer: boolean } = { defer: false, }, ): Sequence { - const { defer = false } = options; const positionedOrStackingContexts: Array = []; const blockLevels: Array = []; const floats: Array = []; @@ -248,11 +247,11 @@ export namespace PaintingOrder { positionedOrStackingContexts, ); - canvas = paintLayer(device, canvas, negatives, element, defer); - canvas = paintLayer(device, canvas, blockLevels, element, defer); - canvas = paintLayer(device, canvas, floats, element, defer); - canvas = paintLayer(device, canvas, inlines, element, defer); - canvas = paintLayer(device, canvas, nonNegatives, element, defer); + canvas = paintLayer(device, canvas, negatives, element, options); + canvas = paintLayer(device, canvas, blockLevels, element, options); + canvas = paintLayer(device, canvas, floats, element, options); + canvas = paintLayer(device, canvas, inlines, element, options); + canvas = paintLayer(device, canvas, nonNegatives, element, options); return canvas; } @@ -271,9 +270,9 @@ export namespace PaintingOrder { canvas: Sequence, layer: Array, parent: Element, - defer: boolean, + options: { defer: boolean }, ): Sequence { - if (defer) { + if (options.defer) { return canvas.concat(layer); } From 38bae0941017d6c13c23ce358aab1b78d729e90d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:44:03 +0000 Subject: [PATCH 36/36] Extract API --- docs/review/api/alfa-painting-order.api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/review/api/alfa-painting-order.api.md b/docs/review/api/alfa-painting-order.api.md index 1f76a6db50..27b5c6675c 100644 --- a/docs/review/api/alfa-painting-order.api.md +++ b/docs/review/api/alfa-painting-order.api.md @@ -18,6 +18,8 @@ import type { Serializable } from '@siteimprove/alfa-json'; export class PaintingOrder implements Equatable, Hashable, Serializable { protected constructor(elements: Array_2); // (undocumented) + get elements(): Iterable_2; + // (undocumented) equals(value: this): boolean; // (undocumented) equals(value: unknown): value is this;