Skip to content
Draft
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
118 changes: 118 additions & 0 deletions src/__tests__/compiler/compiler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,121 @@ test("simplifies rem", () => {
],
});
});

describe("CSS-wide color keywords", () => {
const stylesheetFor = (value: string) =>
compile(`.child { color: ${value}; }`).stylesheet();

test("compiles to the inherited-color variable instead of being dropped", () => {
// lightningcss emits `color: inherit` as an UnparsedProperty (the keyword is
// not a CssColor), which parseUnparsed used to drop. Per CSS Color,
// `currentcolor` used as the value of `color` is defined as `inherit`, so it
// resolves to the same inherited-color variable. The ABSENCE of a `v` entry
// is the no-self-reference guarantee — publishing this value as its own
// --__rn-css-color would seed a circular var(--__rn-css-color).
expect(stylesheetFor("inherit")).toStrictEqual({
s: [
[
"child",
[
{
s: [1, 1],
d: [[[{}, "var", "__rn-css-color"], "color", 1]],
dv: 1,
},
],
],
],
});
});

test("inherit and currentcolor compile identically (CSS Color spec identity)", () => {
expect(stylesheetFor("inherit")).toStrictEqual(
stylesheetFor("currentcolor"),
);
});

test("currentcolor still resolves to the inherited-color variable (unchanged)", () => {
// The token/ident branch that hunk 1 restructured also carries currentcolor;
// this pins that currentcolor keeps compiling to the same lookup, and — like
// inherit — never self-publishes a `v`.
expect(stylesheetFor("currentcolor")).toStrictEqual({
s: [
[
"child",
[
{
s: [1, 1],
d: [[[{}, "var", "__rn-css-color"], "color", 1]],
dv: 1,
},
],
],
],
});
});

test("a normal color still publishes --__rn-css-color to descendants", () => {
expect(stylesheetFor("red")).toStrictEqual({
s: [
[
"child",
[
{
s: [1, 1],
d: [{ color: "#f00" }],
v: [["__rn-css-color", "#f00"]],
},
],
],
],
});
});

test("inherit on a non-color property is still dropped (no inheritance context)", () => {
expect(
compile(`.child { font-size: inherit; }`).stylesheet(),
).toStrictEqual({});
});

test("color: initial is still dropped (different semantics, out of scope)", () => {
expect(stylesheetFor("initial")).toStrictEqual({});
});

test("color: unset resolves like inherit (unset on an inherited property is inherit)", () => {
// Per CSS Cascade, `unset` computes to `inherit` on inherited properties,
// and `color` is inherited — so it maps to the same inherited-color variable.
expect(stylesheetFor("unset")).toStrictEqual(stylesheetFor("inherit"));
});

test("keyword matching is case-insensitive (INHERIT)", () => {
// CSS-wide keywords are case-insensitive; lightningcss does not fold case.
expect(stylesheetFor("INHERIT")).toStrictEqual(stylesheetFor("inherit"));
});

test("currentColor (camelCase) resolves like currentcolor", () => {
// The spelling React/JS authors reach for; it is valid, case-insensitive CSS.
expect(stylesheetFor("currentColor")).toStrictEqual(
stylesheetFor("currentcolor"),
);
});

test("currentcolor resolves on a non-color property too (border-color)", () => {
expect(
compile(`.child { border-color: currentcolor; }`).stylesheet(),
).toStrictEqual({
s: [
[
"child",
[
{
s: [1, 1],
d: [[[{}, "var", "__rn-css-color"], "borderColor", 1]],
dv: 1,
},
],
],
],
});
});
});
103 changes: 103 additions & 0 deletions src/__tests__/native/colors.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react-native";
import { Text } from "react-native-css/components/Text";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";

Expand Down Expand Up @@ -165,3 +166,105 @@ describe("currentcolor", () => {
});
});
});

