diff --git a/packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts b/packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts new file mode 100644 index 000000000000..1901a4ade934 --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts @@ -0,0 +1,172 @@ +'use strict'; + +import type { ValueProcessor } from '../../types'; +import { ValueProcessorTarget } from '../../types'; +import { ReanimatedError } from '../../errors'; +import createPropsBuilder from '../createPropsBuilder'; + +type TestStyle = { + width?: number; + margin?: string | number; + borderRadius?: number; + padding?: number; + shadowColor?: number; + shadowOpacity?: number; + shadowRadius?: number; + height?: number; +}; + +type ConfigEntry = boolean | { process: ValueProcessor } | 'loop'; + +type TestConfig = Record; + +const BASE_CONFIG: TestConfig = { + width: false, + margin: false, + borderRadius: false, + padding: false, + shadowColor: false, + shadowOpacity: false, + shadowRadius: false, + height: false, +}; + +const createBuilder = (configOverrides: Partial) => { + const config: TestConfig = { ...BASE_CONFIG, ...configOverrides }; + + return createPropsBuilder({ + config, + processConfigValue(configValue) { + if (configValue === true) { + return (value: unknown) => value; + } + + if (configValue === 'loop') { + return configValue; + } + + if ( + configValue && + typeof configValue === 'object' && + 'process' in configValue && + typeof configValue.process === 'function' + ) { + return configValue.process; + } + + return undefined; + }, + }); +}; + +describe(createPropsBuilder, () => { + test('skips undefined values unless includeUndefined is true', () => { + const builder = createBuilder({ + width: true, + margin: true, + borderRadius: true, + }); + + const style: TestStyle = { + width: undefined, + margin: 'auto', + borderRadius: 10, + }; + + expect(builder.build(style)).toEqual({ + margin: 'auto', + borderRadius: 10, + }); + + expect( + builder.build(style, { includeUndefined: true }) + ).toEqual({ + width: undefined, + margin: 'auto', + borderRadius: 10, + }); + }); + + test('ignores properties not present in config', () => { + const builder = createBuilder({ width: true }); + + const style: TestStyle = { + width: 120, + height: 300, + }; + + expect(builder.build(style)).toEqual({ width: 120 }); + }); + + test('passes provided context to processors', () => { + const processor = jest.fn().mockReturnValue(24); + const builder = createBuilder({ + borderRadius: { process: processor }, + }); + + builder.build({ borderRadius: 12 }, { + target: ValueProcessorTarget.CSS, + }); + + expect(processor).toHaveBeenCalledWith(12, { + target: ValueProcessorTarget.CSS, + }); + }); + + test('uses default target context when target not set', () => { + const processor = jest.fn().mockReturnValue(10); + const builder = createBuilder({ + padding: { process: processor }, + }); + + builder.build({ padding: 5 }); + + expect(processor).toHaveBeenCalledWith(5, { + target: ValueProcessorTarget.Default, + }); + }); + + test('merges record results without overwriting original props', () => { + const builder = createBuilder({ + shadowColor: { + process: () => ({ + shadowOpacity: 0.5, + shadowRadius: 6, + }), + }, + shadowOpacity: true, + shadowRadius: true, + }); + + const style: TestStyle = { + shadowColor: 0xff0000, + shadowOpacity: 0.8, + }; + + expect(builder.build(style)).toEqual({ + shadowOpacity: 0.8, + shadowRadius: 6, + }); + }); + + test('allows processors to return undefined based on includeUndefined option', () => { + const builder = createBuilder({ + width: { + process: () => undefined, + }, + }); + + expect(builder.build({ width: 10 })).toEqual({}); + expect( + builder.build({ width: 10 }, { includeUndefined: true }) + ).toEqual({ width: undefined }); + }); + + test('throws when processor resolution exceeds maximum depth', () => { + expect(() => + createBuilder({ + width: 'loop', + }) + ).toThrow(new ReanimatedError('Max process depth for props builder reached for property width')); + }); +}); diff --git a/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts b/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts deleted file mode 100644 index 49acbfb72474..000000000000 --- a/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; -import type { PlainStyle } from '../../types'; -import { ValueProcessorTarget } from '../../types'; -import createStyleBuilder from '../createStyleBuilder'; -// TODO - add more tests - -describe(createStyleBuilder, () => { - const styleBuilder = createStyleBuilder({ - width: true, - margin: true, - borderRadius: true, - flexDirection: true, - }); - - test("doesn't include undefined values", () => { - const style: PlainStyle = { - width: undefined, - margin: 'auto', - borderRadius: 10, - flexDirection: undefined, - }; - - expect(styleBuilder.buildFrom(style)).toEqual({ - margin: 'auto', - borderRadius: 10, - }); - }); - - test("doesn't include properties that are not in the config", () => { - const style: PlainStyle = { - width: 100, - height: 100, // height is not in the config - }; - - expect(styleBuilder.buildFrom(style)).toEqual({ - width: 100, - }); - }); - - test('passes context to processors', () => { - const processor = jest.fn(); - - const builder = createStyleBuilder( - { - borderRadius: { - process: processor, - }, - }, - { - target: ValueProcessorTarget.CSS, - } - ); - - builder.buildFrom({ borderRadius: 5 }); - - expect(processor).toHaveBeenCalledWith(5, { - target: ValueProcessorTarget.CSS, - }); - }); - - test('uses default target when none provided', () => { - const processor = jest.fn(); - - const builder = createStyleBuilder({ padding: { process: processor } }); - - builder.buildFrom({ padding: 8 }); - - expect(processor).toHaveBeenCalledWith(8, { - target: ValueProcessorTarget.Default, - }); - }); -}); diff --git a/packages/react-native-reanimated/src/common/style/config.ts b/packages/react-native-reanimated/src/common/style/config.ts index 9c3dca648cdf..f0e99f50c868 100644 --- a/packages/react-native-reanimated/src/common/style/config.ts +++ b/packages/react-native-reanimated/src/common/style/config.ts @@ -14,11 +14,11 @@ import { processTransform, processTransformOrigin, } from './processors'; -import type { StyleBuilderConfig } from './types'; +import type { PropsBuilderConfig } from './types'; const colorAttributes = { process: processColor }; -export const BASE_PROPERTIES_CONFIG: StyleBuilderConfig = { +export const BASE_PROPERTIES_CONFIG: PropsBuilderConfig = { /** Layout and Positioning */ // FLEXBOX flex: true, diff --git a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts new file mode 100644 index 000000000000..2edd0867b14a --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -0,0 +1,111 @@ +'use strict'; +import { ReanimatedError } from '../errors'; +import type { + UnknownRecord, + ValueProcessor, + ValueProcessorContext, +} from '../types'; +import { ValueProcessorTarget } from '../types'; +import { isRecord } from '../utils'; + +const MAX_PROCESS_DEPTH = 10; + +type CreatePropsBuilderParams = { + config: TPropsConfig; + processConfigValue: ( + configValue: TPropsConfig[keyof TPropsConfig] + ) => ValueProcessor | TPropsConfig[keyof TPropsConfig] | undefined; +}; + +export type PropsBuilderResult = { + build( + props: TProps, + options?: { + includeUndefined?: boolean; + target?: ValueProcessorTarget; + } + ): UnknownRecord; +}; + +export default function createPropsBuilder< + TProps extends UnknownRecord, + TPropsConfig extends UnknownRecord, +>({ + processConfigValue, + config, +}: CreatePropsBuilderParams): PropsBuilderResult { + const processedConfig = Object.entries(config).reduce< + Record + >((acc, [key, configValue]) => { + let processedValue: ReturnType = + configValue as TPropsConfig[keyof TPropsConfig]; + + let depth = 0; + while (processedValue) { + if (++depth > MAX_PROCESS_DEPTH) { + throw new ReanimatedError( + `Max process depth for props builder reached for property ${key}` + ); + } + + if (typeof processedValue === 'function') { + acc[key] = processedValue as ValueProcessor; + break; + } + + processedValue = processConfigValue(processedValue); + } + + return acc; + }, {}); + + return { + build( + props: Readonly, + { + includeUndefined = false, + target = ValueProcessorTarget.Default, + }: { + includeUndefined?: boolean; + target?: ValueProcessorTarget; + } = {} + ) { + 'worklet'; + const context: ValueProcessorContext = { target }; + + return Object.entries(props).reduce( + (acc, [key, value]) => { + const processor = processedConfig[key]; + + if (!processor) { + // Props is not supported, skip it + return acc; + } + + const processedValue = processor(value, context); + + const valueIsRecord = isRecord(value); + const processedValueIsRecord = isRecord(processedValue); + + if (processedValue === undefined && !includeUndefined) { + // Skip if value is undefined and we don't want to include undefined values + return acc; + } + + if (processedValueIsRecord && !valueIsRecord) { + for (const processedKey in processedValue) { + if (!(processedKey in props)) { + acc[processedKey] = processedValue[processedKey]; + } + } + } else { + acc[key] = processedValue; + } + + return acc; + }, + {} + ); + }, + }; +} diff --git a/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts b/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts deleted file mode 100644 index 991bcd2180b8..000000000000 --- a/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -import type { AnyRecord, ValueProcessorContext } from '../types'; -import { ValueProcessorTarget } from '../types'; -import { isConfigPropertyAlias, isDefined, isRecord } from '../utils'; -import type { - StyleBuilder, - StyleBuilderConfig, - StyleBuildMiddleware, -} from './types'; - -type StyleBuilderOptions

