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
83 changes: 83 additions & 0 deletions src/__tests__/native/transition-variables.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { StyleSheet } from "react-native";

import { render, screen } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";

// Observe the engine's transition contract before Reanimated consumes it.
jest.mock("../../native/reanimated", () => ({
animatedComponentFamily: (component: unknown) => component,
}));

// A second definition keeps each variable from being inlined, so it resolves at runtime
const decoy = `.decoy { --transition: opacity 9s; --transition-property: opacity; }`;

const renderStyle = (className: string) => {
render(<View testID={testID} className={className} />);
return StyleSheet.flatten(screen.getByTestId(testID).props.style) as
| Record<string, unknown>
| undefined;
};

test.each([
"width 1s ease-in 200ms",
"width 1s, opacity 2s linear",
"width, opacity",
"width 1s cubic-bezier(0.25, 0.5, 0.75, 1)",
"background-image 1s",
"all 300ms ease-out 50ms",
"1s",
])(
"a transition held in a variable expands like the written one: %s",
(value) => {
registerCSS(`
${decoy}
.written { transition: ${value}; }
.held { --transition: ${value}; transition: var(--transition); }
`);

const written = renderStyle("written");

expect(written).toHaveProperty("transitionDuration");
expect(renderStyle("held")).toStrictEqual(written);
},
);

test("a transition names the style key each property is written to", () => {
registerCSS(
`.written { transition-property: width, background-image, border-inline-start-color; }`,
);

expect(renderStyle("written")).toStrictEqual({
transitionProperty: [
"width",
"experimental_backgroundImage",
"borderStartColor",
],
});
});

test("a transition-property held in a variable names the keys the written one names", () => {
registerCSS(`
${decoy}
.written { transition-property: width, background-image, border-inline-start-color; }
.held { --transition-property: width, background-image, border-inline-start-color; transition-property: var(--transition-property); }
`);

expect(renderStyle("held")).toStrictEqual(renderStyle("written"));
});

test.each([
["three times", "width 1s 2s 3s"],
["two easing functions", "width 1s ease-in linear"],
])(
"a transition held in a variable with %s is invalid and writes no transition",
(_case, value) => {
registerCSS(`
${decoy}
.held { --transition: ${value}; transition: var(--transition); }
`);

expect(renderStyle("held")).toStrictEqual({});
},
);
29 changes: 29 additions & 0 deletions src/__tests__/utilities/property-names.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
propertyRename,
toRNProperty,
transitionStyleKey,
} from "react-native-css/utilities";

test("the rename census is not empty", () => {
expect(Object.keys(propertyRename).length).toBeGreaterThan(0);
});

test.each(Object.entries(propertyRename))(
"a transition on %s names the key it is written to",
(property, renamed) => {
expect(transitionStyleKey(property)).toBe(toRNProperty(renamed));
},
);

test.each([
["width", "width"],
["background-color", "backgroundColor"],
["all", "all"],
["none", "none"],
])("a transition on %s names %s", (property, key) => {
expect(transitionStyleKey(property)).toBe(key);
});

test("visibility names no key to animate", () => {
expect(transitionStyleKey("visibility")).toBeUndefined();
});
2 changes: 1 addition & 1 deletion src/compiler/atRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type {
TokenOrValue,
} from "lightningcss";

import { toRNProperty } from "../utilities";
import type { StyleRuleMapping } from "./compiler.types";
import { toRNProperty } from "./selectors";
import { splitByDelimiter } from "./split-by-delimiter";
import type { StylesheetBuilder } from "./stylesheet";

Expand Down
32 changes: 10 additions & 22 deletions src/compiler/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,19 @@ import type {
UnresolvedColor,
} from "lightningcss";

import { isStyleFunction } from "../utilities";
import {
isStyleFunction,
propertyRename,
toRNProperty,
transitionStyleKey,
} from "../utilities";
import { isMappingProperty } from "./atRules";
import type {
StyleDescriptor,
StyleFunction,
StyleRule,
} from "./compiler.types";
import { parseEasingFunction, parseIterationCount } from "./keyframes";
import { toRNProperty } from "./selectors";
import type { StylesheetBuilder } from "./stylesheet";

const CommaSeparator = Symbol("CommaSeparator");
Expand All @@ -61,19 +65,6 @@ type Parser<T extends Declaration["property"] = Declaration["property"]> = (
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => StyleDescriptor | void;

const propertyRename: Record<string, string> = {
"background-image": "experimental_backgroundImage",
// React Native has no border-inline-* props, but ships the equivalent
// RTL-aware border-start-* / border-end-* props
"border-inline-end-color": "border-end-color",
"border-inline-end-width": "border-end-width",
"border-inline-start-color": "border-start-color",
"border-inline-start-width": "border-start-width",
"border-block-start-width": "border-top-width",
"border-block-end-width": "border-bottom-width",
"font-variant-caps": "font-variant",
};

// React Native only supports a uniform borderStyle, so per-side border
// styles have no native equivalent and are dropped. "solid" is dropped
// silently as it matches React Native's default rendering.
Expand All @@ -99,6 +90,8 @@ const unparsedRuntimeParsing = new Set([
"scale",
"text-shadow",
"transform",
"transition",
"transition-property",
"translate",
]);

Expand Down Expand Up @@ -3060,13 +3053,8 @@ export function addTransitionValue(
declaration.property,
declaration.value
.map((v) => v.property)
.filter(
(v) =>
(v in parsers && !["visibility"].includes(v)) ||
v === "all" ||
v === "none",
)
.map((v) => toRNProperty(v)),
.filter((v) => v in parsers || v === "all" || v === "none")
.flatMap((v) => transitionStyleKey(v) ?? []),
);
return;
}
Expand Down
13 changes: 1 addition & 12 deletions src/compiler/selector-builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AttrOperation, Selector, SelectorList } from "lightningcss";

import { Specificity } from "../utilities";
import { Specificity, toRNProperty } from "../utilities";
import type {
AttributeQuery,
AttrSelectorOperator,
Expand Down Expand Up @@ -597,17 +597,6 @@ function isUniversalSelector([first, second]: Selector) {
return first && first.type === "universal" && !second;
}

export function toRNProperty<T extends string>(str: T) {
return str
.replace(/^-rn-/, "")
.replace(/-./g, (x) => x[1]?.toUpperCase() ?? "") as CamelCase<T>;
}

type CamelCase<S extends string> =
S extends `${infer P1}-${infer P2}${infer P3}`
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
: Lowercase<S>;

const operatorMap: Record<AttrOperation["operator"], AttrSelectorOperator> = {
"equal": "=",
"includes": "~=",
Expand Down
Loading