Skip to content

Commit f63c46e

Browse files
committed
fix(ui): Ensure cssLayerName from appearance is preserved during updateProps
1 parent d7317c1 commit f63c46e

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

packages/ui/src/Components.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ const Components = (props: ComponentsProps) => {
397397
}
398398
}
399399

400+
// Extract cssLayerName from theme if present and move it to appearance level
401+
if (restProps.appearance) {
402+
restProps = { ...restProps, appearance: extractCssLayerNameFromAppearance(restProps.appearance) };
403+
}
404+
400405
setState(s => ({ ...s, ...restProps, options: { ...s.options, ...restProps.options } }));
401406
};
402407

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { extractCssLayerNameFromAppearance } from '../extractCssLayerNameFromAppearance';
4+
5+
describe('extractCssLayerNameFromAppearance', () => {
6+
it('promotes cssLayerName from a single theme to the appearance level', () => {
7+
const theme = {
8+
name: 'shadcn',
9+
cssLayerName: 'components',
10+
variables: {},
11+
elements: {},
12+
__type: 'prebuilt_appearance' as const,
13+
};
14+
15+
const result = extractCssLayerNameFromAppearance({ theme });
16+
17+
expect(result?.cssLayerName).toBe('components');
18+
});
19+
20+
it('promotes cssLayerName from a theme array to the appearance level', () => {
21+
const theme = {
22+
name: 'shadcn',
23+
cssLayerName: 'components',
24+
variables: {},
25+
elements: {},
26+
__type: 'prebuilt_appearance' as const,
27+
};
28+
29+
const result = extractCssLayerNameFromAppearance({ theme: [theme] });
30+
31+
expect(result?.cssLayerName).toBe('components');
32+
});
33+
34+
it('preserves explicit cssLayerName on appearance over theme cssLayerName', () => {
35+
const theme = {
36+
name: 'shadcn',
37+
cssLayerName: 'components',
38+
variables: {},
39+
elements: {},
40+
__type: 'prebuilt_appearance' as const,
41+
};
42+
43+
const result = extractCssLayerNameFromAppearance({ theme, cssLayerName: 'custom' });
44+
45+
expect(result?.cssLayerName).toBe('custom');
46+
});
47+
48+
it('returns appearance unchanged when no theme is present', () => {
49+
const appearance = { cssLayerName: 'custom' };
50+
const result = extractCssLayerNameFromAppearance(appearance);
51+
52+
expect(result).toEqual(appearance);
53+
});
54+
55+
it('returns undefined for undefined input', () => {
56+
expect(extractCssLayerNameFromAppearance(undefined)).toBeUndefined();
57+
});
58+
59+
describe('Components.updateProps state merge', () => {
60+
// This replicates the state merge in Components.tsx updateProps handler:
61+
// setState(s => ({ ...s, ...restProps, options: { ...s.options, ...restProps.options } }))
62+
//
63+
// When ClerkProvider re-renders, it calls __internal_updateProps({ appearance: { theme: shadcn } }).
64+
// This arrives in updateProps as restProps = { appearance: { theme: shadcn } }.
65+
// The bug: cssLayerName buried in theme is not promoted to appearance level,
66+
// so StyleCacheProvider never wraps Clerk CSS in @layer.
67+
68+
function updatePropsStateMerge(currentState: Record<string, any>, restProps: Record<string, any>) {
69+
// This is the exact logic from Components.tsx line 400 (without fix)
70+
return { ...currentState, ...restProps, options: { ...currentState.options, ...restProps.options } };
71+
}
72+
73+
function updatePropsStateMergeFixed(currentState: Record<string, any>, restProps: Record<string, any>) {
74+
if (restProps.appearance) {
75+
restProps = { ...restProps, appearance: extractCssLayerNameFromAppearance(restProps.appearance) };
76+
}
77+
return { ...currentState, ...restProps, options: { ...currentState.options, ...restProps.options } };
78+
}
79+
80+
const theme = {
81+
name: 'shadcn',
82+
cssLayerName: 'components',
83+
variables: {},
84+
elements: {},
85+
__type: 'prebuilt_appearance' as const,
86+
};
87+
88+
const initialState = {
89+
appearance: { theme, cssLayerName: 'components' }, // after mountComponentRenderer extraction
90+
options: {},
91+
};
92+
93+
it('BUG: updateProps overwrites extracted cssLayerName with raw appearance from ClerkProvider', () => {
94+
// ClerkProvider sends raw appearance (cssLayerName only inside theme, not at top level)
95+
const restProps = { appearance: { theme } };
96+
const newState = updatePropsStateMerge(initialState, restProps);
97+
98+
// cssLayerName is lost — it's only inside theme, not at the appearance level
99+
expect(newState.appearance.cssLayerName).toBeUndefined();
100+
});
101+
102+
it('FIX: updateProps extracts cssLayerName from theme before merging into state', () => {
103+
const restProps = { appearance: { theme } };
104+
const newState = updatePropsStateMergeFixed(initialState, restProps);
105+
106+
expect(newState.appearance.cssLayerName).toBe('components');
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)