diff --git a/.changeset/few-clouds-play.md b/.changeset/few-clouds-play.md new file mode 100644 index 0000000000..d57ab69734 --- /dev/null +++ b/.changeset/few-clouds-play.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-style": minor +--- + +**Added:** CSS shorthand property `mask` and corresponding longhand properties is now supported. diff --git a/packages/alfa-style/src/property/helpers/match-layers.ts b/packages/alfa-style/src/property/helpers/match-layers.ts new file mode 100644 index 0000000000..3152284d09 --- /dev/null +++ b/packages/alfa-style/src/property/helpers/match-layers.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/property/mask-clip.ts b/packages/alfa-style/src/property/mask-clip.ts index a2b08e9c03..e05318d69a 100644 --- a/packages/alfa-style/src/property/mask-clip.ts +++ b/packages/alfa-style/src/property/mask-clip.ts @@ -2,14 +2,17 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; const { either } = Parser; type Specified = List>; type Computed = Specified; -export const initialItem = Keyword.of("border-box"); +export namespace MaskClip { + export const initialItem = Keyword.of("border-box"); +} /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-clip} @@ -17,7 +20,7 @@ export const initialItem = Keyword.of("border-box"); * @internal */ export default Longhand.of( - List.of([initialItem]), + List.of([MaskClip.initialItem]), List.parseCommaSeparated(either(Box.parseCoordBox, Keyword.parse("no-clip"))), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-composite.ts b/packages/alfa-style/src/property/mask-composite.ts index 853b176270..bb7e80a783 100644 --- a/packages/alfa-style/src/property/mask-composite.ts +++ b/packages/alfa-style/src/property/mask-composite.ts @@ -1,7 +1,8 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; export type CompositingOperator = | Keyword<"add"> diff --git a/packages/alfa-style/src/property/mask-image.ts b/packages/alfa-style/src/property/mask-image.ts index fd52551ba7..7111fb511c 100644 --- a/packages/alfa-style/src/property/mask-image.ts +++ b/packages/alfa-style/src/property/mask-image.ts @@ -6,10 +6,12 @@ import { URL, type Parser as CSSParser, } from "@siteimprove/alfa-css"; +import { Selective } from "@siteimprove/alfa-selective"; const { either } = Parser; import { Longhand } from "../longhand.js"; +import { Resolver } from "../resolver.js"; export type MaskReference = Keyword<"none"> | Image | URL; @@ -33,5 +35,14 @@ type Computed = Specified; export default Longhand.of( List.of([MaskReference.initialItem], ", "), List.parseCommaSeparated(MaskReference.parse), - (value) => value, // TODO: How to resolve absolute URL? + (value, style) => + value.map((images) => + images.map((image) => + Selective.of(image) + .if(Image.isImage, (image) => + image.partiallyResolve(Resolver.length(style)), + ) + .get(), + ), + ), ); diff --git a/packages/alfa-style/src/property/mask-mode.ts b/packages/alfa-style/src/property/mask-mode.ts index 684d9d744e..56cd0f4136 100644 --- a/packages/alfa-style/src/property/mask-mode.ts +++ b/packages/alfa-style/src/property/mask-mode.ts @@ -1,7 +1,8 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; export type MaskingMode = | Keyword<"alpha"> diff --git a/packages/alfa-style/src/property/mask-origin.ts b/packages/alfa-style/src/property/mask-origin.ts index 4e13219ad6..de1e3f125e 100644 --- a/packages/alfa-style/src/property/mask-origin.ts +++ b/packages/alfa-style/src/property/mask-origin.ts @@ -1,12 +1,15 @@ import { Box, Keyword, List } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; type Specified = List; type Computed = Specified; -export const initialItem = Keyword.of("border-box"); +export namespace MaskOrigin { + export const initialItem = Keyword.of("border-box"); +} /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-origin} @@ -14,7 +17,7 @@ export const initialItem = Keyword.of("border-box"); * @internal */ export default Longhand.of( - List.of([initialItem], ", "), + List.of([MaskOrigin.initialItem], ", "), List.parseCommaSeparated(Box.parseCoordBox), (value, style) => value.map((value) => matchLayers(value, style)), ); diff --git a/packages/alfa-style/src/property/mask-position.ts b/packages/alfa-style/src/property/mask-position.ts index 9df75f5adb..1dc321769b 100644 --- a/packages/alfa-style/src/property/mask-position.ts +++ b/packages/alfa-style/src/property/mask-position.ts @@ -6,16 +6,19 @@ import { } from "@siteimprove/alfa-css"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; import { Resolver } from "../resolver.js"; +import { matchLayers } from "./helpers/match-layers.js"; + type Specified = List; type Computed = List; -export const initialItem = Position.of( - Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), - Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), -); +export namespace MaskPosition { + export const initialItem = Position.of( + Position.Side.of(Keyword.of("left"), LengthPercentage.of(0)), + Position.Side.of(Keyword.of("top"), LengthPercentage.of(0)), + ); +} /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mask-position} @@ -23,7 +26,7 @@ export const initialItem = Position.of( * @internal */ export default Longhand.of( - List.of([initialItem], ", "), + List.of([MaskPosition.initialItem], ", "), List.parseCommaSeparated(Position.parse(/* legacySyntax */ true)), (value, style) => value.map((positions) => diff --git a/packages/alfa-style/src/property/mask-repeat.ts b/packages/alfa-style/src/property/mask-repeat.ts index 9b3c5def10..55504d02d6 100644 --- a/packages/alfa-style/src/property/mask-repeat.ts +++ b/packages/alfa-style/src/property/mask-repeat.ts @@ -2,7 +2,8 @@ import { Keyword, List, type Parser as CSSParser } from "@siteimprove/alfa-css"; import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/property/mask-size.ts b/packages/alfa-style/src/property/mask-size.ts index 993729084e..d00337da67 100644 --- a/packages/alfa-style/src/property/mask-size.ts +++ b/packages/alfa-style/src/property/mask-size.ts @@ -8,7 +8,8 @@ import { Parser } from "@siteimprove/alfa-parser"; import { Longhand } from "../longhand.js"; import { Resolver } from "../resolver.js"; -import { matchLayers } from "./mask.js"; + +import { matchLayers } from "./helpers/match-layers.js"; const { either } = Parser; diff --git a/packages/alfa-style/src/property/mask.ts b/packages/alfa-style/src/property/mask.ts index a412d0f60b..5007d3b0fa 100644 --- a/packages/alfa-style/src/property/mask.ts +++ b/packages/alfa-style/src/property/mask.ts @@ -3,7 +3,6 @@ import { List, type Parser as CSSParser, Position, - type Value, Box, Keyword, } from "@siteimprove/alfa-css"; @@ -13,16 +12,14 @@ import { Option } from "@siteimprove/alfa-option"; import { Shorthand } from "../shorthand.js"; -import type { Style } from "../style.js"; - import { MaskReference } from "./mask-image.js"; import { BgSize } from "./mask-size.js"; import { RepeatStyle } from "./mask-repeat.js"; import { CompositingOperator } from "./mask-composite.js"; import { MaskingMode } from "./mask-mode.js"; -import { initialItem as posInitialItem } from "./mask-position.js"; -import { initialItem as clipInitialItem } from "./mask-clip.js"; -import { initialItem as originInitialItem } from "./mask-origin.js"; +import { MaskPosition } from "./mask-position.js"; +import { MaskClip } from "./mask-clip.js"; +import { MaskOrigin } from "./mask-origin.js"; const { doubleBar, @@ -35,42 +32,6 @@ const { separatedList, } = Parser; -/** - * {@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), - ", ", - ); -} - const slash = delimited(option(Token.parseWhitespace), Token.parseDelim("/")); const parsePosAndSize: CSSParser<[Position, Option]> = pair( @@ -169,11 +130,11 @@ export default Shorthand.of( for (const layer of layers) { const [image, pos, size, repeat, origin, clip, composite, mode] = layer; images.push(image ?? MaskReference.initialItem); - positions.push(pos ?? posInitialItem); + positions.push(pos ?? MaskPosition.initialItem); sizes.push(size ?? BgSize.initialItem); repeats.push(repeat ?? RepeatStyle.initialItem); - origins.push(origin ?? originInitialItem); - clips.push(clip ?? clipInitialItem); + origins.push(origin ?? MaskOrigin.initialItem); + clips.push(clip ?? MaskClip.initialItem); composites.push(composite ?? CompositingOperator.initialItem); modes.push(mode ?? MaskingMode.initialItem); } diff --git a/packages/alfa-style/src/tsconfig.json b/packages/alfa-style/src/tsconfig.json index 80e803580f..91bba34172 100644 --- a/packages/alfa-style/src/tsconfig.json +++ b/packages/alfa-style/src/tsconfig.json @@ -39,6 +39,7 @@ "./predicate/is-block-container.ts", "./predicate/is-flex-container.ts", "./predicate/is-grid-container.ts", + "./property/helpers/match-layers.ts", "./property/background.ts", "./property/background-attachment.ts", "./property/background-clip.ts", diff --git a/packages/alfa-style/test/common.ts b/packages/alfa-style/test/common.ts index e4c9283347..60a8a43413 100755 --- a/packages/alfa-style/test/common.ts +++ b/packages/alfa-style/test/common.ts @@ -32,6 +32,17 @@ export function specified( return Style.from(element, device, context).specified(name).toJSON(); } +/** + * @internal + */ +export function computed( + element: Element, + name: N, + context: Context = Context.empty(), +): Value.JSON> { + return Style.from(element, device, context).computed(name).toJSON(); +} + /** * @internal */ diff --git a/packages/alfa-style/test/property/mask-clip.spec.tsx b/packages/alfa-style/test/property/mask-clip.spec.tsx index 31ece611f7..54f3aae722 100644 --- a/packages/alfa-style/test/property/mask-clip.spec.tsx +++ b/packages/alfa-style/test/property/mask-clip.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is border-box", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-clip").toJSON(), { + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: " ", @@ -39,9 +33,7 @@ test("#computed parses single keywords", (t) => { ] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-clip").toJSON(), { + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -58,14 +50,16 @@ test("#computed parses single keywords", (t) => { }); test("#computed parses multiple layers", (t) => { - const element =
; - - const style = Style.from(element, device); + const element = ( +
+ ); - t.deepEqual(style.computed("mask-clip").toJSON(), { + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -82,18 +76,19 @@ test("#computed parses multiple layers", (t) => { }, source: h.declaration("mask-clip", "padding-box, no-clip").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-clip").toJSON(), { + + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -116,13 +111,15 @@ 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 style = Style.from(element, device); - t.deepEqual(style.computed("mask-clip").toJSON(), { + + t.deepEqual(computed(element, "mask-clip"), { value: { type: "list", separator: ", ", @@ -141,8 +138,6 @@ test("#computed repeats values when there are more layers than values", (t) => { }, ], }, - source: h - .declaration("mask-clip", "view-box, fill-box") - .toJSON(), + source: h.declaration("mask-clip", "view-box, fill-box").toJSON(), }); }); diff --git a/packages/alfa-style/test/property/mask-composite.spec.tsx b/packages/alfa-style/test/property/mask-composite.spec.tsx index 0a931218cb..7080334298 100644 --- a/packages/alfa-style/test/property/mask-composite.spec.tsx +++ b/packages/alfa-style/test/property/mask-composite.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is add", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -31,9 +25,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["add", "subtract", "intersect", "exclude"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -59,9 +51,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -90,9 +80,7 @@ test("#computed discards excess values when there are more values than layers", > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", @@ -121,9 +109,7 @@ test("#computed repeats values when there are more layers than values", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-composite").toJSON(), { + t.deepEqual(computed(element, "mask-composite"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-image.spec.tsx b/packages/alfa-style/test/property/mask-image.spec.tsx index 0ee3a1656e..a50040eb72 100644 --- a/packages/alfa-style/test/property/mask-image.spec.tsx +++ b/packages/alfa-style/test/property/mask-image.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is none", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-image").toJSON(), { + t.deepEqual(computed(element, "mask-image"), { value: { type: "list", separator: ", ", @@ -30,9 +24,7 @@ test("initial value is none", (t) => { test("#computed parses url value", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-image").toJSON(), { + t.deepEqual(computed(element, "mask-image"), { value: { type: "list", separator: ", ", @@ -54,8 +46,8 @@ test("#computed parses linear-gradient value", (t) => { const element = (
); - const style = Style.from(element, device); - t.deepEqual(style.computed("mask-image").toJSON(), { + + t.deepEqual(computed(element, "mask-image"), { value: { type: "list", separator: ", ", @@ -72,18 +64,48 @@ test("#computed parses linear-gradient value", (t) => { items: [ { color: { - color: "red", - format: "named", type: "color", + format: "rgb", + alpha: { + type: "percentage", + value: 1, + }, + red: { + type: "percentage", + value: 1, + }, + green: { + type: "percentage", + value: 0, + }, + blue: { + type: "percentage", + value: 0, + }, }, position: null, type: "stop", }, { color: { - color: "blue", - format: "named", type: "color", + format: "rgb", + alpha: { + type: "percentage", + value: 1, + }, + red: { + type: "percentage", + value: 0, + }, + green: { + type: "percentage", + value: 0, + }, + blue: { + type: "percentage", + value: 1, + }, }, position: null, type: "stop", @@ -97,3 +119,33 @@ test("#computed parses linear-gradient value", (t) => { source: h.declaration("mask-image", "linear-gradient(red, blue)").toJSON(), }); }); + +test("#computed parses multiple layers", (t) => { + const element = ( +
+ ); + + t.deepEqual(computed(element, "mask-image"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "foo.svg", + }, + }, + { + type: "image", + image: { + type: "url", + url: "bar.svg", + }, + }, + ], + }, + source: h.declaration("mask-image", "url(foo.svg), url(bar.svg)").toJSON(), + }); +}); diff --git a/packages/alfa-style/test/property/mask-mode.spec.tsx b/packages/alfa-style/test/property/mask-mode.spec.tsx index 928ce668fc..59e7d916a8 100644 --- a/packages/alfa-style/test/property/mask-mode.spec.tsx +++ b/packages/alfa-style/test/property/mask-mode.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is match-source", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -31,9 +25,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["alpha", "luminance", "match-source"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -58,8 +50,8 @@ test("#computed parses multiple layers", (t) => { }} > ); - const style = Style.from(element, device); - t.deepEqual(style.computed("mask-mode").toJSON(), { + + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -88,9 +80,7 @@ test("#computed discards excess values when there are more values than layers", > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", @@ -121,9 +111,7 @@ test("#computed repeats values when there are more layers than values", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-mode").toJSON(), { + t.deepEqual(computed(element, "mask-mode"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-origin.spec.tsx b/packages/alfa-style/test/property/mask-origin.spec.tsx index 0ff5bf7437..da10230917 100644 --- a/packages/alfa-style/test/property/mask-origin.spec.tsx +++ b/packages/alfa-style/test/property/mask-origin.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is border-box", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -38,9 +32,7 @@ test("#computed parses single keywords", (t) => { ] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -66,9 +58,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -97,9 +87,7 @@ test("#computed discards excess values when there are more values than layers", > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", @@ -130,9 +118,7 @@ test("#computed repeats values when there are more layers than values", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-origin").toJSON(), { + t.deepEqual(computed(element, "mask-origin"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-position.spec.tsx b/packages/alfa-style/test/property/mask-position.spec.tsx index c24726391e..c35669ebce 100644 --- a/packages/alfa-style/test/property/mask-position.spec.tsx +++ b/packages/alfa-style/test/property/mask-position.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is 0% 0%", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -54,9 +48,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["top", "bottom"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -85,9 +77,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["left", "right"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -115,9 +105,7 @@ test("#computed parses single keywords", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -142,9 +130,7 @@ test("#computed parses single keywords", (t) => { test("#computed parses lengths and percentages", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", @@ -191,9 +177,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-position").toJSON(), { + t.deepEqual(computed(element, "mask-position"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask-repeat.spec.tsx b/packages/alfa-style/test/property/mask-repeat.spec.tsx index cf5862c926..34816be52e 100644 --- a/packages/alfa-style/test/property/mask-repeat.spec.tsx +++ b/packages/alfa-style/test/property/mask-repeat.spec.tsx @@ -1,18 +1,12 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is repeat", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -37,9 +31,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["repeat-x", "repeat-y"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -57,9 +49,7 @@ test("#computed parses single keywords", (t) => { 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(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -70,8 +60,8 @@ test("#computed parses single keywords", (t) => { values: [ { type: "keyword", - value: kw - } + value: kw, + }, ], }, ], @@ -83,8 +73,7 @@ test("#computed parses single keywords", (t) => { test("#computed parses at most two space separated values", (t) => { const element1 =
; - const style1 = Style.from(element1, device); - t.deepEqual(style1.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element1, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -109,8 +98,7 @@ test("#computed parses at most two space separated values", (t) => { }); const element2 =
; - const style2 = Style.from(element2, device); - t.deepEqual(style2.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element2, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -132,14 +120,16 @@ test("#computed parses at most two space separated values", (t) => { }); test("#computed parses mutiple layers", (t) => { - const element =
; - - const style = Style.from(element, device); + const element = ( +
+ ); - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -150,12 +140,12 @@ test("#computed parses mutiple layers", (t) => { values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, { @@ -163,12 +153,11 @@ test("#computed parses mutiple layers", (t) => { separator: " ", values: [ { - type: "keyword", - value: "space" - } - ] - } + value: "space", + }, + ], + }, ], }, source: h.declaration("mask-repeat", "round repeat, space").toJSON(), @@ -176,14 +165,16 @@ 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); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -194,12 +185,12 @@ test("#computed discards excess values when there are more values than layers", values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, ], @@ -209,14 +200,16 @@ 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); - - t.deepEqual(style.computed("mask-repeat").toJSON(), { + t.deepEqual(computed(element, "mask-repeat"), { value: { type: "list", separator: ", ", @@ -227,12 +220,12 @@ test("#computed repeats values when there are more layers than values", (t) => { values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, { @@ -240,11 +233,10 @@ test("#computed repeats values when there are more layers than values", (t) => { separator: " ", values: [ { - type: "keyword", - value: "space" - } - ] + value: "space", + }, + ], }, { type: "list", @@ -252,12 +244,12 @@ test("#computed repeats values when there are more layers than values", (t) => { values: [ { type: "keyword", - value: "round" + value: "round", }, { type: "keyword", - value: "repeat" - } + value: "repeat", + }, ], }, ], diff --git a/packages/alfa-style/test/property/mask-size.spec.tsx b/packages/alfa-style/test/property/mask-size.spec.tsx index b72b9a5316..08d3ead997 100644 --- a/packages/alfa-style/test/property/mask-size.spec.tsx +++ b/packages/alfa-style/test/property/mask-size.spec.tsx @@ -1,11 +1,7 @@ import { test } from "@siteimprove/alfa-test"; import { h } from "@siteimprove/alfa-dom"; -import { Device } from "@siteimprove/alfa-device"; - -import { Style } from "../../dist/index.js"; - -const device = Device.standard(); +import { computed } from "../common.js"; test("initial value is auto", (t) => { const element = ( @@ -16,9 +12,7 @@ test("initial value is auto", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -53,9 +47,7 @@ test("#computed parses single keywords", (t) => { for (const kw of ["cover", "contain"] as const) { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -74,9 +66,7 @@ test("#computed parses single keywords", (t) => { test("#computed parses percentage width", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -100,9 +90,7 @@ test("#computed parses percentage width", (t) => { test("#computed resolves em width", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -127,9 +115,7 @@ test("#computed resolves em width", (t) => { test("#computed parses pixel width", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -154,9 +140,7 @@ test("#computed parses pixel width", (t) => { test("#computed parses width and height", (t) => { const element =
; - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", @@ -192,9 +176,7 @@ test("#computed parses multiple layers", (t) => { > ); - const style = Style.from(element, device); - - t.deepEqual(style.computed("mask-size").toJSON(), { + t.deepEqual(computed(element, "mask-size"), { value: { type: "list", separator: ", ", diff --git a/packages/alfa-style/test/property/mask.spec.tsx b/packages/alfa-style/test/property/mask.spec.tsx new file mode 100644 index 0000000000..73e87fbfb2 --- /dev/null +++ b/packages/alfa-style/test/property/mask.spec.tsx @@ -0,0 +1,343 @@ +import { test } from "@siteimprove/alfa-test"; +import { h } from "@siteimprove/alfa-dom"; + +import { computed } from "../common.js"; + +test("longhands resolve correctly from shorthand", (t) => { + const mask = + "url(foo.svg) 50% 0% / 12px repeat-x view-box padding-box subtract luminance"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-image"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "foo.svg", + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-position"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0.5, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "percentage", + value: 0, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-size"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 12, + }, + ], + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-repeat"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "repeat-x", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-origin"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-clip"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "padding-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-composite"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "subtract", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-mode"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "luminance", + }, + ], + }, + source: decl.toJSON(), + }); +}); + +test("if one `` value and the `no-clip` keyword are present then `` sets `mask-origin` and `no-clip` sets `mask-clip`", (t) => { + const mask = "url(foo.svg) view-box no-clip"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-origin"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-clip"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "no-clip", + }, + ], + }, + source: decl.toJSON(), + }); +}); + +test("if one `` value and no `no-clip` keyword are present then `` sets both `mask-origin` and `mask-clip`", (t) => { + const mask = "url(foo.svg) view-box"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-origin"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-clip"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "keyword", + value: "view-box", + }, + ], + }, + source: decl.toJSON(), + }); +}); + +test("longhands resolves correctly from shorthand with layers", (t) => { + const mask = "url(foo.svg) 50% 0% / 12px, url(bar.svg) 0% 50%"; + const decl = h.declaration("mask", mask); + const element =
; + + t.deepEqual(computed(element, "mask-image"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "image", + image: { + type: "url", + url: "foo.svg", + }, + }, + { + type: "image", + image: { + type: "url", + url: "bar.svg", + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-position"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0.5, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "percentage", + value: 0, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + { + type: "position", + horizontal: { + type: "side", + offset: { + type: "percentage", + value: 0, + }, + side: { + type: "keyword", + value: "left", + }, + }, + vertical: { + type: "side", + offset: { + type: "percentage", + value: 0.5, + }, + side: { + type: "keyword", + value: "top", + }, + }, + }, + ], + }, + source: decl.toJSON(), + }); + + t.deepEqual(computed(element, "mask-size"), { + value: { + type: "list", + separator: ", ", + values: [ + { + type: "list", + separator: " ", + values: [ + { + type: "length", + unit: "px", + value: 12, + }, + ], + }, + { + type: "list", + separator: " ", + values: [ + { + type: "keyword", + value: "auto", + }, + ], + }, + ], + }, + source: decl.toJSON(), + }); +}); diff --git a/packages/alfa-style/test/tsconfig.json b/packages/alfa-style/test/tsconfig.json index a4415680a6..40bb05d75e 100644 --- a/packages/alfa-style/test/tsconfig.json +++ b/packages/alfa-style/test/tsconfig.json @@ -62,6 +62,7 @@ "./property/mask-position.spec.tsx", "./property/mask-repeat.spec.tsx", "./property/mask-size.spec.tsx", + "./property/mask.spec.tsx", "./property/mix-blend-mode.spec.tsx", "./property/opacity.spec.tsx", "./property/outline.spec.tsx",