diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index 7162ffab61..43d0d1f227 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -2,6 +2,7 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; +import { matchLayers } from "./mask.js"; const { either } = Parser; @@ -16,5 +17,5 @@ type Computed = Specified; export default Longhand.of( List.of([Keyword.of("border-box")]), List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), - (value) => value, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index f9c451b8ff..55b6d441d5 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,7 +2,7 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; -import { Value } from "../value.js"; +import { matchLayers } from "./mask.js"; const { either } = Parser; @@ -31,48 +31,16 @@ type Computed = Specified; * {@link https://drafts.fxtf.org/css-masking/#the-mask-repeat} * * @privateRemarks - * The computed value depends on the number of layers. - * A layer is created for each of the comma separated values for `mask-image`. - * - * If there are more values than layers, the excess layers are discarded. - * Otherwise, the values must be repeated until the numbers match. - * - * See {@link https://drafts.fxtf.org/css-masking/#layering}. - * - * @privateRemarks * The spec says that the computed value "Consists of: two keywords, one per dimension", * which could be taken to mean that the one-keyword shorthand values should be expanded to their two-keyword longhands, * e.g. `repeat-x` would be expanded to `repeat no-repeat` in the computed style, - * but that is not the current behavior observed in the Dev Tools of Chrome and Firefox. - * We mimic the behavior of the browser and do no expand the shorthands. + * but that is not the current behavior observed in the DevTools of Chrome and Firefox. + * We mimic the behavior of the browsers and do not expand the shorthands. * * @internal */ export default Longhand.of( List.of([List.of([Keyword.of("repeat")], " ")], ", "), List.parseCommaSeparated(repeatStyle), - (value, style) => { - const numberOfLayers = Math.max( - style.computed("mask-image").value.values.length, - 1, - ); - - const numberOfValues = value.value.values.length; - if (numberOfValues === numberOfLayers) { - return value; - } - - return Value.of( - List.of( - (numberOfLayers < numberOfValues - ? value.value.values - : Array(Math.ceil(numberOfLayers / numberOfValues)) - .fill(value.value.values) - .flat() - ).slice(0, numberOfLayers), - ", ", - ), - value.source, - ); - }, + (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts new file mode 100644 index 0000000000..9c80491cee --- /dev/null +++ b/packages/alfa-style/src/property/mask.ts @@ -0,0 +1,39 @@ +import { List, type Value } from "@siteimprove/alfa-css"; + +import type { Style } from "../style.js"; + +/** + * {@link https://drafts.fxtf.org/css-masking/#layering}. + * + * @remarks + * The computed value depends on the number of layers. + * A layer is created for each of the comma separated values for `mask-image`. + * + * If there are more values than layers, the excess values are discarded. + * Otherwise, the values must be repeated + * until the number of values matches the number of layers. + */ +export function matchLayers( + value: List, + style: Style, +): List { + const numberOfLayers = Math.max( + style.computed("mask-image").value.values.length, + 1, + ); + + const numberOfValues = value.values.length; + if (numberOfValues === numberOfLayers) { + return value; + } + + return List.of( + (numberOfLayers < numberOfValues + ? value.values + : Array(Math.ceil(numberOfLayers / numberOfValues)) + .fill(value.values) + .flat() + ).slice(0, numberOfLayers), + ", ", + ); +} diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 3f316be31f..80e803580f 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -158,6 +158,7 @@ "./property/mask-position.ts", "./property/mask-repeat.ts", "./property/mask-size.ts", + "./property/mask.ts", "./property/margin-bottom.ts", "./property/margin-left.ts", "./property/margin-right.ts", diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx index c7a66a1f0f..31ece611f7 100644 --- a/packages/alfa-style/test/property/mask-clip.spec.tsx +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -57,10 +57,15 @@ test("#computed parses single keywords", (t) => { } }); -test("#computed parses multiple keywords", (t) => { - const element1 =
; - const style1 = Style.from(element1, device); - t.deepEqual(style1.computed("mask-clip").toJSON(), { +test("#computed parses multiple layers", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-clip").toJSON(), { value: { type: "list", separator: ", ", @@ -78,11 +83,46 @@ test("#computed parses multiple keywords", (t) => { source: h.declaration("mask-clip", "padding-box, no-clip").toJSON(), }); - const element2 = ( -
+}); + +test("#computed discards excess values when there are more values than layers", (t) => { + const element = ( +
+ ); + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-clip").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + { + type: "keyword", + value: "fill-box", + }, + ], + }, + source: h + .declaration("mask-clip", "view-box, fill-box, border-box") + .toJSON(), + }); +}); + +test("#computed repeats values when there are more layers than values", (t) => { + const element = ( +
); - const style2 = Style.from(element2, device); - t.deepEqual(style2.computed("mask-clip").toJSON(), { + const style = Style.from(element, device); + t.deepEqual(style.computed("mask-clip").toJSON(), { value: { type: "list", separator: ", ", @@ -97,12 +137,12 @@ test("#computed parses multiple keywords", (t) => { }, { type: "keyword", - value: "border-box", + value: "view-box", }, ], }, source: h - .declaration("mask-clip", "view-box, fill-box, border-box") + .declaration("mask-clip", "view-box, fill-box") .toJSON(), }); }); diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index d51009db67..cf5862c926 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -132,7 +132,10 @@ test("#computed parses at most two space separated values", (t) => { }); test("#computed parses mutiple layers", (t) => { - const element =
; + const element =
; const style = Style.from(element, device); @@ -173,7 +176,10 @@ test("#computed parses mutiple layers", (t) => { }); test("#computed discards excess values when there are more values than layers", (t) => { - const element =
; + const element =
; const style = Style.from(element, device); @@ -203,7 +209,10 @@ test("#computed discards excess values when there are more values than layers", }); test("#computed repeats values when there are more layers than values", (t) => { - const element =
; + const element =
; const style = Style.from(element, device);