describe("inherit", () => {
test("color: inherit resolves to the parent's color", () => {
registerCSS(`
.parent { color: red; }
.child { color: inherit; }
`);

render(
<View testID="parent" className="parent">
<View testID="child" className="child" />
</View>,
);

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

test("text-inherit: a child Text inherits its parent's color", () => {
// The shape that surfaced the bug: a labelled button whose label renders
// React Native's default color (black) on native instead of the button's
// foreground color, while web inherits correctly.
registerCSS(`
.button { color: white; }
.label { color: inherit; }
`);

render(
<View testID="button" className="button">
<Text testID="label" className="label" />
</View>,
);

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

test("inherit chains through an inheriting ancestor without breaking the chain", () => {
// The middle node inherits and must NOT republish a circular
// --__rn-css-color, or the grandchild would fail to resolve the color.
registerCSS(`
.parent { color: red; }
.mid { color: inherit; }
.child { color: inherit; }
`);

render(
<View testID="parent" className="parent">
<View testID="mid" className="mid">
<View testID="child" className="child" />
</View>
</View>,
);

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

test("inherit follows the nearest colored ancestor", () => {
registerCSS(`
.outer { color: red; }
.inner { color: blue; }
.child { color: inherit; }
`);

render(
<View className="outer">
<View className="inner">
<View testID="child" className="child" />
</View>
</View>,
);

expect(screen.getByTestId("child").props.style).toStrictEqual({
color: "#00f",
});
});

test("color: unset inherits the parent's color, same as inherit", () => {
// `unset` computes to `inherit` on inherited properties, and color is one.
registerCSS(`
.parent { color: red; }
.child { color: unset; }
`);

render(
<View testID="parent" className="parent">
<View testID="child" className="child" />
</View>,
);

expect(screen.getByTestId("child").props.style).toStrictEqual({
color: "#f00",
});
});
});
12 changes: 10 additions & 2 deletions src/__tests__/vendor/tailwind/typography.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,17 @@ describe("Typography - Text Color", () => {
});
});
test("text-inherit", async () => {
// Per CSS Color, `inherit` on the `color` property is defined as
// `currentcolor`, so text-inherit resolves to the platform label color —
// identical to text-current above — instead of being dropped with a warning.
expect(await renderCurrentTest()).toStrictEqual({
props: {},
warnings: { values: { color: "inherit" } },
props: {
style: {
color: {
semantic: ["label", "labelColor"],
},
},
},
});
});
});
Expand Down
32 changes: 28 additions & 4 deletions src/compiler/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,10 +950,15 @@ export function parseUnparsedDeclaration(
}

if (property === "color") {
// Publish the resolved color to descendants as --__rn-css-color — the
// variable `currentcolor` and `color: inherit` both resolve against. Skip
// when the value IS that same lookup (e.g. `color: inherit`) so an
// inheriting rule never seeds a circular `--__rn-css-color:
// var(--__rn-css-color)` that would break resolution for its own subtree.
if (
!isStyleFunction(value) ||
value[1] !== "var" ||
value[2] !== "-css-color"
value[2] !== "__rn-css-color"
) {
builder.addDescriptor("--__rn-css-color", value);
}
Expand Down Expand Up @@ -1263,11 +1268,30 @@ export function parseUnparsed(
return;
}

if (value === "inherit" || value === "initial") {
// CSS-wide keywords and `currentcolor` are case-insensitive.
const keyword = value.toLowerCase();

// Per CSS Color, `currentcolor` as the value of `color` is defined as
// `inherit`; and per CSS Cascade, `unset` on an inherited property
// (`color` is inherited) computes to `inherit` too. So `currentcolor`
// (valid on any property) and `inherit` / `unset` on `color` all
// resolve to the inherited-color variable every color rule publishes
// to its subtree (see parseUnparsedDeclaration).
if (
keyword === "currentcolor" ||
((keyword === "inherit" || keyword === "unset") &&
property === "color")
) {
return [{}, "var", "__rn-css-color"] as const;
}

// `inherit` and `initial` on any other property have no per-property
// resolution context here — drop with a warning. `unset` on a
// non-color property (= `initial` there) and `revert` /
// `revert-layer` keep their existing fall-through handling below.
if (keyword === "inherit" || keyword === "initial") {
builder.addWarning("value", value);
return;
} else if (value === "currentcolor") {
return [{}, "var", "__rn-css-color"] as const;
}

if (value === "true") {
Expand Down