Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/__tests__/native/box-shadow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ describe("@property defaults with shadow variables", () => {
]);
});

test("currentcolor resolves to platform color object", () => {
test("currentcolor resolves to the concrete root seed", () => {
registerCSS(`
@property --my-shadow {
syntax: "*";
Expand Down Expand Up @@ -827,8 +827,22 @@ describe("@property defaults with shadow variables", () => {
blurRadius: 0,
spreadDistance: 2,
});
// currentcolor resolves to a platform color object, not a string
expect(typeof component.props.style.boxShadow[0].color).toBe("object");
expect(component.props.style.boxShadow[0].color).toBe("#000000");
});

test("a platformColor() color reaches the shadow as the platform's object", () => {
registerCSS(`
.test {
box-shadow: 0 0 0 2px platformColor(label);
}
`);

render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

expect(component.props.style.boxShadow[0].color).toStrictEqual({
semantic: ["label"],
});
});

test("three vars with two transparent (Tailwind ring pattern)", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/native/filters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ describe("filter: drop-shadow()", () => {
render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

// currentcolor resolves to a PlatformColor object — requires
// "color" type (not "string") in the shorthand handler pattern
// currentcolor resolves through the root seed, which is a concrete
// scheme-aware colour on every platform — the light arm here.
expect(component.props.style.filter).toStrictEqual([
{
dropShadow: {
offsetX: 0,
offsetY: 4,
standardDeviation: 6,
color: { semantic: ["label", "labelColor"] },
color: "#000000",
},
},
]);
Expand Down
125 changes: 125 additions & 0 deletions src/__tests__/native/root-color-seed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { act, render, screen } from "@testing-library/react-native";
import { compile } from "react-native-css/compiler";
import { TextInput } from "react-native-css/components/TextInput";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";
import { colorScheme } from "react-native-css/runtime";

const consumers: [declaration: string, prop: (string | number)[]][] = [
["color: currentcolor", ["style", "color"]],
["background-color: currentcolor", ["style", "backgroundColor"]],
["border-color: currentcolor", ["style", "borderColor"]],
["border-top-color: currentcolor", ["style", "borderTopColor"]],
["border-inline-start-color: currentcolor", ["style", "borderStartColor"]],
["outline-color: currentcolor", ["style", "outlineColor"]],
["text-decoration: underline currentcolor", ["style", "textDecorationColor"]],
["caret-color: currentcolor", ["cursorColor"]],
["box-shadow: 0 0 0 2px currentcolor", ["style", "boxShadow", 0, "color"]],
[
"box-shadow: inset 0 0 0 2px currentcolor",
["style", "boxShadow", 0, "color"],
],
["text-shadow: 1px 1px 2px currentcolor", ["style", "textShadowColor"]],
["fill: currentcolor", ["fill"]],
["stroke: currentcolor", ["stroke"]],
];

test.each(consumers)(
"%s compiles to a reference to the root color",
(declaration) => {
const rule = compile(`.c { ${declaration}; }`).stylesheet().s?.[0]?.[1][0];

expect(rule?.d).toContainEqual(
expect.arrayContaining([[{}, "var", "__rn-css-color"]]),
);
},
);

// CSS Color 4: with no `color` set, `currentcolor` takes `color`'s initial
// value, `CanvasText` — black in a light scheme and white in a dark one.
describe.each([
["light", "#000000"],
["dark", "#FFFFFF"],
] as const)("in a %s scheme", (scheme, seed) => {
test.each(consumers)(
`%s with no color above it paints ${seed}`,
(declaration, prop) => {
colorScheme.set(scheme);
registerCSS(`.c { ${declaration}; }`);
render(<TextInput testID={testID} className="c" />);

expect(screen.getByTestId(testID).props).toHaveProperty(prop, seed);
},
);
});

test("no scheme preference takes the light seed", () => {
colorScheme.set(null);
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#000000",
});
});

test("the seed follows a scheme change on a mounted element", () => {
colorScheme.set("light");
registerCSS(`.c { color: currentcolor; }`);
render(<View testID={testID} className="c" />);
const element = screen.getByTestId(testID);

expect(element.props.style).toStrictEqual({ color: "#000000" });

act(() => {
colorScheme.set("dark");
});

expect(element.props.style).toStrictEqual({ color: "#FFFFFF" });
});

test("an ancestor's color overrides the seed", () => {
colorScheme.set("light");
registerCSS(`
.parent { color: red; }
.child { color: currentcolor; }
`);
render(
<View testID="parent" className="parent">
<View testID={testID} className="child" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#f00",
});
});

test("a :root color overrides the seed", () => {
colorScheme.set("light");
registerCSS(`
:root { color: #123456; }
.c { color: currentcolor; }
`);
render(<View testID={testID} className="c" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#123456",
});
});

test("a scheme-conditioned :root color overrides the seed in that scheme", () => {
colorScheme.set("dark");
registerCSS(`
:root { color: #123456; }
@media (prefers-color-scheme: dark) {
:root { color: #eeeeee; }
}
.c { color: currentcolor; }
`);
render(<View testID={testID} className="c" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#eee",
});
});
5 changes: 2 additions & 3 deletions src/__tests__/native/text-shadow.test.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ describe("text-shadow", () => {
render(<Text testID={testID} className="my-class" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
textShadowColor: {
semantic: ["label", "labelColor"],
},
// The root seed, concrete on every platform — light arm.
textShadowColor: "#000000",
textShadowOffset: {
height: 10,
width: 10,
Expand Down
54 changes: 54 additions & 0 deletions src/__tests__/vendor/tailwind/current-color.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { screen } from "@testing-library/react-native";
import { TextInput } from "react-native-css/components";
import { colorScheme } from "react-native-css/runtime";

import { render } from "./_tailwind";

const utilities: [className: string, prop: (string | number)[]][] = [
["text-current", ["style", "color"]],
["bg-current", ["style", "backgroundColor"]],
["border-2 border-current", ["style", "borderColor"]],
["outline-2 outline-current", ["style", "outlineColor"]],
["underline decoration-current", ["style", "textDecorationColor"]],
["caret-current", ["cursorColor"]],
["fill-current", ["fill"]],
["stroke-current", ["stroke"]],
["ring", ["style", "boxShadow", 0, "color"]],
["inset-ring", ["style", "boxShadow", 0, "color"]],
["placeholder:text-current", ["placeholderTextColor"]],
];

describe.each([
["light", "#000000", "rgba(0, 0, 0, 1)"],
["dark", "#FFFFFF", "rgba(255, 255, 255, 1)"],
] as const)("in a %s scheme", (scheme, seed, mixedSeed) => {
test.each(utilities)(`%s paints ${seed}`, async (className, prop) => {
colorScheme.set(scheme);
await render(<TextInput testID="current" className={className} />);

expect(screen.getByTestId("current").props).toHaveProperty(prop, seed);
});

// Tailwind mixes the shadow color with `color-mix()`, which the runtime writes as `rgba()`.
test(`shadow-current paints ${mixedSeed}`, async () => {
colorScheme.set(scheme);
await render(
<TextInput testID="current" className="shadow-sm shadow-current" />,
);

expect(screen.getByTestId("current").props).toHaveProperty(
["style", "boxShadow", 0, "color"],
mixedSeed,
);
});
});

test("an explicit ring color wins over the currentcolor default", async () => {
colorScheme.set("light");
await render(<TextInput testID="current" className="ring-2 ring-red-500" />);

expect(screen.getByTestId("current").props).toHaveProperty(
["style", "boxShadow", 0, "color"],
"#fb2c36",
);
});
5 changes: 2 additions & 3 deletions src/__tests__/vendor/tailwind/interactivity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ describe("Interactivity - Caret Color", () => {
test("caret-current", async () => {
expect(await renderCurrentTest()).toStrictEqual({
props: {
cursorColor: {
semantic: ["label", "labelColor"],
},
// The root seed, concrete on every platform — light arm.
cursorColor: "#000000",
style: {},
},
});
Expand Down
5 changes: 2 additions & 3 deletions src/__tests__/vendor/tailwind/typography.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,8 @@ describe("Typography - Text Color", () => {
expect(await renderCurrentTest()).toStrictEqual({
props: {
style: {
color: {
semantic: ["label", "labelColor"],
},
// The root seed, concrete on every platform — light arm.
color: "#000000",
},
},
});
Expand Down
20 changes: 10 additions & 10 deletions src/native-internal/root.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Platform, PlatformColor } from "react-native";

import type { StyleDescriptor, VariableValue } from "react-native-css/compiler";

import { testMediaQuery } from "../native/conditions/media-query";
Expand Down Expand Up @@ -33,12 +31,14 @@ export const rootVariables = rootVariableFamily();
export const universalVariables = rootVariableFamily();

rootVariables("__rn-css-rem").set([[14]]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument

/**
* `color`'s initial value, `CanvasText` (CSS Color 4): black, or white in a dark scheme.
* Concrete rather than `PlatformColor`, because Android's border, ring and shadow paths
* and iOS's border, outline and shadow paths never resolve a semantic color
* (facebook/react-native#57836), so a semantic fallback paints nothing.
*/
rootVariables("__rn-css-color").set([
[
Platform.OS === "ios"
? PlatformColor("label", "labelColor")
: PlatformColor("?attr/textColorPrimary", "SystemBaseHighColor"),
],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);
["#FFFFFF", [["=", "prefers-color-scheme", "dark"]]],
["#000000"],
]);
2 changes: 1 addition & 1 deletion src/native/styles/shorthands/box-shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isStyleDescriptorArray } from "react-native-css/utilities";
import type { StyleFunctionResolver } from "../resolve";
import { shorthandHandler } from "./_handler";

// "color" type accepts both strings ("#fff") and objects (PlatformColor from currentcolor).
// "color" type accepts both strings ("#fff") and objects (a platformColor() value).
// Using "string" here would reject platform color objects and silently drop the shadow.
const color = ["color", "color"] as const;
const offsetX = ["offsetX", "number"] as const;
Expand Down