Skip to content

Commit e82ffd8

Browse files
committed
fix: ensure attrs propagate to component overrides
1 parent cc0f724 commit e82ffd8

4 files changed

Lines changed: 127 additions & 17 deletions

File tree

packages/kstyled/src/__tests__/runtime.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
import React from 'react';
12
import { StyleSheet, View, Text } from 'react-native';
23
import { styled } from '../styled';
34
import { css } from '../css';
45
import type { CompiledStyles, StyleMetadata } from '../types';
56
import { normalizeStyleValue } from '../css-runtime-parser';
67
import { mergeDynamicPatches } from '../utils/style-merger';
78

9+
jest.mock('../theme', () => {
10+
const actual = jest.requireActual('../theme');
11+
return {
12+
...actual,
13+
useTheme: () => actual.defaultTheme,
14+
};
15+
});
16+
817
describe('kstyled Runtime Tests', () => {
918
describe('Static Styles', () => {
1019
test('should create component with static styles using __withStyles', () => {
@@ -226,6 +235,48 @@ describe('kstyled Runtime Tests', () => {
226235
editable: false,
227236
});
228237
});
238+
239+
test('should forward attrs values to rendered component', () => {
240+
const Base = React.forwardRef((props: any, _ref) => {
241+
return React.createElement('Base', props);
242+
});
243+
244+
const compiledStyles = StyleSheet.create({
245+
base: { padding: 4 },
246+
}) as unknown as CompiledStyles;
247+
248+
const StyledComp = styled(Base).__withStyles({
249+
compiledStyles,
250+
styleKeys: ['base'],
251+
attrs: {
252+
accessibilityRole: 'button',
253+
testID: 'default-id',
254+
},
255+
});
256+
257+
const element = (StyledComp as any).render({ testID: 'override-id' }, null);
258+
259+
expect(element.props.accessibilityRole).toBe('button');
260+
expect(element.props.testID).toBe('override-id');
261+
expect(element.props.style).toBeDefined();
262+
});
263+
264+
test('should allow attrs to override base component via as', () => {
265+
const Base = React.forwardRef((props: any, _ref) => React.createElement('Base', props));
266+
const Override = React.forwardRef((props: any, _ref) => React.createElement('Override', props));
267+
268+
const StyledComp = styled(Base).__withStyles({
269+
attrs: () => ({
270+
as: Override,
271+
}),
272+
});
273+
274+
const elementWithOverride = (StyledComp as any).render({}, null);
275+
expect(elementWithOverride.type).toBe(Override);
276+
277+
const elementWithProp = (StyledComp as any).render({ as: Base }, null);
278+
expect(elementWithProp.type).toBe(Base);
279+
});
229280
});
230281

231282
describe('Inline css`` with Memoization', () => {

packages/kstyled/src/styled.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
SectionList,
1515
} from 'react-native';
1616
import type { StyledComponent, CompiledStyles, AttrsFunction } from './types';
17-
import type { StyleMetadata, StyledFactory, StyleObject, PropsWithTheme, StyleValue, DynamicPatchFunction } from './types/styled-types';
17+
import type { StyleMetadata, StyledFactory, StyleObject, PropsWithTheme, StyleValue, DynamicPatchFunction, AttrsValue } from './types/styled-types';
1818
import { useTheme } from './theme';
1919
import { parseCSS } from './css-runtime-parser';
2020
import {
@@ -32,6 +32,7 @@ import {
3232
filterProps,
3333
hasTransientProps,
3434
mergeAttrsWithProps,
35+
combineAttrs,
3536
} from './utils/props-filter';
3637

3738
/**
@@ -71,6 +72,8 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
7172

7273
// Extract base component's metadata (handles Animated components)
7374
const baseMetadata = extractBaseMetadata(BaseComponent);
75+
const combinedAttrs = combineAttrs(baseMetadata.attrs, attrs);
76+
const hasAttrs = Boolean(combinedAttrs);
7477

7578
// Merge parent and child styles
7679
const { mergedCompiledStyles, mergedStyleKeys } = mergeMetadata(
@@ -85,11 +88,11 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
8588
);
8689

8790
const StyledComponent = forwardRef<unknown, Record<string, unknown>>((props, ref) => {
88-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89-
const Component = (props.as || BaseComponent) as ComponentType<any>;
90-
9191
// Fast path: static styles only (no dynamic, no attrs, no external style)
92-
if (!getDynamicPatch && !attrs && !props.style) {
92+
if (!getDynamicPatch && !hasAttrs && !props.style) {
93+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
94+
const Component = (props.as || BaseComponent) as ComponentType<any>;
95+
9396
// Check if we have any transient props first
9497
const hasTransient = hasTransientProps(props);
9598

@@ -105,13 +108,13 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
105108
}
106109

107110
// Slow path: dynamic styles, attrs, or external styles
108-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
109-
const { as: _, style: externalStyle, ...restProps } = props;
111+
const { as: asProp, style: externalStyle, ...restProps } = props;
110112
const theme = useTheme();
111113

112114
// Build final props with theme
113115
const propsWithTheme: PropsWithTheme = { ...restProps, theme };
114-
const mergedProps = mergeAttrsWithProps(attrs, propsWithTheme);
116+
const propsWithAttrs = mergeAttrsWithProps(combinedAttrs, propsWithTheme);
117+
const mergedProps = propsWithAttrs;
115118

116119
// Compute and merge dynamic patches
117120
const dynamicPatch = mergeDynamicPatches(
@@ -132,9 +135,11 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
132135
);
133136

134137
// Filter props and add styles
135-
const forwardedProps = filterProps(restProps, ref);
138+
const forwardedProps = filterProps(propsWithAttrs, ref);
136139
forwardedProps.style = styles;
137140

141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
const Component = (asProp || (propsWithAttrs.as as ComponentType<any>) || BaseComponent) as ComponentType<any>;
138143
return <Component {...forwardedProps} />;
139144
});
140145

@@ -158,7 +163,7 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
158163
): StyledComponent<C, P, AttrsP & NewAttrs> {
159164
return createStyledComponent({
160165
...metadata,
161-
attrs: attrsArg as Record<string, unknown>,
166+
attrs: attrsArg as AttrsValue,
162167
});
163168
};
164169

@@ -174,7 +179,7 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
174179
compiledStyles: mergedCompiledStyles,
175180
styleKeys: mergedStyleKeys,
176181
getDynamicPatch: combinedGetDynamicPatch,
177-
attrs,
182+
attrs: combinedAttrs,
178183
},
179184
BaseComponent
180185
);
@@ -208,7 +213,7 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
208213
compiledStyles,
209214
styleKeys: compiledStyles ? ['base'] : undefined,
210215
getDynamicPatch: dynamicGetter as DynamicPatchFunction | undefined,
211-
attrs: baseAttrs as Record<string, unknown>,
216+
attrs: baseAttrs as AttrsValue,
212217
};
213218

214219
return createStyledComponent(metadata);
@@ -220,7 +225,11 @@ function styledFunction<C extends ComponentType<any>, P = {}, AttrsP = {}>(
220225
factory.__withStyles = function (
221226
metadata: StyleMetadata
222227
): StyledComponent<C, P> {
223-
return createStyledComponent({ ...metadata, attrs: baseAttrs as Record<string, unknown> });
228+
const mergedAttrs = combineAttrs(
229+
baseAttrs as AttrsValue | undefined,
230+
metadata.attrs
231+
);
232+
return createStyledComponent({ ...metadata, attrs: mergedAttrs });
224233
};
225234

226235
/**

packages/kstyled/src/utils/component-metadata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ function extractMetadataRecursive(
4747
return {};
4848
}
4949

50-
// Check if component has metadata directly
50+
// Check if component has metadata directly (works for objects and functions)
5151
if (
52-
typeof component === 'object' &&
5352
component !== null &&
53+
(typeof component === 'object' || typeof component === 'function') &&
5454
'__kstyled_metadata__' in component
5555
) {
5656
return (component as ComponentWithMetadata).__kstyled_metadata__ || {};

packages/kstyled/src/utils/props-filter.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,59 @@ export function mergeAttrsWithProps(
5555
return propsWithTheme;
5656
}
5757

58+
const resolvedAttrs = resolveAttrs(attrs, propsWithTheme);
59+
if (!resolvedAttrs) {
60+
return propsWithTheme;
61+
}
62+
63+
return { ...resolvedAttrs, ...propsWithTheme };
64+
}
65+
66+
/**
67+
* Combine base and child attrs preserving execution order
68+
* Later attrs override earlier ones
69+
*/
70+
export function combineAttrs(
71+
baseAttrs: AttrsValue | undefined,
72+
childAttrs: AttrsValue | undefined
73+
): AttrsValue | undefined {
74+
if (!baseAttrs) {
75+
return childAttrs;
76+
}
77+
if (!childAttrs) {
78+
return baseAttrs;
79+
}
80+
81+
// If both are plain objects we can merge once
82+
if (typeof baseAttrs !== 'function' && typeof childAttrs !== 'function') {
83+
return { ...baseAttrs, ...childAttrs };
84+
}
85+
86+
// Otherwise evaluate sequentially at runtime so each attrs function
87+
// can see the props (including previous attrs results)
88+
return (props: PropsWithTheme) => {
89+
const baseResult = resolveAttrs(baseAttrs, props);
90+
const propsWithBase = baseResult ? { ...props, ...baseResult } : props;
91+
const childResult = resolveAttrs(childAttrs, propsWithBase);
92+
93+
return {
94+
...(baseResult || {}),
95+
...(childResult || {}),
96+
};
97+
};
98+
}
99+
100+
function resolveAttrs(
101+
attrs: AttrsValue | undefined,
102+
propsWithTheme: PropsWithTheme
103+
): Record<string, unknown> | undefined {
104+
if (!attrs) {
105+
return undefined;
106+
}
107+
58108
if (typeof attrs === 'function') {
59-
return { ...propsWithTheme, ...attrs(propsWithTheme) };
109+
return attrs(propsWithTheme) || undefined;
60110
}
61111

62-
return { ...propsWithTheme, ...attrs };
112+
return attrs;
63113
}

0 commit comments

Comments
 (0)