Skip to content

Commit d2771c3

Browse files
committed
fix: use last-wins semantics for duplicate transform types
When the same transform type appears multiple times (e.g., rotate-45 rotate-90), the last value now wins, matching Tailwind CSS behavior. - Different transform types are still combined into one array - Same transform type replaces the previous value - Add tests for last-wins behavior
1 parent 0ceff90 commit d2771c3

File tree

3 files changed

+124
-13
lines changed

3 files changed

+124
-13
lines changed

src/parser/transforms.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,4 +455,26 @@ describe("parseClassName - multiple transforms", () => {
455455
transform: [{ rotate: "37deg" }, { scale: 0.2 }, { translateX: 50 }],
456456
});
457457
});
458+
459+
// "Last wins" behavior for same transform type (Tailwind parity)
460+
it("should use last value for duplicate rotate (Tailwind parity)", () => {
461+
const result = parseClassName("rotate-45 rotate-90");
462+
expect(result).toEqual({
463+
transform: [{ rotate: "90deg" }],
464+
});
465+
});
466+
467+
it("should use last value for duplicate scale (Tailwind parity)", () => {
468+
const result = parseClassName("scale-50 scale-110");
469+
expect(result).toEqual({
470+
transform: [{ scale: 1.1 }],
471+
});
472+
});
473+
474+
it("should preserve different types while replacing duplicates", () => {
475+
const result = parseClassName("rotate-45 scale-110 rotate-90");
476+
expect(result).toEqual({
477+
transform: [{ rotate: "90deg" }, { scale: 1.1 }],
478+
});
479+
});
458480
});

src/utils/mergeStyles.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ describe("mergeStyles", () => {
2424
});
2525
});
2626