= { - buildMiddleware?: StyleBuildMiddleware

; - separatelyInterpolatedNestedProperties?: (keyof P)[]; - target?: ValueProcessorTarget; -}; - -class StyleBuilderImpl

implements StyleBuilder

{ - private readonly buildMiddleware: StyleBuildMiddleware

; - private readonly config: StyleBuilderConfig

; - private readonly separatelyInterpolatedNestedProperties_: (keyof P)[]; - private readonly context: ValueProcessorContext; - - private processedProps = {} as P; - - constructor(config: StyleBuilderConfig

, options?: StyleBuilderOptions

) { - this.config = config; - this.buildMiddleware = options?.buildMiddleware ?? ((props) => props); - this.separatelyInterpolatedNestedProperties_ = - options?.separatelyInterpolatedNestedProperties ?? []; - this.context = { - target: options?.target ?? ValueProcessorTarget.Default, - }; - } - - isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean { - return this.separatelyInterpolatedNestedProperties_.includes(property); - } - - add(property: keyof P, value: P[keyof P]): void { - const configValue = this.config[property]; - - if (!configValue || !isDefined(value)) { - return; - } - - if (configValue === true) { - this.maybeAssignProp(property, value); - } else if (isConfigPropertyAlias

(configValue)) { - this.add(configValue.as, value); - } else { - const { process } = configValue; - const processedValue = process ? process(value, this.context) : value; - - if (!isDefined(processedValue)) { - return; - } - - if (isRecord

(processedValue)) { - this.maybeAssignProps(processedValue); - } else { - this.maybeAssignProp(property, processedValue); - } - } - } - - build(): P | null { - const result = this.buildMiddleware(this.processedProps); - this.cleanup(); - - if (Object.keys(result).length === 0) { - return null; - } - - return result; - } - - buildFrom(props: P): P | null { - Object.entries(props).forEach(([key, value]) => this.add(key, value)); - return this.build(); - } - - private maybeAssignProp(property: keyof P, value: P[keyof P]) { - this.processedProps[property] ??= value; - } - - private maybeAssignProps(props: P) { - Object.entries(props).forEach(([key, value]) => - this.maybeAssignProp(key, value) - ); - } - - private cleanup() { - this.processedProps = {} as P; - } -} - -export default function createStyleBuilder

