diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx new file mode 100644 index 00000000..0d8f5e4b --- /dev/null +++ b/src/__tests__/native/absolute-units.test.tsx @@ -0,0 +1,161 @@ +import { fireEvent, render, screen } from "@testing-library/react-native"; +import { View } from "react-native-css/components/View"; +import { registerCSS, testID } from "react-native-css/jest"; + +import { dimensions } from "../../native/reactivity"; + +// https://www.w3.org/TR/css-values-4/#absolute-lengths: 1in = 2.54cm = 25.4mm = 101.6Q = 72pt = 6pc = 96px +const absoluteLengths = [ + ["1in", 96], + ["2.54cm", 96], + ["10mm", 37.7953], + ["4Q", 3.7795], + ["3pt", 4], + ["1pc", 16], +] as const; + +function styleFor(className: string): unknown { + render(); + return screen.getByTestId(testID).props.style; +} + +test.each(absoluteLengths)("width: %s is %d pixels", (length, pixels) => { + registerCSS(`.subject { width: ${length}; }`); + + expect(styleFor("subject")).toStrictEqual({ width: pixels }); +}); + +test.each(absoluteLengths)( + "a variable holding %s resolves to %d pixels at runtime", + (length, pixels) => { + registerCSS(`.subject { --width: ${length}; width: var(--width); }`, { + inlineVariables: false, + }); + + expect(styleFor("subject")).toStrictEqual({ width: pixels }); + }, +); + +test.each([ + ["width: 1IN", { width: 96 }], + ["width: 2.54CM", { width: 96 }], + ["margin-left: -1pc", { marginLeft: -16 }], + ["margin-top: -0.75pt", { marginTop: -1 }], + ["transform: translateX(1in)", { transform: [{ translateX: 96 }] }], + [ + "box-shadow: 3pt 6pt 1pc -3pt #000", + { + boxShadow: [ + { + offsetX: 4, + offsetY: 8, + blurRadius: 16, + spreadDistance: -4, + color: "#000", + }, + ], + }, + ], + ["font-size: 12pt", { fontSize: 16 }], +] as const)("%s converts to pixels", (declaration, style) => { + registerCSS(`.subject { ${declaration}; }`); + + expect(styleFor("subject")).toStrictEqual(style); +}); + +test("an absolute length converts when rem is not inlined", () => { + registerCSS(`.subject { width: 1in; }`, { inlineRem: false }); + + expect(styleFor("subject")).toStrictEqual({ width: 96 }); +}); + +test("a media query reads an absolute length at 96 pixels an inch", () => { + dimensions.set({ ...dimensions.get(), width: 750 }); + registerCSS(` + @media (min-width: 7.8in) { .subject { width: 1pc; } } + @media (min-width: 7.9in) { .subject { height: 1pc; } } + `); + + expect(styleFor("subject")).toStrictEqual({ width: 16 }); +}); + +test("a shorthand converts each of its lengths", () => { + registerCSS(`.subject { margin: 1in 1pc; }`); + + expect(styleFor("subject")).toStrictEqual({ + marginTop: 96, + marginRight: 16, + marginBottom: 96, + marginLeft: 16, + }); +}); + +test.each([ + ["width", "width"], + ["border-radius", "borderRadius"], +] as const)( + "an infinite absolute %s clamps as an infinite px one does", + (property, styleProperty) => { + registerCSS(` + .inch { ${property}: calc(infinity * 1in); } + .pixel { ${property}: calc(infinity * 1px); } + `); + + expect(styleFor("inch")).toStrictEqual({ + [styleProperty]: 9007199254740990, + }); + expect(styleFor("pixel")).toStrictEqual({ + [styleProperty]: 9007199254740990, + }); + }, +); + +test("an inlined variable holding an infinite absolute length clamps as px does", () => { + registerCSS(` + .inch { --inch-radius: calc(infinity * 1pt); border-radius: var(--inch-radius); } + .pixel { --pixel-radius: calc(infinity * 1px); border-radius: var(--pixel-radius); } + `); + + expect(styleFor("inch")).toStrictEqual({ borderRadius: 9007199254740991 }); + expect(styleFor("pixel")).toStrictEqual({ borderRadius: 9007199254740991 }); +}); + +test("a runtime calc() adds an absolute length", () => { + registerCSS(`.subject { --width: 10px; width: calc(var(--width) + 1in); }`, { + inlineVariables: false, + }); + + expect(styleFor("subject")).toStrictEqual({ width: 106 }); +}); + +test("an @property initial value converts", () => { + registerCSS(` + @property --width { syntax: ""; inherits: false; initial-value: 1in; } + .subject { width: var(--width); } + `); + + expect(styleFor("subject")).toStrictEqual({ width: 96 }); +}); + +test.each([ + [390, "styles its child", { width: 16 }], + [380, "leaves its child unstyled", undefined], +] as const)( + "a container %d pixels wide against (min-width: 4in) %s", + (containerWidth, _outcome, style) => { + registerCSS(` + .container { container-name: box; container-type: inline-size; } + @container box (min-width: 4in) { .child { width: 1pc; } } + `); + render( + + + , + ); + fireEvent(screen.getByTestId("container"), "layout", { + nativeEvent: { layout: { width: containerWidth, height: 100 } }, + }); + + expect(screen.getByTestId(testID).props.style).toStrictEqual(style); + }, +); diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 9ca2aab8..88019584 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -20,6 +20,7 @@ import type { GradientItemFor_DimensionPercentageFor_LengthValue, Length, LengthPercentageOrAuto, + LengthUnit, LengthValue, LineDirection, LineHeight, @@ -1414,6 +1415,16 @@ export function parseLengthDeclaration( return parseLength(declaration.value, builder); } +/** Pixels in each absolute unit: https://www.w3.org/TR/css-values-4/#absolute-lengths */ +const absoluteUnitPixels = { + cm: 96 / 2.54, + in: 96, + mm: 96 / 25.4, + pc: 16, + pt: 96 / 72, + q: 96 / 101.6, +} as const satisfies Partial>; + export function parseLength( length: | number @@ -1461,6 +1472,10 @@ export function parseLength( case "q": case "pt": case "pc": + return parseLength( + { unit: "px", value: length.value * absoluteUnitPixels[length.unit] }, + builder, + ); case "ex": case "rex": case "ch":