27-
describe("transform array merging", () => {
28-
it("should concatenate transform arrays", () => {
27+
describe("transform array merging - different types combined", () => {
28+
it("should combine different transform types", () => {
2929
const target = { transform: [{ rotate: "45deg" }] };
3030
const source = { transform: [{ scale: 1.1 }] };
3131
expect(mergeStyles(target, source)).toEqual({
@@ -59,6 +59,48 @@ describe("mergeStyles", () => {
5959
});
6060
});
6161

62+
describe("transform array merging - same type last wins (Tailwind parity)", () => {
63+
it("should replace same transform type with last value", () => {
64+
const target = { transform: [{ rotate: "45deg" }] };
65+
const source = { transform: [{ rotate: "90deg" }] };
66+
expect(mergeStyles(target, source)).toEqual({
67+
transform: [{ rotate: "90deg" }],
68+
});
69+
});
70+
71+
it("should replace same scale type with last value", () => {
72+
const target = { transform: [{ scale: 0.5 }] };
73+
const source = { transform: [{ scale: 1.1 }] };
74+
expect(mergeStyles(target, source)).toEqual({
75+
transform: [{ scale: 1.1 }],
76+
});
77+
});
78+
79+
it("should preserve order when replacing - rotate stays in position", () => {
80+
const target = { transform: [{ rotate: "45deg" }, { scale: 1.1 }] };
81+
const source = { transform: [{ rotate: "90deg" }] };
82+
expect(mergeStyles(target, source)).toEqual({
83+
transform: [{ rotate: "90deg" }, { scale: 1.1 }],
84+
});
85+
});
86+
87+
it("should handle mixed: replace same types, add new types", () => {
88+
const target = { transform: [{ rotate: "45deg" }, { scale: 0.5 }] };
89+
const source = { transform: [{ scale: 1.1 }, { translateX: 10 }] };
90+
expect(mergeStyles(target, source)).toEqual({
91+
transform: [{ rotate: "45deg" }, { scale: 1.1 }, { translateX: 10 }],
92+
});
93+
});
94+
95+
it("should handle scaleX and scaleY as different types", () => {
96+
const target = { transform: [{ scaleX: 0.5 }] };
97+
const source = { transform: [{ scaleY: 1.5 }] };
98+
expect(mergeStyles(target, source)).toEqual({
99+
transform: [{ scaleX: 0.5 }, { scaleY: 1.5 }],
100+
});
101+
});
102+
});
103+
62104
describe("mixed properties", () => {
63105
it("should handle mix of standard and transform properties", () => {
64106
const target = { margin: 4, transform: [{ rotate: "45deg" }] };

src/utils/mergeStyles.ts

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,56 @@
11
/**
22
* Smart merge utility for StyleObject values
3-
* Handles array properties (like transform) by concatenating instead of overwriting
3+
* Handles transform arrays with "last wins" semantics for same transform types
44
*/
55

6-
import type { StyleObject } from "../types/core";
6+
import type { StyleObject, TransformStyle } from "../types/core";
77

88
/**
9-
* Properties that should be merged as arrays (concatenated) rather than overwritten
9+
* Get the transform type key from a transform object
10+
* e.g., { rotate: '45deg' } -> 'rotate', { scale: 1.1 } -> 'scale'
1011
*/
11-
const ARRAY_MERGE_PROPERTIES = new Set<string>(["transform"]);
12+
function getTransformType(transform: TransformStyle): string {
13+
return Object.keys(transform)[0];
14+
}
1215

1316
/**
14-
* Merge two StyleObject instances, handling array properties specially
17+
* Merge transform arrays with "last wins" semantics for duplicate transform types.
18+
* Different transform types are combined, but if the same type appears twice,
19+
* the later one replaces the earlier one (matching Tailwind CSS behavior).
20+
*
21+
* @example
22+
* // Different types are combined
23+
* mergeTransforms([{ rotate: '45deg' }], [{ scale: 1.1 }])
24+
* // => [{ rotate: '45deg' }, { scale: 1.1 }]
25+
*
26+
* @example
27+
* // Same type: last wins
28+
* mergeTransforms([{ rotate: '45deg' }], [{ rotate: '90deg' }])
29+
* // => [{ rotate: '90deg' }]
30+
*/
31+
function mergeTransforms(target: TransformStyle[], source: TransformStyle[]): TransformStyle[] {
32+
// Build result by processing target first, then source
33+
// For each source transform, replace any existing transform of the same type
34+
const result: TransformStyle[] = [...target];
35+
36+
for (const sourceTransform of source) {
37+
const sourceType = getTransformType(sourceTransform);
38+
const existingIndex = result.findIndex((t) => getTransformType(t) === sourceType);
39+
40+
if (existingIndex !== -1) {
41+
// Replace existing transform of same type (last wins)
42+
result[existingIndex] = sourceTransform;
43+
} else {
44+
// Add new transform type
45+
result.push(sourceTransform);
46+
}
47+
}
48+
49+
return result;
50+
}
51+
52+
/**
53+
* Merge two StyleObject instances, handling transform arrays specially
1554
*
1655
* @param target - The target object to merge into (mutated)
1756
* @param source - The source object to merge from
@@ -23,30 +62,38 @@ const ARRAY_MERGE_PROPERTIES = new Set<string>(["transform"]);
2362
* // => { margin: 4, padding: 8 }
2463
*
2564
* @example
26-
* // Array properties (transform) are concatenated
65+
* // Different transform types are combined
2766
* mergeStyles(
2867
* { transform: [{ rotate: '45deg' }] },
2968
* { transform: [{ scale: 1.1 }] }
3069
* )
3170
* // => { transform: [{ rotate: '45deg' }, { scale: 1.1 }] }
71+
*
72+
* @example
73+
* // Same transform type: last wins (Tailwind parity)
74+
* mergeStyles(
75+
* { transform: [{ rotate: '45deg' }] },
76+
* { transform: [{ rotate: '90deg' }] }
77+
* )
78+
* // => { transform: [{ rotate: '90deg' }] }
3279
*/
3380
export function mergeStyles(target: StyleObject, source: StyleObject): StyleObject {
3481
for (const key in source) {
3582
if (Object.prototype.hasOwnProperty.call(source, key)) {
3683
const sourceValue = source[key];
3784

38-
// Handle array merge properties (like transform)
39-
if (ARRAY_MERGE_PROPERTIES.has(key) && Array.isArray(sourceValue)) {
85+
// Handle transform arrays specially
86+
if (key === "transform" && Array.isArray(sourceValue)) {
4087
const targetValue = target[key];
4188
if (Array.isArray(targetValue)) {
42-
// Concatenate arrays
43-
(target as Record<string, unknown>)[key] = [...targetValue, ...sourceValue];
89+
// Merge transforms with "last wins" for same types
90+
target.transform = mergeTransforms(targetValue, sourceValue);
4491
} else {
4592
// No existing array, just assign
4693
target[key] = sourceValue;
4794
}
4895
} else {
49-
// Standard Object.assign behavior for non-array properties
96+
// Standard Object.assign behavior for non-transform properties
5097
target[key] = sourceValue;
5198
}
5299
}

0 commit comments

Comments
 (0)