From 9ae20debcdebf6e4226f8182f5cda66d6da6b032 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 16 Oct 2026 12:12:11 +0300 Subject: [PATCH] fix(native): let a rule's shadow list replace an earlier rule's A compiled box-shadow is written index by index, and the walk reused the list an earlier rule had already written for the element. A later rule's list therefore merged into it: `[green]` over `[red, blue]` rendered `[green, blue]`, and an outset shadow over an inset one kept `inset: true`. The same CSS resolved at runtime, through a variable, already replaced the list. Each rule's first write to a list now starts a new one. --- src/__tests__/native/box-shadow.test.tsx | 122 +++++++++++++++++++++++ src/native/styles/calculate-props.ts | 5 +- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/__tests__/native/box-shadow.test.tsx b/src/__tests__/native/box-shadow.test.tsx index 85a809a5..8e3fcc36 100644 --- a/src/__tests__/native/box-shadow.test.tsx +++ b/src/__tests__/native/box-shadow.test.tsx @@ -1,6 +1,10 @@ +import { StyleSheet } from "react-native"; + import { render, screen } from "@testing-library/react-native"; +import { compile } from "react-native-css/compiler"; import { View } from "react-native-css/components/View"; import { registerCSS, testID } from "react-native-css/jest"; +import { StyleCollection } from "react-native-css/native"; test("single shadow - basic", () => { registerCSS(` @@ -911,3 +915,121 @@ describe("@property defaults with shadow variables", () => { ]); }); }); + +describe("a rule's shadow list replaces the list an earlier rule declared", () => { + const earlier = [ + { + offsetX: 1, + offsetY: 1, + blurRadius: 0, + spreadDistance: 0, + color: "#123456", + }, + { + offsetX: 2, + offsetY: 2, + blurRadius: 0, + spreadDistance: 0, + color: "#654321", + }, + ]; + const later = [ + { + offsetX: 3, + offsetY: 3, + blurRadius: 0, + spreadDistance: 0, + color: "#13579b", + }, + ]; + + const cases = [ + { + name: "a list over a longer one", + css: `.first { box-shadow: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321; } .second { box-shadow: 3px 3px 0 0 #13579b; }`, + expected: later, + }, + { + name: "a list over an inset shadow", + css: `.first { box-shadow: inset 1px 1px 0 0 #123456; } .second { box-shadow: 3px 3px 0 0 #13579b; }`, + expected: later, + }, + { + name: "a list over one held in a variable", + css: `.first { --shadows: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321; box-shadow: var(--shadows); } .second { box-shadow: 3px 3px 0 0 #13579b; }`, + expected: later, + }, + { + name: "a list held in a variable over a longer one", + css: `.first { box-shadow: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321; } .second { --shadow: 3px 3px 0 0 #13579b; box-shadow: var(--shadow); }`, + expected: later, + }, + { + name: "a list held in a variable over another", + css: `.first { --shadows: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321; box-shadow: var(--shadows); } .second { --shadow: 3px 3px 0 0 #13579b; box-shadow: var(--shadow); }`, + expected: later, + }, + { + name: "none over a list", + css: `.first { box-shadow: 1px 1px 0 0 #123456; } .second { box-shadow: none; }`, + expected: "none", + }, + { + name: "a list over none", + css: `.first { box-shadow: none; } .second { box-shadow: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321; }`, + expected: earlier, + }, + { + name: "an important list over an earlier important one", + css: `.first { box-shadow: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321 !important; } .second { box-shadow: 3px 3px 0 0 #13579b !important; }`, + expected: later, + }, + { + name: "an important list under a later list", + css: `.first { box-shadow: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321 !important; } .second { box-shadow: 3px 3px 0 0 #13579b; }`, + expected: earlier, + }, + { + name: "a more specific list over a longer, later one", + css: `.second.second { box-shadow: 3px 3px 0 0 #13579b; } .first { box-shadow: 1px 1px 0 0 #123456, 2px 2px 0 0 #654321; }`, + expected: later, + }, + ]; + + describe.each([ + { variables: "inlined", options: {}, sheet: "as compiled" }, + { + variables: "resolved at runtime", + options: { inlineVariables: false } as const, + sheet: "as compiled", + }, + { + variables: "inlined", + options: {}, + sheet: "as the JSON a device receives", + }, + { + variables: "resolved at runtime", + options: { inlineVariables: false } as const, + sheet: "as the JSON a device receives", + }, + ])( + "variables $variables, the sheet injected $sheet", + ({ options, sheet }) => { + test.each(cases)("$name", ({ css, expected }) => { + const stylesheet = compile(css, options).stylesheet(); + StyleCollection.inject( + sheet === "as compiled" + ? stylesheet + : (JSON.parse(JSON.stringify(stylesheet)) as typeof stylesheet), + ); + + render(); + + expect( + StyleSheet.flatten(screen.getByTestId(testID).props.style), + ).toStrictEqual({ boxShadow: expected }); + }); + }, + ); +}); diff --git a/src/native/styles/calculate-props.ts b/src/native/styles/calculate-props.ts index 28b9ed36..0c472ee2 100644 --- a/src/native/styles/calculate-props.ts +++ b/src/native/styles/calculate-props.ts @@ -106,6 +106,8 @@ export function applyDeclarations( topLevelTarget = target, ) { const originalTarget = target; + // A list these declarations write replaces the one an earlier rule declared, so each starts a new list + const startedLists = new WeakSet(); for (const declaration of declarations) { target = originalTarget; @@ -141,8 +143,9 @@ export function applyDeclarations( if (prop.startsWith("[") && prop.endsWith("]")) { prop = Number(prop.slice(1, -1)); - if (!Array.isArray(previousTarget[previousProp])) { + if (!startedLists.has(previousTarget[previousProp])) { previousTarget[previousProp] = []; + startedLists.add(previousTarget[previousProp]); target = previousTarget[previousProp]; } }