( - config: StyleBuilderConfig

, - options?: StyleBuilderOptions

-): StyleBuilder> { - return new StyleBuilderImpl(config, options); -} diff --git a/packages/react-native-reanimated/src/common/style/index.ts b/packages/react-native-reanimated/src/common/style/index.ts index f74aa304acf5..4428e32d1898 100644 --- a/packages/react-native-reanimated/src/common/style/index.ts +++ b/packages/react-native-reanimated/src/common/style/index.ts @@ -1,6 +1,7 @@ 'use strict'; export * from './config'; -export { default as createStyleBuilder } from './createStyleBuilder'; export * from './processors'; -export type { StyleBuilder } from './types'; +export * from './registry'; +export { default as createPropsBuilder } from './createPropsBuilder'; +export { default as propsBuilder, createNativePropsBuilder } from './propsBuilder'; export type * from './types'; diff --git a/packages/react-native-reanimated/src/common/style/processors/shadows.ts b/packages/react-native-reanimated/src/common/style/processors/shadows.ts index 08886b5e981c..9bc527090efd 100644 --- a/packages/react-native-reanimated/src/common/style/processors/shadows.ts +++ b/packages/react-native-reanimated/src/common/style/processors/shadows.ts @@ -35,10 +35,10 @@ const parseBlurRadius = (value: string) => { export const processBoxShadowNative: ValueProcessor< ReadonlyArray | string, - ProcessedBoxShadowValue[] + ProcessedBoxShadowValue[] | null > = (value, context) => { if (value === 'none') { - return; + return null; } const parsedShadow = diff --git a/packages/react-native-reanimated/src/common/style/propsBuilder.ts b/packages/react-native-reanimated/src/common/style/propsBuilder.ts new file mode 100644 index 000000000000..e8ee9dcfb462 --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/propsBuilder.ts @@ -0,0 +1,69 @@ +'use strict'; +import type { + ConfigPropertyAlias, + PlainStyle, + UnknownRecord, + ValueProcessor, +} from '../types'; +import { isConfigPropertyAlias, isRecord } from '../utils'; +import { BASE_PROPERTIES_CONFIG } from './config'; +import createPropsBuilder from './createPropsBuilder'; + +const hasValueProcessor = ( + configValue: unknown +): configValue is { process: ValueProcessor } => + isRecord(configValue) && 'process' in configValue; + +type PropsBuilderPropertyConfig< + TProps extends UnknownRecord = UnknownRecord, + K extends keyof TProps = keyof TProps, +> = + | boolean // true - included, false - excluded + | ConfigPropertyAlias // alias for another property + | { + // value can have any type as it is passed to CPP where we can expect a different + // type than in the React Native stylesheet (e.g. number for colors instead of string) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + process: ValueProcessor[K], any>; // for custom value processing + }; + +export type NativePropsBuilder = ReturnType< + typeof createPropsBuilder; + }>> +>; + +export function createNativePropsBuilder( + config: Required<{ + [K in keyof TProps]: PropsBuilderPropertyConfig; + }> +) { + type TConfig = typeof config; + + return createPropsBuilder({ + config, + processConfigValue(configValue) { + if (configValue === true) { + return (value) => { + 'worklet'; + return value; + }; + } + if (isConfigPropertyAlias(configValue)) { + return config[configValue.as]; + } + if (hasValueProcessor(configValue)) { + return (value, context) => { + 'worklet'; + return configValue.process(value, context); + }; + } + }, + }); +} + +const propsBuilder = createNativePropsBuilder( + BASE_PROPERTIES_CONFIG +); + +export default propsBuilder; diff --git a/packages/react-native-reanimated/src/common/style/registry.ts b/packages/react-native-reanimated/src/common/style/registry.ts new file mode 100644 index 000000000000..fe2e8873c786 --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/registry.ts @@ -0,0 +1,74 @@ +'use strict'; +import { ReanimatedError } from '../errors'; +import type { PlainStyle, UnknownRecord } from '../types'; +import propsBuilder, { + createNativePropsBuilder, + type NativePropsBuilder, + type PropsBuilderConfig, +} from './propsBuilder'; + +export const ERROR_MESSAGES = { + propsBuilderNotFound: (componentName: string) => + `CSS props builder for component ${componentName} was not found`, +}; + +const DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Set([ + 'boxShadow', + 'shadowOffset', + 'textShadowOffset', + 'transformOrigin', +]); + +const COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Map< + string, + Set +>(); + +const basePropsBuilder = propsBuilder as NativePropsBuilder; + +const PROPS_BUILDERS: Record = {}; + +export function hasPropsBuilder(componentName: string): boolean { + return !!PROPS_BUILDERS[componentName] || componentName.startsWith('RCT'); +} + +export function getPropsBuilder(componentName: string) { + const componentPropsBuilder = PROPS_BUILDERS[componentName]; + + if (componentPropsBuilder) { + return componentPropsBuilder; + } + + if (componentName.startsWith('RCT')) { + // This captures all React Native components (prefixed with RCT) + return basePropsBuilder; + } + + throw new ReanimatedError(ERROR_MESSAGES.propsBuilderNotFound(componentName)); +} + +export function registerComponentPropsBuilder( + componentName: string, + config: PropsBuilderConfig, + options: { + separatelyInterpolatedNestedProperties?: readonly string[]; + } = {} +) { + PROPS_BUILDERS[componentName] = createNativePropsBuilder(config) as NativePropsBuilder; + + if (options.separatelyInterpolatedNestedProperties?.length) { + COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.set( + componentName, + new Set(options.separatelyInterpolatedNestedProperties) + ); + } +} + +export function getSeparatelyInterpolatedNestedProperties( + componentName: string +): ReadonlySet { + return ( + COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.get(componentName) ?? + DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES + ); +} diff --git a/packages/react-native-reanimated/src/common/style/types.ts b/packages/react-native-reanimated/src/common/style/types.ts index f5e1896b15ac..1554d2c9f0cd 100644 --- a/packages/react-native-reanimated/src/common/style/types.ts +++ b/packages/react-native-reanimated/src/common/style/types.ts @@ -1,19 +1,28 @@ 'use strict'; -import type { AnyRecord, ConfigPropertyAlias, ValueProcessor } from '../types'; +import type { + AnyRecord, + ConfigPropertyAlias, + ValueProcessor, + ValueProcessorContext, +} from '../types'; -export type StyleBuildMiddleware

= (props: P) => P; +export type PropsBuildMiddleware

= (props: P) => P; -export type StyleBuilder

= { - isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean; - add(property: keyof P, value: P[keyof P]): void; - buildFrom(props: P): P | null; +export type PropsBuilder

= { + build( + props: Readonly

, + options?: { + includeUndefined?: boolean; + target?: ValueProcessorContext['target']; + } + ): AnyRecord; }; type PropertyValueConfigBase

= | boolean // true - included, false - excluded | ConfigPropertyAlias

; // alias for another property -type StyleBuilderPropertyConfig< +type PropsBuilderPropertyConfig< P extends AnyRecord, K extends keyof P = keyof P, > = @@ -25,6 +34,6 @@ type StyleBuilderPropertyConfig< process: ValueProcessor[K], any>; // for custom value processing }; -export type StyleBuilderConfig

= { - [K in keyof Required

]: StyleBuilderPropertyConfig; +export type PropsBuilderConfig

= { + [K in keyof Required

]: PropsBuilderPropertyConfig; }; diff --git a/packages/react-native-reanimated/src/common/types/config.ts b/packages/react-native-reanimated/src/common/types/config.ts index 0de0902fe665..0d1bff1e6d0e 100644 --- a/packages/react-native-reanimated/src/common/types/config.ts +++ b/packages/react-native-reanimated/src/common/types/config.ts @@ -11,10 +11,10 @@ export type ValueProcessorContext = { target: ValueProcessorTarget; }; -export type ValueProcessor = ( +export type ValueProcessor = ( value: NonMutable, context?: ValueProcessorContext -) => Maybe | Record; +) => R | Record; export type ConfigPropertyAlias

= { as: keyof P; diff --git a/packages/react-native-reanimated/src/common/types/helpers.ts b/packages/react-native-reanimated/src/common/types/helpers.ts index 6a924c7633d0..2e9e4ba25a37 100644 --- a/packages/react-native-reanimated/src/common/types/helpers.ts +++ b/packages/react-native-reanimated/src/common/types/helpers.ts @@ -13,6 +13,7 @@ export type Maybe = T | null | undefined; export type NonMutable = T extends object ? Readonly : T; export type AnyRecord = Record; +export type UnknownRecord = Record; export type AnyComponent = ComponentType; diff --git a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts index dc82813b0d39..de64390aa584 100644 --- a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts +++ b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts @@ -2,76 +2,76 @@ import { BASE_PROPERTIES_CONFIG } from '../../../common'; import { ERROR_MESSAGES, - getStyleBuilder, - hasStyleBuilder, - registerComponentStyleBuilder, -} from '../registry'; + getPropsBuilder, + hasPropsBuilder, + registerComponentPropsBuilder, +} from '../../../common/style'; describe('registry', () => { - describe('hasStyleBuilder', () => { + describe('hasPropsBuilder', () => { test('returns true for registered component names', () => { const componentName = 'CustomComponent'; const config = { width: true, height: true }; - registerComponentStyleBuilder(componentName, config); + registerComponentPropsBuilder(componentName, config); - expect(hasStyleBuilder(componentName)).toBe(true); + expect(hasPropsBuilder(componentName)).toBe(true); }); test('returns true for RCT prefixed component names', () => { - expect(hasStyleBuilder('RCTView')).toBe(true); - expect(hasStyleBuilder('RCTText')).toBe(true); + expect(hasPropsBuilder('RCTView')).toBe(true); + expect(hasPropsBuilder('RCTText')).toBe(true); }); test('returns false for unregistered component names', () => { - expect(hasStyleBuilder('UnregisteredComponent')).toBe(false); + expect(hasPropsBuilder('UnregisteredComponent')).toBe(false); }); }); - describe('getStyleBuilder', () => { - test('returns registered style builder for custom component', () => { + describe('getPropsBuilder', () => { + test('returns registered props builder for custom component', () => { const componentName = 'CustomComponent'; const config = { width: true, height: true }; - registerComponentStyleBuilder(componentName, config); - const styleBuilder = getStyleBuilder(componentName); + registerComponentPropsBuilder(componentName, config); + const propsBuilder = getPropsBuilder(componentName); - expect(styleBuilder).toBeDefined(); - expect(typeof styleBuilder.buildFrom).toBe('function'); + expect(propsBuilder).toBeDefined(); + expect(typeof propsBuilder.build).toBe('function'); }); - test('returns base style builder for RCT prefixed components', () => { - const styleBuilder = getStyleBuilder('RCTView'); + test('returns base props builder for RCT prefixed components', () => { + const propsBuilder = getPropsBuilder('RCTView'); - expect(styleBuilder).toBeDefined(); - expect(typeof styleBuilder.buildFrom).toBe('function'); + expect(propsBuilder).toBeDefined(); + expect(typeof propsBuilder.build).toBe('function'); }); test('throws error for unregistered component names', () => { expect(() => { - getStyleBuilder('UnregisteredComponent'); - }).toThrow(ERROR_MESSAGES.styleBuilderNotFound('UnregisteredComponent')); + getPropsBuilder('UnregisteredComponent'); + }).toThrow(ERROR_MESSAGES.propsBuilderNotFound('UnregisteredComponent')); }); }); - describe('registerComponentStyleBuilder', () => { + describe('registerComponentPropsBuilder', () => { test('registers a style builder', () => { const componentName = 'TestComponent'; const config = { width: true, height: true }; - registerComponentStyleBuilder(componentName, config); + registerComponentPropsBuilder(componentName, config); - expect(hasStyleBuilder(componentName)).toBe(true); - expect(getStyleBuilder(componentName)).toBeDefined(); + expect(hasPropsBuilder(componentName)).toBe(true); + expect(getPropsBuilder(componentName)).toBeDefined(); }); test('works with base properties config', () => { const componentName = 'BaseConfigComponent'; - registerComponentStyleBuilder(componentName, BASE_PROPERTIES_CONFIG); + registerComponentPropsBuilder(componentName, BASE_PROPERTIES_CONFIG); - expect(hasStyleBuilder(componentName)).toBe(true); - expect(getStyleBuilder(componentName)).toBeDefined(); + expect(hasPropsBuilder(componentName)).toBe(true); + expect(getPropsBuilder(componentName)).toBeDefined(); }); }); }); diff --git a/packages/react-native-reanimated/src/css/native/index.ts b/packages/react-native-reanimated/src/css/native/index.ts index 22caa3f07256..6e8b3d3bb890 100644 --- a/packages/react-native-reanimated/src/css/native/index.ts +++ b/packages/react-native-reanimated/src/css/native/index.ts @@ -3,5 +3,4 @@ export * from './keyframes'; export * from './managers'; export * from './normalization'; export * from './proxy'; -export * from './registry'; export type * from './types'; diff --git a/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts b/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts index 2e5813c4b62a..ca64dd3a3820 100644 --- a/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts +++ b/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts @@ -3,7 +3,6 @@ import type { PlainStyle } from '../../../common'; import { CSSKeyframesRuleBase } from '../../models'; import type { CSSAnimationKeyframes } from '../../types'; import { normalizeAnimationKeyframes } from '../normalization'; -import { getStyleBuilder } from '../registry'; import type { NormalizedCSSAnimationKeyframesConfig } from '../types'; export default class CSSKeyframesRuleImpl< @@ -24,7 +23,7 @@ export default class CSSKeyframesRuleImpl< if (!this.normalizedKeyframesCache_[viewName]) { this.normalizedKeyframesCache_[viewName] = normalizeAnimationKeyframes( this.cssRules, - getStyleBuilder(viewName) + viewName ); } diff --git a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts index 6c1b9f4d8bb2..65f145dc5ad9 100644 --- a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts +++ b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts @@ -1,14 +1,12 @@ 'use strict'; -import type { AnyRecord } from '../../../common'; import { ReanimatedError } from '../../../common'; -import type { StyleBuilder } from '../../../common/style'; import type { ShadowNodeWrapper } from '../../../commonTypes'; import type { ViewInfo } from '../../../createAnimatedComponent/commonTypes'; import type { CSSStyle } from '../../types'; import type { ICSSManager } from '../../types/interfaces'; import { filterCSSAndStyleProperties } from '../../utils'; import { setViewStyle } from '../proxy'; -import { getStyleBuilder, hasStyleBuilder } from '../registry'; +import { getPropsBuilder, hasPropsBuilder } from '../../../common/style'; import CSSAnimationsManager from './CSSAnimationsManager'; import CSSTransitionsManager from './CSSTransitionsManager'; @@ -17,7 +15,9 @@ export default class CSSManager implements ICSSManager { private readonly cssTransitionsManager: CSSTransitionsManager; private readonly viewTag: number; private readonly viewName: string; - private readonly styleBuilder: StyleBuilder | null = null; + private readonly propsBuilder: + | ReturnType + | null = null; private isFirstUpdate: boolean = true; constructor({ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo) { @@ -25,8 +25,8 @@ export default class CSSManager implements ICSSManager { const wrapper = shadowNodeWrapper as ShadowNodeWrapper; this.viewName = viewName; - this.styleBuilder = hasStyleBuilder(viewName) - ? getStyleBuilder(viewName) + this.propsBuilder = hasPropsBuilder(viewName) + ? getPropsBuilder(viewName) : null; this.cssAnimationsManager = new CSSAnimationsManager( wrapper, @@ -40,13 +40,18 @@ export default class CSSManager implements ICSSManager { const [animationProperties, transitionProperties, filteredStyle] = filterCSSAndStyleProperties(style); - if (!this.styleBuilder && (animationProperties || transitionProperties)) { + if (!this.propsBuilder && (animationProperties || transitionProperties)) { throw new ReanimatedError( `Tried to apply CSS animations to ${this.viewName} which is not supported` ); } - const normalizedStyle = this.styleBuilder?.buildFrom(filteredStyle); + let normalizedStyle: CSSStyle | null = null; + if (this.propsBuilder) { + normalizedStyle = this.propsBuilder.build( + filteredStyle as Parameters[0] + ) as CSSStyle | null; + } // If the update is called during the first css style update, we won't // trigger CSS transitions and set styles before attaching CSS transitions diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts index 2be3af317edd..c7e7ac7392b1 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts @@ -1,12 +1,9 @@ 'use strict'; import { ReanimatedError } from '../../../../../common'; import type { CSSAnimationKeyframeSelector } from '../../../../types'; -import { getStyleBuilder } from '../../../registry'; import { ERROR_MESSAGES, normalizeAnimationKeyframes } from '../keyframes'; describe(normalizeAnimationKeyframes, () => { - const styleBuilder = getStyleBuilder('RCTView'); // Must be a valid view name - describe('offset normalization', () => { describe('when offset is valid', () => { test.each([ @@ -22,7 +19,7 @@ describe(normalizeAnimationKeyframes, () => { expect( normalizeAnimationKeyframes( { [offset]: { opacity: 1 } }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { opacity: [{ offset: expected, value: 1 }] }, @@ -39,7 +36,7 @@ describe(normalizeAnimationKeyframes, () => { expect(() => normalizeAnimationKeyframes( { [value]: { opacity: 1 } }, - styleBuilder + 'RCTView' ) ).toThrow( new ReanimatedError(ERROR_MESSAGES.invalidOffsetType(value)) @@ -56,7 +53,7 @@ describe(normalizeAnimationKeyframes, () => { expect(() => normalizeAnimationKeyframes( { [value]: { opacity: 1 } }, - styleBuilder + 'RCTView' ) ).toThrow( new ReanimatedError(ERROR_MESSAGES.invalidOffsetRange(value)) @@ -78,7 +75,7 @@ describe(normalizeAnimationKeyframes, () => { expect( normalizeAnimationKeyframes( { [offset]: { opacity: 1 } }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframeTimingFunctions: {}, @@ -100,7 +97,7 @@ describe(normalizeAnimationKeyframes, () => { ])('throws an error for %p', (offset, errorMsg) => { const value = offset as CSSAnimationKeyframeSelector; expect(() => - normalizeAnimationKeyframes({ [value]: { opacity: 1 } }, styleBuilder) + normalizeAnimationKeyframes({ [value]: { opacity: 1 } }, 'RCTView') ).toThrow(new ReanimatedError(errorMsg)); }); }); @@ -115,7 +112,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.5 }, to: { opacity: 1 }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -136,7 +133,7 @@ describe(normalizeAnimationKeyframes, () => { from: { shadowOffset: { width: 0, height: 0 } }, to: { shadowOffset: { width: 10, height: 10 } }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -165,7 +162,7 @@ describe(normalizeAnimationKeyframes, () => { '25%': { opacity: 0.25 }, from: { opacity: 0 }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -188,7 +185,7 @@ describe(normalizeAnimationKeyframes, () => { from: { transform: [{ scale: 0 }, { rotate: '0deg' }] }, to: { transform: [{ scale: 1 }, { rotate: '360deg' }] }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -208,7 +205,7 @@ describe(normalizeAnimationKeyframes, () => { from: { opacity: 0, transform: undefined }, to: { opacity: 1 }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -229,7 +226,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.5 }, to: {}, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -250,7 +247,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.75 }, '75%': { opacity: 1, animationTimingFunction: 'ease-out' }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -281,7 +278,7 @@ describe(normalizeAnimationKeyframes, () => { { '0%, 100%': { opacity: 0, animationTimingFunction: 'ease-in' }, }, - styleBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts index f624585aeb2b..88fb55c6e091 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts @@ -1,7 +1,7 @@ 'use strict'; import { ReanimatedError } from '../../../../../common'; import type { Repeat } from '../../../../types'; -import { getStyleBuilder } from '../../../registry'; +import { getPropsBuilder } from '../../../../../common/style'; import { ERROR_MESSAGES, normalizeAnimationKeyframes, @@ -9,6 +9,11 @@ import { processKeyframes, } from '../keyframes'; +type PropsBuilderInstance = ReturnType; +type BuildFn = PropsBuilderInstance['build']; +type BuildReturn = ReturnType; +type BuildArgs = Parameters; + describe(normalizeKeyframeSelector, () => { describe('single selector', () => { describe('keyword', () => { @@ -82,28 +87,21 @@ describe(normalizeKeyframeSelector, () => { }); }); -function mockStyleBuilder( - separatelyInterpolatedNestedProperties: string[] = [] -) { - const separatelyInterpolatedNestedPropertiesSet = new Set( - separatelyInterpolatedNestedProperties +const createMockPropsBuilder = () => { + const buildMock = jest.fn((style) => + style as BuildReturn ); return { - buildFrom: jest.fn().mockImplementation((props) => props), - isSeparatelyInterpolatedNestedProperty: jest - .fn() - .mockImplementation((property) => - separatelyInterpolatedNestedPropertiesSet.has(property) - ), - add: jest.fn(), + builder: { build: buildMock } as PropsBuilderInstance, + buildMock, }; -} +}; describe(processKeyframes, () => { describe('offset handling', () => { test('sorts keyframes and accepts percentages', () => { - const styleBuilder = mockStyleBuilder(); + const { builder } = createMockPropsBuilder(); const keyframes = { '75%': { opacity: 0.75 }, from: { opacity: 0 }, @@ -111,7 +109,7 @@ describe(processKeyframes, () => { to: { opacity: 1 }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, builder)).toEqual([ { offset: 0, style: { opacity: 0 } }, { offset: 0.25, style: { opacity: 0.25 } }, { offset: 0.75, style: { opacity: 0.75 } }, @@ -120,13 +118,13 @@ describe(processKeyframes, () => { }); test('splits multi-selector entries into separate keyframes', () => { - const styleBuilder = mockStyleBuilder(); + const { builder } = createMockPropsBuilder(); const keyframes = { 'from, 50%': { opacity: 0.5 }, to: { opacity: 1 }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, builder)).toEqual([ { offset: 0, style: { opacity: 0.5 } }, { offset: 0.5, style: { opacity: 0.5 } }, { offset: 1, style: { opacity: 1 } }, @@ -141,7 +139,9 @@ describe(processKeyframes, () => { '100%': { transform: [{ translateX: 100 }] }, }; - expect(processKeyframes(keyframes, getStyleBuilder('RCTView'))).toEqual([ + expect( + processKeyframes(keyframes, getPropsBuilder('RCTView')) + ).toEqual([ { offset: 0, style: { transform: [{ translateX: 0 }] } }, { offset: 1, style: { transform: [{ translateX: 100 }] } }, ]); @@ -155,7 +155,7 @@ describe(processKeyframes, () => { to: { transformOrigin: toTransformOrigin }, }; - const result = processKeyframes(keyframes, getStyleBuilder('RCTView')); + const result = processKeyframes(keyframes, getPropsBuilder('RCTView')); expect(result).toEqual([ { @@ -178,7 +178,7 @@ describe(processKeyframes, () => { to: { [property]: { width: 10, height: 5 } }, }; - const result = processKeyframes(keyframes, getStyleBuilder('RCTView')); + const result = processKeyframes(keyframes, getPropsBuilder('RCTView')); expect(result).toEqual([ { @@ -223,7 +223,7 @@ describe(processKeyframes, () => { }, }; - const result = processKeyframes(keyframes, getStyleBuilder('RCTView')); + const result = processKeyframes(keyframes, getPropsBuilder('RCTView')); expect(result).toEqual([ { @@ -280,30 +280,34 @@ describe(processKeyframes, () => { }); test('drops keyframes when processed style is undefined', () => { - const styleBuilder = mockStyleBuilder(['shadowOffset']); - styleBuilder.buildFrom + const { builder, buildMock } = createMockPropsBuilder(); + buildMock .mockImplementationOnce(() => undefined) - .mockImplementation((style) => style); + .mockImplementation((style) => style as BuildReturn); const keyframes = { from: { shadowOffset: { width: 0, height: 0 } }, to: { shadowOffset: { width: 10, height: 5 } }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, builder)).toEqual([ { offset: 1, style: { shadowOffset: { width: 10, height: 5 } } }, ]); + + buildMock.mockRestore(); }); test('merges styles for duplicate offsets', () => { - const styleBuilder = mockStyleBuilder(); + const { builder } = createMockPropsBuilder(); const keyframes = { '0%': { opacity: 0.5 }, '0': { transform: [{ scale: 1 }] }, '100%': { opacity: 1 }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect( + processKeyframes(keyframes, builder) + ).toEqual([ { offset: 0, style: { opacity: 0.5, transform: [{ scale: 1 }] }, @@ -314,8 +318,6 @@ describe(processKeyframes, () => { }); describe(normalizeAnimationKeyframes, () => { - const styleBuilder = getStyleBuilder('RCTView'); - test('aggregates styles and timing functions across keyframes', () => { const result = normalizeAnimationKeyframes( { @@ -326,7 +328,7 @@ describe(normalizeAnimationKeyframes, () => { }, to: { opacity: 1 }, }, - styleBuilder + 'RCTView' ); expect(result).toEqual({ @@ -353,7 +355,7 @@ describe(normalizeAnimationKeyframes, () => { from: { opacity: 0, animationTimingFunction: 'ease-in' }, to: { opacity: 1, animationTimingFunction: 'ease-out' }, }, - styleBuilder + 'RCTView' ); expect(result).toEqual({ diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts index 4714f7eb1d54..5acaa2fa8049 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { AnyRecord, StyleBuilder } from '../../../../common'; +import type { AnyRecord } from '../../../../common'; import { isDefined, isNumber, ReanimatedError } from '../../../../common'; import type { StyleProps } from '../../../../commonTypes'; import { PERCENTAGE_REGEX } from '../../../constants'; @@ -14,6 +14,10 @@ import type { NormalizedCSSKeyframeTimingFunctions, } from '../../types'; import { normalizeTimingFunction } from '../common'; +import { + getPropsBuilder, + getSeparatelyInterpolatedNestedProperties, +} from '../../../../common/style'; export const ERROR_MESSAGES = { invalidOffsetType: (selector: CSSAnimationKeyframeSelector) => @@ -67,12 +71,14 @@ type ProcessedKeyframes = Array<{ export function processKeyframes( keyframes: CSSAnimationKeyframes, - styleBuilder: StyleBuilder + propsBuilder: ReturnType ): ProcessedKeyframes { return Object.entries(keyframes) .flatMap( ([selector, { animationTimingFunction = undefined, ...style } = {}]) => { - const normalizedStyle = styleBuilder.buildFrom(style); + const normalizedStyle = propsBuilder.build( + style as Parameters['build']>[0] + ); if (!normalizedStyle) { return []; } @@ -98,19 +104,19 @@ export function processKeyframes( }, []); } -function processStyleProperties( +function processStyleProperties( offset: number, - style: S, + style: AnyRecord, keyframeStyle: AnyRecord, - styleBuilder: StyleBuilder + separatelyInterpolatedNestedProperties: ReadonlySet ) { Object.entries(style).forEach(([property, value]) => { if (!isDefined(value)) { return; } - if (typeof value === 'object') { - if (styleBuilder.isSeparatelyInterpolatedNestedProperty(property)) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + if (separatelyInterpolatedNestedProperties.has(property)) { if (!keyframeStyle[property]) { keyframeStyle[property] = Array.isArray(value) ? [] : {}; } @@ -118,7 +124,7 @@ function processStyleProperties( offset, value, keyframeStyle[property], - styleBuilder + separatelyInterpolatedNestedProperties ); return; } @@ -133,14 +139,22 @@ function processStyleProperties( export function normalizeAnimationKeyframes( keyframes: CSSAnimationKeyframes, - styleBuilder: StyleBuilder + viewName: string ): NormalizedCSSAnimationKeyframesConfig { + const propsBuilder = getPropsBuilder(viewName); + const separatelyInterpolatedNestedProperties = + getSeparatelyInterpolatedNestedProperties(viewName); const keyframesStyle: NormalizedCSSKeyframesStyle = {}; const timingFunctions: NormalizedCSSKeyframeTimingFunctions = {}; - processKeyframes(keyframes, styleBuilder).forEach( + processKeyframes(keyframes, propsBuilder).forEach( ({ offset, style, timingFunction }) => { - processStyleProperties(offset, style, keyframesStyle, styleBuilder); + processStyleProperties( + offset, + style, + keyframesStyle, + separatelyInterpolatedNestedProperties + ); if (timingFunction && offset < 1) { timingFunctions[offset] = normalizeTimingFunction(timingFunction); } diff --git a/packages/react-native-reanimated/src/css/native/registry.ts b/packages/react-native-reanimated/src/css/native/registry.ts deleted file mode 100644 index f7cacf62fe25..000000000000 --- a/packages/react-native-reanimated/src/css/native/registry.ts +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -import type { StyleBuilder, StyleBuilderConfig } from '../../common'; -import { - BASE_PROPERTIES_CONFIG, - createStyleBuilder, - ReanimatedError, - ValueProcessorTarget, -} from '../../common'; - -export const ERROR_MESSAGES = { - styleBuilderNotFound: (componentName: string) => - `CSS style builder for component ${componentName} was not found`, -}; - -const baseStyleBuilder = createStyleBuilder(BASE_PROPERTIES_CONFIG, { - separatelyInterpolatedNestedProperties: [ - 'boxShadow', - 'shadowOffset', - 'textShadowOffset', - 'transformOrigin', - ], - target: ValueProcessorTarget.CSS, -}); - -const STYLE_BUILDERS: Record = {}; - -export function hasStyleBuilder(componentName: string): boolean { - return !!STYLE_BUILDERS[componentName] || componentName.startsWith('RCT'); -} - -export function getStyleBuilder(componentName: string): StyleBuilder { - const styleBuilder = STYLE_BUILDERS[componentName]; - - if (styleBuilder) { - return styleBuilder; - } - - // This captures all React Native components - if (componentName.startsWith('RCT')) { - return baseStyleBuilder; - } - - throw new ReanimatedError(ERROR_MESSAGES.styleBuilderNotFound(componentName)); -} - -export function registerComponentStyleBuilder( - componentName: string, - config: StyleBuilderConfig -) { - STYLE_BUILDERS[componentName] = createStyleBuilder(config, { - target: ValueProcessorTarget.CSS, - }); -} diff --git a/packages/react-native-reanimated/src/css/svg/init.ts b/packages/react-native-reanimated/src/css/svg/init.ts index c3fe3b53ca8b..aa78db623330 100644 --- a/packages/react-native-reanimated/src/css/svg/init.ts +++ b/packages/react-native-reanimated/src/css/svg/init.ts @@ -1,5 +1,5 @@ 'use strict'; -import { registerComponentStyleBuilder } from '../native'; +import { registerComponentPropsBuilder } from '../../common/style'; import { SVG_CIRCLE_PROPERTIES_CONFIG, SVG_ELLIPSE_PROPERTIES_CONFIG, @@ -9,11 +9,11 @@ import { } from './native'; export function initSvgCssSupport() { - registerComponentStyleBuilder('RNSVGCircle', SVG_CIRCLE_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGEllipse', SVG_ELLIPSE_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGLine', SVG_LINE_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGPath', SVG_PATH_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGRect', SVG_RECT_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGCircle', SVG_CIRCLE_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGEllipse', SVG_ELLIPSE_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGLine', SVG_LINE_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGPath', SVG_PATH_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGRect', SVG_RECT_PROPERTIES_CONFIG); // TODO: Add more SVG components as they are implemented } diff --git a/packages/react-native-reanimated/src/css/svg/native/configs/common.ts b/packages/react-native-reanimated/src/css/svg/native/configs/common.ts index 4e57edaa60c8..5870568062a9 100644 --- a/packages/react-native-reanimated/src/css/svg/native/configs/common.ts +++ b/packages/react-native-reanimated/src/css/svg/native/configs/common.ts @@ -16,7 +16,7 @@ import type { TransformProps, } from 'react-native-svg'; -import type { StyleBuilderConfig } from '../../../../common'; +import type { PropsBuilderConfig } from '../../../../common/style'; import { convertStringToNumber, processColorSVG, @@ -26,11 +26,11 @@ import { const colorAttributes = { process: processColorSVG }; -const colorProps: StyleBuilderConfig = { +const colorProps: PropsBuilderConfig = { color: colorAttributes, }; -const fillProps: StyleBuilderConfig = { +const fillProps: PropsBuilderConfig = { fill: colorAttributes, fillOpacity: { process: processOpacity }, fillRule: { @@ -41,7 +41,7 @@ const fillProps: StyleBuilderConfig = { }, }; -const strokeProps: StyleBuilderConfig = { +const strokeProps: PropsBuilderConfig = { stroke: colorAttributes, strokeWidth: true, strokeOpacity: { process: processOpacity }, @@ -74,12 +74,12 @@ const strokeProps: StyleBuilderConfig = { }, }; -const clipProps: StyleBuilderConfig = { +const clipProps: PropsBuilderConfig = { clipRule: true, clipPath: true, // TODO - maybe preprocess this? }; -const transformProps: StyleBuilderConfig = { +const transformProps: PropsBuilderConfig = { translate: true, // TODO - add preprocessor (NumberArray) and split to translateX and translateY translateX: true, translateY: true, @@ -98,25 +98,25 @@ const transformProps: StyleBuilderConfig = { transform: true, // TODO - add preprocessor }; -const responderProps: StyleBuilderConfig< +const responderProps: PropsBuilderConfig< Omit > = { pointerEvents: true, }; // TODO - check what these props are doing and if we need to preprocess them -const commonMarkerProps: StyleBuilderConfig = { +const commonMarkerProps: PropsBuilderConfig = { marker: true, markerStart: true, markerMid: true, markerEnd: true, }; -const commonMaskProps: StyleBuilderConfig = { +const commonMaskProps: PropsBuilderConfig = { mask: true, // TODO - add preprocessor }; -const commonFilterProps: StyleBuilderConfig = { +const commonFilterProps: PropsBuilderConfig = { filter: true, // TODO - add preprocessor }; @@ -127,7 +127,7 @@ type NonAnimatablePropNames = | keyof NativeProps | keyof AccessibilityProps; -export type SvgStyleBuilderConfig = StyleBuilderConfig< +export type SvgStyleBuilderConfig = PropsBuilderConfig< Omit >; diff --git a/packages/react-native-reanimated/src/updateProps/updateProps.ts b/packages/react-native-reanimated/src/updateProps/updateProps.ts index 3f5085c5f16c..6bb823ad6549 100644 --- a/packages/react-native-reanimated/src/updateProps/updateProps.ts +++ b/packages/react-native-reanimated/src/updateProps/updateProps.ts @@ -6,18 +6,11 @@ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets'; import { IS_JEST, - processBoxShadowNative, - processColorsInProps, - processFilter, - processTransform, ReanimatedError, SHOULD_BE_USE_WEB, + propsBuilder, } from '../common'; -import { - processBoxShadowWeb, - processFilterWeb, - processTransformOrigin, -} from '../common/web'; +import { processBoxShadowWeb, processFilterWeb } from '../common/web'; import type { AnimatedStyle, ShadowNodeWrapper, @@ -55,24 +48,10 @@ if (SHOULD_BE_USE_WEB) { } else { updateProps = (viewDescriptors, updates) => { 'worklet'; - /* TODO: Improve this config structure in the future - * The goal is to create a simplified version of `src/css/platform/native/config.ts`, - * containing only properties that require processing and their associated processors - * */ - processColorsInProps(updates); - if ('transformOrigin' in updates) { - updates.transformOrigin = processTransformOrigin(updates.transformOrigin); - } - if ('transform' in updates) { - updates.transform = processTransform(updates.transform); - } - if ('boxShadow' in updates) { - updates.boxShadow = processBoxShadowNative(updates.boxShadow); - } - if ('filter' in updates) { - updates.filter = processFilter(updates.filter); - } - global.UpdatePropsManager.update(viewDescriptors, updates); + global.UpdatePropsManager.update( + viewDescriptors, + propsBuilder.build(updates) + ); }; }