From 983ee0170a3bec6d8491aa152b0a29ca250f835f 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, 2 Dec 2024 14:50:42 +0100 Subject: [PATCH] Implement mask-repeat --- packages/alfa-style/src/property/mask-clip.ts | 1 + .../alfa-style/src/property/mask-composite.ts | 1 + .../alfa-style/src/property/mask-image.ts | 1 + packages/alfa-style/src/property/mask-mode.ts | 1 + .../alfa-style/src/property/mask-position.ts | 1 + .../alfa-style/src/property/mask-repeat.ts | 47 +++++- packages/alfa-style/src/property/mask-size.ts | 1 + .../test/property/mask-repeat.spec.tsx | 156 +++++++++++++++++- 8 files changed, 204 insertions(+), 5 deletions(-) diff --git a/packages/alfa-style/src/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index c5cd61120a..7162ffab61 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -10,6 +10,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index 6ec11918e0..72f6cd67e9 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -19,6 +19,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index 808c9b3265..f60cd1438a 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -22,6 +22,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 22ab9cc2e1..cc47e9e392 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -17,6 +17,7 @@ type Computed = Specified; /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-mode} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 35d5a22d18..0d70a883d0 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -32,6 +32,7 @@ const initialItem: Computed.Item = Position.Side.of( // TODO: Copied from background-position-x.ts. Adjust for mask-position. /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 6c951af880..f9c451b8ff 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,6 +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"; const { either } = Parser; @@ -27,11 +28,51 @@ type Specified = List; type Computed = Specified; /** - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat} + * {@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. + * * @internal */ export default Longhand.of( - List.of([List.of([Keyword.of("repeat")])]), + List.of([List.of([Keyword.of("repeat")], " ")], ", "), List.parseCommaSeparated(repeatStyle), - (value) => value, + (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, + ); + }, ); diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index e7bd93be45..52cb21dbde 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -67,6 +67,7 @@ const initialItem = Tuple.of(Keyword.of("auto"), Keyword.of("auto")); /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-size} + * * @internal */ export default Longhand.of( diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index 38c9621788..d51009db67 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -15,7 +15,7 @@ test("initial value is repeat", (t) => { t.deepEqual(style.computed("mask-repeat").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "list", @@ -53,6 +53,32 @@ test("#computed parses single keywords", (t) => { source: h.declaration("mask-repeat", kw).toJSON(), }); } + + for (const kw of ["repeat", "space", "round", "no-repeat"] as const) { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: kw + } + ], + }, + ], + }, + source: h.declaration("mask-repeat", kw).toJSON(), + }); + } }); test("#computed parses at most two space separated values", (t) => { @@ -87,7 +113,7 @@ test("#computed parses at most two space separated values", (t) => { t.deepEqual(style2.computed("mask-repeat").toJSON(), { value: { type: "list", - separator: " ", + separator: ", ", values: [ { type: "list", @@ -104,3 +130,129 @@ test("#computed parses at most two space separated values", (t) => { source: null, }); }); + +test("#computed parses mutiple layers", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + { + type: "list", + separator: " ", + values: [ + { + + type: "keyword", + value: "space" + } + ] + } + ], + }, + source: h.declaration("mask-repeat", "round repeat, space").toJSON(), + }); +}); + +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-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + ], + }, + source: h.declaration("mask-repeat", "round repeat, space").toJSON(), + }); +}); + +test("#computed repeats values when there are more layers than values", (t) => { + const element =
; + + const style = Style.from(element, device); + + t.deepEqual(style.computed("mask-repeat").toJSON(), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + { + type: "list", + separator: " ", + values: [ + { + + type: "keyword", + value: "space" + } + ] + }, + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "round" + }, + { + type: "keyword", + value: "repeat" + } + ], + }, + ], + }, + source: h.declaration("mask-repeat", "round repeat, space").toJSON(), + }); +});