Skip to content

Commit b26fed0

Browse files
feat: add CSS @Property rule support
1 parent b0b35b1 commit b26fed0

File tree

7 files changed

+390
-7
lines changed

7 files changed

+390
-7
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { compile } from "react-native-css/compiler";
2+
3+
test("@property with length initial value", () => {
4+
const compiled = compile(`
5+
@property --tw-translate-x {
6+
syntax: "<length-percentage>";
7+
inherits: false;
8+
initial-value: 0px;
9+
}
10+
`);
11+
12+
const result = compiled.stylesheet();
13+
expect(result.vr).toBeDefined();
14+
15+
const vrMap = new Map(result.vr);
16+
expect(vrMap.has("tw-translate-x")).toBe(true);
17+
expect(vrMap.get("tw-translate-x")).toStrictEqual([[0]]);
18+
});
19+
20+
test("@property without initial value is skipped", () => {
21+
const compiled = compile(`
22+
@property --tw-ring-color {
23+
syntax: "*";
24+
inherits: false;
25+
}
26+
`);
27+
28+
const result = compiled.stylesheet();
29+
expect(result.vr).toBeUndefined();
30+
});
31+
32+
test("@property with number initial value", () => {
33+
const compiled = compile(`
34+
@property --tw-backdrop-opacity {
35+
syntax: "<number>";
36+
inherits: false;
37+
initial-value: 1;
38+
}
39+
`);
40+
41+
const result = compiled.stylesheet();
42+
expect(result.vr).toBeDefined();
43+
44+
const vrMap = new Map(result.vr);
45+
expect(vrMap.get("tw-backdrop-opacity")).toStrictEqual([[1]]);
46+
});
47+
48+
test("@property with color initial value", () => {
49+
const compiled = compile(`
50+
@property --tw-ring-offset-color {
51+
syntax: "<color>";
52+
inherits: false;
53+
initial-value: #fff;
54+
}
55+
`);
56+
57+
const result = compiled.stylesheet();
58+
expect(result.vr).toBeDefined();
59+
60+
const vrMap = new Map(result.vr);
61+
expect(vrMap.get("tw-ring-offset-color")).toStrictEqual([["#fff"]]);
62+
});
63+
64+
test("@property with token-list initial value (shadow)", () => {
65+
const compiled = compile(`
66+
@property --tw-shadow {
67+
syntax: "*";
68+
inherits: false;
69+
initial-value: 0 0 #0000;
70+
}
71+
`);
72+
73+
const result = compiled.stylesheet();
74+
expect(result.vr).toBeDefined();
75+
76+
const vrMap = new Map(result.vr);
77+
expect(vrMap.get("tw-shadow")).toStrictEqual([[[0, 0, "#0000"]]]);
78+
});
79+
80+
test("@property defaults are root variables, not universal", () => {
81+
const compiled = compile(`
82+
@property --tw-shadow {
83+
syntax: "*";
84+
inherits: false;
85+
initial-value: 0 0 #0000;
86+
}
87+
`);
88+
89+
const result = compiled.stylesheet();
90+
expect(result.vr).toBeDefined();
91+
expect(result.vu).toBeUndefined();
92+
});
93+
94+
test("@supports -moz-orient fallback no longer fires", () => {
95+
const compiled = compile(`
96+
@supports (-moz-orient: inline) {
97+
*, ::before, ::after, ::backdrop {
98+
--tw-shadow: 0 0 #0000;
99+
}
100+
}
101+
`);
102+
103+
const result = compiled.stylesheet();
104+
expect(result.vu).toBeUndefined();
105+
});
106+
107+
test("@property + class override produces valid stylesheet", () => {
108+
const compiled = compile(`
109+
@property --tw-shadow {
110+
syntax: "*";
111+
inherits: false;
112+
initial-value: 0 0 #0000;
113+
}
114+
@property --tw-inset-shadow {
115+
syntax: "*";
116+
inherits: false;
117+
initial-value: 0 0 #0000;
118+
}
119+
@property --tw-ring-shadow {
120+
syntax: "*";
121+
inherits: false;
122+
initial-value: 0 0 #0000;
123+
}
124+
@property --tw-inset-ring-shadow {
125+
syntax: "*";
126+
inherits: false;
127+
initial-value: 0 0 #0000;
128+
}
129+
@property --tw-ring-offset-shadow {
130+
syntax: "*";
131+
inherits: false;
132+
initial-value: 0 0 #0000;
133+
}
134+
135+
.shadow-md {
136+
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
137+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
138+
}
139+
`);
140+
141+
const result = compiled.stylesheet();
142+
expect(result.vr).toBeDefined();
143+
expect(result.s).toBeDefined();
144+
145+
const shadowRule = result.s?.find(([name]) => name === "shadow-md");
146+
expect(shadowRule).toBeDefined();
147+
});
148+
149+
test("@property with percentage initial value", () => {
150+
const compiled = compile(`
151+
@property --tw-shadow-alpha {
152+
syntax: "<percentage>";
153+
inherits: false;
154+
initial-value: 100%;
155+
}
156+
`);
157+
158+
const result = compiled.stylesheet();
159+
expect(result.vr).toBeDefined();
160+
161+
const vrMap = new Map(result.vr);
162+
expect(vrMap.get("tw-shadow-alpha")).toStrictEqual([["100%"]]);
163+
});
164+
165+
test("multiple @property declarations with verified values", () => {
166+
const compiled = compile(`
167+
@property --tw-translate-x {
168+
syntax: "<length-percentage>";
169+
inherits: false;
170+
initial-value: 0px;
171+
}
172+
@property --tw-translate-y {
173+
syntax: "<length-percentage>";
174+
inherits: false;
175+
initial-value: 0px;
176+
}
177+
@property --tw-rotate {
178+
syntax: "<angle>";
179+
inherits: false;
180+
initial-value: 0deg;
181+
}
182+
`);
183+
184+
const result = compiled.stylesheet();
185+
expect(result.vr).toBeDefined();
186+
187+
const vrMap = new Map(result.vr);
188+
expect(vrMap.get("tw-translate-x")).toStrictEqual([[0]]);
189+
expect(vrMap.get("tw-translate-y")).toStrictEqual([[0]]);
190+
expect(vrMap.get("tw-rotate")).toStrictEqual([["0deg"]]);
191+
});
192+
193+
test("@property with repeated single-child unwraps to scalar", () => {
194+
const compiled = compile(`
195+
@property --my-offset {
196+
syntax: "<length>+";
197+
inherits: false;
198+
initial-value: 10px;
199+
}
200+
`);
201+
202+
const result = compiled.stylesheet();
203+
expect(result.vr).toBeDefined();
204+
205+
const vrMap = new Map(result.vr);
206+
// Single-child repeated (<length>+ with one value) should unwrap
207+
// to the same shape as a direct <length-percentage> type
208+
expect(vrMap.get("my-offset")).toStrictEqual([[10]]);
209+
});
210+
211+
test("@property with repeated multi-child preserves array", () => {
212+
const compiled = compile(`
213+
@property --my-offsets {
214+
syntax: "<length>+";
215+
inherits: false;
216+
initial-value: 10px 20px;
217+
}
218+
`);
219+
220+
const result = compiled.stylesheet();
221+
expect(result.vr).toBeDefined();
222+
223+
const vrMap = new Map(result.vr);
224+
// Multi-child repeated (<length>+ with two values) keeps the array form
225+
expect(vrMap.get("my-offsets")).toStrictEqual([[[10, 20]]]);
226+
});

