Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/react-native-reanimated/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
### 🐛 Bug fixes

- Fix a short `animationDelay` list on web applying no delay to the animations past its end instead of repeating, the way CSS does and the native path already did. ([#10442](https://github.com/software-mansion/react-native-reanimated/pull/10442) by [@dennytosp](https://github.com/dennytosp))
- Fix padded string style values throwing, retaining their padding or parsing incorrectly, including SVG gradient stop offsets such as `'50% '`. ([#10422](https://github.com/software-mansion/react-native-reanimated/pull/10422) by [@matipl01](https://github.com/matipl01))
- Fix `filter` strings using the CSS `hue-rotate()` and `drop-shadow()` spellings being discarded together with every other filter in the same declaration. ([#10383](https://github.com/software-mansion/react-native-reanimated/pull/10383) by [@dennytosp](https://github.com/dennytosp))
- Fix single-argument `translate()` and `skew()` in transform strings repeating the argument on the Y axis instead of leaving it at zero, so `translate(100px)` no longer also moves the element down. ([#10385](https://github.com/software-mansion/react-native-reanimated/pull/10385) by [@dennytosp](https://github.com/dennytosp))
- Fix the Metro configuration type import to use the public package export. ([#10454](https://github.com/software-mansion/react-native-reanimated/pull/10454) by [@sneakykiwi](https://github.com/sneakykiwi))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,36 @@ describe(createPropsBuilder, () => {
)
);
});

describe('trimming string values', () => {
test('trims a string before handing it to the processor', () => {
const processor = jest.fn().mockReturnValue(0);
const builder = createBuilder({ margin: { process: processor } });

builder.build({ margin: ' 10px\n' });

expect(processor).toHaveBeenCalledWith('10px', {
target: ValueProcessorTarget.Default,
});
});

test('leaves non-string values untouched', () => {
const processor = jest.fn().mockReturnValue(0);
const builder = createBuilder({ margin: { process: processor } });

builder.build({ margin: 10 });

expect(processor).toHaveBeenCalledWith(10, {
target: ValueProcessorTarget.Default,
});
});

test('does not trim properties that have no processor', () => {
const builder = createBuilder({ margin: true });

expect(builder.build({ margin: ' 10px ' })).toEqual({
margin: ' 10px ',
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

import { ValueProcessorTarget } from '../../types';
import { createNativePropsBuilder } from '../propsBuilder';
import { createNativePropsBuilder, stylePropsBuilder } from '../propsBuilder';

describe('createNativePropsBuilder', () => {
describe('build without context', () => {
Expand Down Expand Up @@ -224,3 +224,43 @@ describe('createNativePropsBuilder', () => {
});
});
});

describe('stylePropsBuilder', () => {
describe('a padded string value parses like a bare one', () => {
test.each([
['backgroundColor', ' red ', 'red'],
['color', '\n#ff0000\n', '#ff0000'],
['borderTopColor', ' rgba(0, 0, 0, 0.5) ', 'rgba(0, 0, 0, 0.5)'],
['shadowColor', '\t#0f0', '#0f0'],
[
'transform',
' translate(25, 25) scale(2) ',
'translate(25, 25) scale(2)',
],
['transformOrigin', ' left top ', 'left top'],
['boxShadow', ' 0px 4px 8px red ', '0px 4px 8px red'],
['filter', ' blur(5px) brightness(0.5) ', 'blur(5px) brightness(0.5)'],
['fontWeight', ' bold ', 'bold'],
['aspectRatio', ' 1 / 2 ', '1 / 2'],
['gap', ' 8 ', '8'],
])('%s', (property, padded, bare) => {
expect(stylePropsBuilder.build({ [property]: padded })).toEqual(
stylePropsBuilder.build({ [property]: bare })
);
});
});

test('trims every padded value of a multi-property style', () => {
expect(
stylePropsBuilder.build({
backgroundColor: ' blue ',
transform: '\n rotate(45deg)\n',
opacity: 0.5,
})
).toEqual({
backgroundColor: 4278190335,
transform: [{ rotate: '45deg' }],
opacity: 0.5,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from '../types';
import { ValueProcessorTarget } from '../types';
import { isRecord } from '../utils';
import { processStyleValue } from './processStyleValue';

const MAX_PROCESS_DEPTH = 10;

Expand Down Expand Up @@ -93,7 +94,7 @@ export default function createPropsBuilder<
continue;
}

const processedValue = configValue(value, context);
const processedValue = processStyleValue(configValue, value, context);

if (isRecord(processedValue) && !isRecord(value)) {
// The value processor may return multiple values for a single property
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/common/style/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export * from './config';
export { default as createPropsBuilder } from './createPropsBuilder';
export * from './processors';
export * from './processStyleValue';
export { type NativePropsBuilder, stylePropsBuilder } from './propsBuilder';
export * from './registry';
// `AllStyleProps` is intentionally not re-exported — it is internal to the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

import type {
NonMutable,
ValueProcessor,
ValueProcessorContext,
} from '../types';

export function processStyleValue<V, R>(
processor: ValueProcessor<V, R>,
value: NonMutable<V>,
context?: ValueProcessorContext
) {
'worklet';
// CSS strips whitespace around a declaration value, so processors can
// assume they are given a trimmed string.
const normalizedValue = (
typeof value === 'string' ? value.trim() : value
) as NonMutable<V>;
return processor(normalizedValue, context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,6 @@ describe(processTransform, () => {
{ rotate: '45deg' },
],
},
{
input: ' translate(25, 25) scale(2) ',
output: [{ translateX: 25 }, { translateY: 25 }, { scale: 2 }],
},
{
input: '\n translate(25, 25)\n scale(2)\n',
output: [{ translateX: 25 }, { translateY: 25 }, { scale: 2 }],
},
{
input: 'translate(50, 50) scale(1.5, 2) skew(30deg, 15deg)',
output: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,6 @@ describe(processTransformOrigin, () => {
},
],
},
{
name: 'padded string syntax',
cases: [
{
name: 'surrounded by whitespace',
cases: [
{ input: ' 50% 50%', output: ['50%', '50%', 0] },
{ input: ' left top ', output: [0, 0, 0] },
{ input: '\n 25px 25% 25px\n', output: [25, '25%', 25] },
{ input: '\tcenter', output: ['50%', '50%', 0] },
],
},
],
},
{
name: 'array syntax',
cases: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ export const processTransform: ValueProcessor<TransformsArray | string> = (
}

return value
.trim()
.split(/\)\s*/)
.filter(Boolean)
.flatMap((part) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ export const processTransformOrigin: ValueProcessor<
> = (value) => {
'worklet';
const isArray = Array.isArray(value);
let components =
typeof value === 'string' ? value.trim().split(/\s+/) : value;
let components = typeof value === 'string' ? value.split(/\s+/) : value;
const customParse = isArray ? () => null : parsePx;

if (components.length < 1 || components.length > 3) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

import {
getPropsBuilder,
registerComponentPropsBuilder,
} from '../../../../../common';
import { SVG_TEXT_PROPERTIES_CONFIG } from '../../configs';
import { processNumberArray } from '../others';

describe(processNumberArray, () => {
test.each([
['1 2 3', ['1', '2', '3']],
['1,2,3', ['1', '2', '3']],
['1, 2 , 3', ['1', '2', '3']],
])('splits %j into %p', (input, expected) => {
expect(processNumberArray(input)).toEqual(expected);
});

test('wraps a lone number into an array', () => {
expect(processNumberArray(5)).toEqual([5]);
});

test('returns an array unchanged', () => {
expect(processNumberArray([1, 2])).toEqual([1, 2]);
});

// The processor is reached only through a props builder, which hands it an
// already trimmed value, so padding has to be covered through the builder.
test('the props builder strips the padding before splitting', () => {
registerComponentPropsBuilder('RNSVGText', SVG_TEXT_PROPERTIES_CONFIG);

expect(getPropsBuilder('RNSVGText').build({ x: ' 1, 2, 3 ' })).toEqual({
x: ['1', '2', '3'],
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

import { processPercentage } from '../percentage';

describe(processPercentage, () => {
test.each([
['50%', 0.5],
['0%', 0],
['100%', 1],
['12.5%', 0.125],
])('converts %s to %p', (input, expected) => {
expect(processPercentage(input)).toBe(expected);
});

test.each([
[0.25, 0.25],
['0.25', 0.25],
])('passes %p through as a plain number', (input, expected) => {
expect(processPercentage(input)).toBe(expected);
});

test.each([
['150%', 1],
[2, 1],
['-10%', 0],
[-1, 0],
['nonsense', 1],
])('clamps %p to %p', (input, expected) => {
expect(processPercentage(input)).toBe(expected);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ describe(processSVGGradientStops, () => {
});

describe('Sorting and Offsets', () => {
test('trims string offsets and opacity before processing', () => {
const result = processSVGGradientStops([
{ offset: ' 50% ', color: 'red', opacity: ' 50% ' },
]);

expect(result).toEqual([0.5, 2164195328]);
});

test('sorts stops by offset in ascending order', () => {
const input = [
{ offset: 1, color: 'blue' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const processNumberArray: ValueProcessor<
} else if (typeof value === 'number') {
return [value];
} else if (typeof value === 'string') {
return value.trim().replace(commaReg, ' ').split(spaceReg);
return value.replace(commaReg, ' ').split(spaceReg);
} else {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import type { NumberProp } from 'react-native-svg';

import type { ValueProcessor } from '../../../../common';

export const processPercentage: ValueProcessor<NumberProp, number> = (
percentage
) => {
export const processPercentage = ((percentage: NumberProp) => {
const value =
typeof percentage === 'string' && percentage.trim().endsWith('%')
typeof percentage === 'string' && percentage.endsWith('%')
? +percentage.slice(0, -1) / 100
: +percentage;
return isNaN(value) || value > 1 ? 1 : Math.max(value, 0);
};
}) satisfies ValueProcessor<NumberProp, number>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use strict';

import { logger, type ValueProcessor } from '../../../../common';
import {
logger,
processStyleValue,
type ValueProcessor,
} from '../../../../common';
import type { CSSGradientStop } from '../../../types';
import { processColorSVG } from './colors';
import { processPercentage } from './percentage';
Expand All @@ -16,15 +20,15 @@ export const processSVGGradientStops = ((stops) => {
}
const intermediate = stops.map((stop) => {
const rawColor = stop.color && processColorSVG(stop.color);
const stopOpacity = processPercentage(stop.opacity ?? 1);
const stopOpacity = processStyleValue(processPercentage, stop.opacity ?? 1);
const finalColor =
typeof rawColor === 'number' && typeof stopOpacity === 'number'
? ((Math.round(((rawColor >>> 24) & 0xff) * stopOpacity) << 24) |
(rawColor & 0x00ffffff)) >>>
0
: rawColor;
return {
offset: processPercentage(stop.offset ?? 0),
offset: processStyleValue(processPercentage, stop.offset ?? 0),
color: finalColor,
};
}) as { offset: number; color: number | false | string }[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';

import {
processColorsInProps,
processStyleValue,
processTransform,
processTransformOrigin,
stylePropsBuilder,
Expand Down Expand Up @@ -34,10 +35,16 @@ const updateProps: (
if (isAnimatedProps) {
processColorsInProps(updates);
if ('transformOrigin' in updates) {
updates.transformOrigin = processTransformOrigin(updates.transformOrigin);
updates.transformOrigin = processStyleValue(
processTransformOrigin,
updates.transformOrigin
);
}
if ('transform' in updates) {
updates.transform = processTransform(updates.transform);
updates.transform = processStyleValue(
processTransform,
updates.transform
);
}
}

Expand Down
Loading