Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Next

- Fixed: pass raw numeric values and raw color strings through instead of throwing when they are not theme keys, restoring compatibility with `react-native-reanimated` 4.4+ settled animations [#355](https://github.com/Shopify/restyle/issues/355) by [KAMRONBEK](https://github.com/KAMRONBEK)

## 2.4.5 - 2025-03-19

- Fixed: dist folder not being generated when building the project [#302](https://github.com/Shopify/restyle/pull/302) by [kelset](https://github.com/naqvitalha)
Expand Down
33 changes: 32 additions & 1 deletion src/test/createRestyleComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import createRestyleComponent from '../createRestyleComponent';
import {
backgroundColor,
BackgroundColorProps,
border,
BorderProps,
SpacingProps,
spacing,
SpacingShorthandProps,
Expand All @@ -25,6 +27,9 @@ const theme = {
spacing: {
s: 8,
},
borderRadii: {
s: 4,
},
breakpoints: {
phone: 0,
tablet: 376,
Expand Down Expand Up @@ -61,12 +66,13 @@ jest.mock('react-native/Libraries/Utilities/useWindowDimensions', () => ({

const Component = createRestyleComponent<
BackgroundColorProps<Theme> &
BorderProps<Theme> &
SpacingProps<Theme> &
SpacingShorthandProps<Theme> &
OpacityProps<Theme> &
ViewProps,
Theme
>([backgroundColor, spacing, spacingShorthand, opacity]);
>([backgroundColor, border, spacing, spacingShorthand, opacity]);
const cardVariant = createVariant<ThemeWithVariant, 'cardVariants'>({
themeKey: 'cardVariants',
});
Expand Down Expand Up @@ -230,5 +236,30 @@ describe('createRestyleComponent', () => {
style: [{gap: 8, columnGap: 8, rowGap: 8}],
});
});

// react-native-reanimated 4.4+ re-renders settled animations with the
// resolved raw style values set as top-level props (bypassing TypeScript,
// hence the casts), e.g. borderRadius={13} on an animated restyle component
it('passes raw numeric values through for numeric theme scales', () => {
const {root} = render(
<ThemeProvider theme={theme}>
<Component borderRadius={13 as never} />
</ThemeProvider>,
);
expect(root.findByType(View).props.style).toStrictEqual([
{borderRadius: 13},
]);
});

it('passes raw color strings through for the colors theme scale', () => {
const {root} = render(
<ThemeProvider theme={theme}>
<Component backgroundColor={'rgba(255, 209, 102, 1)' as never} />
</ThemeProvider>,
);
expect(root.findByType(View).props.style).toStrictEqual([
{backgroundColor: 'rgba(255, 209, 102, 1)'},
]);
});
});
});
79 changes: 78 additions & 1 deletion src/test/createRestyleFunction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import createRestyleFunction from '../createRestyleFunction';
import {RNStyle} from '../types';

const theme = {
colors: {},
colors: {
primary: '#FFB6C1',
},
spacing: {},
borderRadii: {
s: 4,
m: 8,
},
opacities: {
invisible: 0,
barelyVisible: 0.1,
Expand Down Expand Up @@ -128,5 +134,76 @@ describe('createRestyleFunction', () => {
).not.toThrow(/does not exist/);
});
});

describe('with a numeric theme scale', () => {
const styleFunc = createRestyleFunction({
property: 'borderRadius',
themeKey: 'borderRadii',
});

it('picks values from the theme', () => {
expect(
styleFunc.func({borderRadius: 'm'}, {theme, dimensions}),
).toStrictEqual({
borderRadius: 8,
});
});

it('passes a raw numeric value through when it is not a theme key', () => {
// react-native-reanimated 4.4+ re-renders settled animations with the
// resolved raw values set as top-level props, e.g. borderRadius={13}
expect(
styleFunc.func({borderRadius: 13}, {theme, dimensions}),
).toStrictEqual({
borderRadius: 13,
});
});

it('passes a raw numeric value through for screen-size specific props', () => {
expect(
styleFunc.func({borderRadius: {phone: 13}}, {theme, dimensions}),
).toStrictEqual({
borderRadius: 13,
});
});

it('throws an error when trying to use an invalid string theme value', () => {
expect(() =>
styleFunc.func({borderRadius: 'xxl'}, {theme, dimensions}),
).toThrow(/does not exist/);
});
});

describe('with the colors theme scale', () => {
const styleFunc = createRestyleFunction({
property: 'backgroundColor',
themeKey: 'colors',
});

it('passes a raw rgba color string through when it is not a theme key', () => {
expect(
styleFunc.func(
{backgroundColor: 'rgba(255, 209, 102, 1)'},
{theme, dimensions},
),
).toStrictEqual({
backgroundColor: 'rgba(255, 209, 102, 1)',
});
});

it('passes a raw hex color string through when it is not a theme key', () => {
expect(
styleFunc.func({backgroundColor: '#FFE6E4'}, {theme, dimensions}),
).toStrictEqual({
backgroundColor: '#FFE6E4',
});
});

it('throws an error when trying to use a misspelled theme color', () => {
expect(() =>
styleFunc.func({backgroundColor: 'primaryy'}, {theme, dimensions}),
).toThrow(/does not exist/);
});
});
});
});
24 changes: 23 additions & 1 deletion src/utilities/getThemeValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,39 @@ export function getThemeValue<
) {
if (transform) return transform({value, theme, themeKey});
if (isThemeKey(theme, themeKey)) {
if (value && theme[themeKey][value as string] === undefined)
if (value && theme[themeKey][value as string] === undefined) {
if (isRawStyleValue(value)) return value;

throw new Error(
`Value '${value}' does not exist in theme['${String(themeKey)}']`,
);
}

return value ? theme[themeKey][value as string] : value;
}

return value;
}

const rawColorValueRegex =
/^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|(rgb|rgba|hsl|hsla|hwb)\(.*\))$/i;

/**
* Whether a value that is not a key in the theme scale is a valid raw style
* value: a numeric literal (e.g. `borderRadius={13}`) or a raw color string
* (e.g. `backgroundColor="rgba(255, 209, 102, 1)"`). Such values are passed
* through as-is instead of throwing, so that libraries setting resolved style
* values as top-level props keep working (e.g. react-native-reanimated 4.4+
* re-renders settled animations with raw values), while unresolvable string
* keys (likely typos) still throw.
*/
function isRawStyleValue(value: PropValue): boolean {
return (
typeof value === 'number' ||
(typeof value === 'string' && rawColorValueRegex.test(value))
);
}

function isThemeKey<Theme extends BaseTheme>(
theme: Theme,
K: keyof Theme | undefined,
Expand Down
Loading