src/__tests__/native/box-shadow.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,67 @@ test("shadow values from CSS variable are resolved", () => {
259259
],
260260
});
261261
});
262+
263+
test("@property defaults enable shadow class override", () => {
264+
registerCSS(`
265+
@property --my-shadow {
266+
syntax: "*";
267+
inherits: false;
268+
initial-value: 0 0 #0000;
269+
}
270+
@property --my-ring {
271+
syntax: "*";
272+
inherits: false;
273+
initial-value: 0 0 #0000;
274+
}
275+
276+
.test {
277+
--my-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
278+
box-shadow: var(--my-ring), var(--my-shadow);
279+
}
280+
`);
281+
282+
render(<View testID={testID} className="test" />);
283+
const component = screen.getByTestId(testID);
284+
285+
expect(component.props.style.boxShadow).toHaveLength(1);
286+
expect(component.props.style.boxShadow[0]).toMatchObject({
287+
offsetX: 0,
288+
offsetY: 4,
289+
blurRadius: 6,
290+
spreadDistance: -1,
291+
});
292+
});
293+
294+
test("@property defaults with currentcolor (object color)", () => {
295+
registerCSS(`
296+
@property --my-shadow {
297+
syntax: "*";
298+
inherits: false;
299+
initial-value: 0 0 #0000;
300+
}
301+
@property --my-ring {
302+
syntax: "*";
303+
inherits: false;
304+
initial-value: 0 0 #0000;
305+
}
306+
307+
.test {
308+
--my-ring: 0 0 0 2px currentcolor;
309+
box-shadow: var(--my-shadow), var(--my-ring);
310+
}
311+
`);
312+
313+
render(<View testID={testID} className="test" />);
314+
const component = screen.getByTestId(testID);
315+
316+
expect(component.props.style.boxShadow).toHaveLength(1);
317+
expect(component.props.style.boxShadow[0]).toMatchObject({
318+
offsetX: 0,
319+
offsetY: 0,
320+
blurRadius: 0,
321+
spreadDistance: 2,
322+
});
323+
// currentcolor resolves to a platform color object, not a string
324+
expect(typeof component.props.style.boxShadow[0].color).toBe("object");
325+
});

0 commit comments

Comments
 (0)