Skip to content
Draft
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
90 changes: 80 additions & 10 deletions packages/react-native-reanimated/src/common/style/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,21 @@ import {
} from './propsBuilder';

export const ERROR_MESSAGES = {
propsBuilderNotFound: (componentName: string) =>
`CSS props builder for component ${componentName} was not found`,
propsBuilderNotFound: (
componentName: string,
componentChildName?: string
) => {
const compoundComponentName = getCompoundComponentName(
componentName,
componentChildName
);

const namesPart = componentChildName
? `${compoundComponentName} or ${componentName}`
: componentName;

return `CSS props builder for component ${namesPart} was not found`;
},
};

const DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Set<string>([
Expand All @@ -28,14 +41,34 @@ const COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Map<

const PROPS_BUILDERS = new Map<string, NativePropsBuilder>();

export function hasPropsBuilder(componentName: string): boolean {
export function hasPropsBuilder(
componentName: string,
componentChildName?: string
): boolean {
const compoundComponentName = getCompoundComponentName(
componentName,
componentChildName
);

return (
!!PROPS_BUILDERS.get(componentName) || isReactNativeViewName(componentName)
!!PROPS_BUILDERS.get(compoundComponentName) ||
!!PROPS_BUILDERS.get(componentName) ||
isReactNativeViewName(componentName)
);
}

export function getPropsBuilder(componentName: string): NativePropsBuilder {
const componentPropsBuilder = PROPS_BUILDERS.get(componentName);
export function getPropsBuilder(
componentName: string,
componentChildName?: string
): NativePropsBuilder {
const compoundComponentName = getCompoundComponentName(
componentName,
componentChildName
);

const componentPropsBuilder =
PROPS_BUILDERS.get(compoundComponentName) ??
PROPS_BUILDERS.get(componentName);

if (componentPropsBuilder) {
return componentPropsBuilder;
Expand All @@ -46,31 +79,68 @@ export function getPropsBuilder(componentName: string): NativePropsBuilder {
return stylePropsBuilder;
}

throw new ReanimatedError(ERROR_MESSAGES.propsBuilderNotFound(componentName));
throw new ReanimatedError(
ERROR_MESSAGES.propsBuilderNotFound(componentName, componentChildName)
);
}

export function registerComponentPropsBuilder<P extends UnknownRecord>(
componentName: string,
config: PropsBuilderConfig<P>,
options: {
separatelyInterpolatedNestedProperties?: readonly string[];
componentChildName?: string;
} = {}
) {
PROPS_BUILDERS.set(componentName, createNativePropsBuilder(config));
const compoundComponentName = getCompoundComponentName(
componentName,
options.componentChildName
);
PROPS_BUILDERS.set(compoundComponentName, createNativePropsBuilder(config));

// If the generalized version is missing but a specialization is provided,
// initialize the generalization with the default config.
if (options.componentChildName && !hasPropsBuilder(componentName)) {
PROPS_BUILDERS.set(componentName, createNativePropsBuilder(config));

if (options.separatelyInterpolatedNestedProperties?.length) {
COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.set(
componentName,
new Set(options.separatelyInterpolatedNestedProperties)
);
}
}

if (options.separatelyInterpolatedNestedProperties?.length) {
COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.set(
componentName,
compoundComponentName,
new Set(options.separatelyInterpolatedNestedProperties)
);
}
}

export function getSeparatelyInterpolatedNestedProperties(
componentName: string
componentName: string,
componentChildName?: string
): ReadonlySet<string> {
const compoundComponentName = getCompoundComponentName(
componentName,
componentChildName
);
return (
COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.get(
compoundComponentName
) ??
COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.get(componentName) ??
DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES
);
}

function getCompoundComponentName(
componentName: string,
componentChildName?: string
): string {
return componentChildName
? `${componentName}$${componentChildName}`
: componentName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ export default class AnimatedComponent<
}

if (!IS_JEST) {
this._CSSManager ??= new CSSManager(this._getViewInfo());
this._CSSManager ??= new CSSManager(
this._getViewInfo(),
this.ChildComponent.displayName
);
this._CSSManager?.update(this._cssStyle);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ describe('registry', () => {
expect(hasPropsBuilder(componentName)).toBe(true);
});

test('returns true for registered compound component names', () => {
const componentName = 'CustomComponent';
const componentChildName = 'CustomComponentChild';
const config = { width: true, height: true };

registerComponentPropsBuilder(componentName, config, {
componentChildName: componentChildName,
});

expect(hasPropsBuilder(componentName, componentChildName)).toBe(true);
});

test('returns true for component names if compound component was registered ', () => {
const componentName = 'CustomComponent';
const componentChildName = 'CustomComponentChild';
const config = { width: true, height: true };

registerComponentPropsBuilder(componentName, config, {
componentChildName: componentChildName,
});

expect(hasPropsBuilder(componentName)).toBe(true);
});

test('returns true for RCT prefixed component names', () => {
expect(hasPropsBuilder('RCTView')).toBe(true);
expect(hasPropsBuilder('RCTText')).toBe(true);
Expand All @@ -40,6 +64,52 @@ describe('registry', () => {
expect(typeof propsBuilder.build).toBe('function');
});

test('returns registered props builder for custom compound component', () => {
const componentName = 'CustomComponent';
const componentChildName = 'CustomComponentChild';
const config = { width: true, height: true };

registerComponentPropsBuilder(componentName, config, {
componentChildName: componentChildName,
});
const propsBuilder = getPropsBuilder(componentName, componentChildName);

expect(propsBuilder).toBeDefined();
expect(typeof propsBuilder.build).toBe('function');
});

test('returns registered props builder for component if custom compound component was registered', () => {
const componentName = 'CustomComponent';
const componentChildName = 'CustomComponentChild';
const config = { width: true, height: true };

registerComponentPropsBuilder(componentName, config, {
componentChildName: componentChildName,
});
const propsBuilder = getPropsBuilder(componentName);

expect(propsBuilder).toBeDefined();
expect(typeof propsBuilder.build).toBe('function');
});

test('returns registered props builder for component different than for the compound component', () => {
const componentName = 'CustomComponent';
const componentChildName = 'CustomComponentChild';
const config = { width: true, height: true };
const config2 = { width: true, height: true, length: true };

registerComponentPropsBuilder(componentName, config);
registerComponentPropsBuilder(componentName, config2, {
componentChildName: componentChildName,
});
const propsBuilder = getPropsBuilder(componentName);
const propsBuilder2 = getPropsBuilder(componentName, componentChildName);

expect(propsBuilder).toBeDefined();
expect(typeof propsBuilder.build).toBe('function');
expect(propsBuilder == propsBuilder2).toBe(false);
});

test('returns base props builder for RCT prefixed components', () => {
const propsBuilder = getPropsBuilder('RCTView');

Expand All @@ -52,6 +122,17 @@ describe('registry', () => {
getPropsBuilder('UnregisteredComponent');
}).toThrow(ERROR_MESSAGES.propsBuilderNotFound('UnregisteredComponent'));
});

test('throws error for unregistered component names for compound component', () => {
expect(() => {
getPropsBuilder('UnregisteredComponent', 'UnregisteredComponentChild');
}).toThrow(
ERROR_MESSAGES.propsBuilderNotFound(
'UnregisteredComponent',
'UnregisteredComponentChild'
)
);
});
});

describe('registerComponentPropsBuilder', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ class CSSKeyframesRegistry {
}
}

add(keyframesRule: CSSKeyframesRuleImpl, viewName: string, viewTag: number) {
add(
keyframesRule: CSSKeyframesRuleImpl,
viewName: string,
viewTag: number,
componentChildName?: string
) {
const existingKeyframesEntry = this.nameToKeyframes_.get(
keyframesRule.name
);
Expand Down Expand Up @@ -64,7 +69,7 @@ class CSSKeyframesRegistry {
registerCSSKeyframes(
keyframesRule.name,
viewName,
keyframesRule.getNormalizedKeyframesConfig(viewName)
keyframesRule.getNormalizedKeyframesConfig(viewName, componentChildName)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ export default class CSSKeyframesRuleImpl<
}

getNormalizedKeyframesConfig(
viewName: string
viewName: string,
componentChildName?: string
): NormalizedCSSAnimationKeyframesConfig {
if (!this.normalizedKeyframesCache_[viewName]) {
this.normalizedKeyframesCache_[viewName] = normalizeAnimationKeyframes(
this.cssRules,
viewName
viewName,
componentChildName
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,20 @@ export default class CSSAnimationsManager implements ICSSAnimationsManager {
private readonly shadowNodeWrapper: ShadowNodeWrapper;
private readonly viewName: string;
private readonly viewTag: number;
private readonly componentChildName?: string;

private attachedAnimations: ProcessedAnimation[] = [];

constructor(
shadowNodeWrapper: ShadowNodeWrapper,
viewName: string,
viewTag: number
viewTag: number,
componentChildName?: string
) {
this.shadowNodeWrapper = shadowNodeWrapper;
this.viewName = viewName;
this.viewTag = viewTag;
this.componentChildName = componentChildName;
}

update(animationProperties: ExistingCSSAnimationProperties | null): void {
Expand Down Expand Up @@ -81,7 +84,12 @@ export default class CSSAnimationsManager implements ICSSAnimationsManager {

// Register keyframes for all new animations
processedAnimations.forEach(({ keyframesRule }) => {
cssKeyframesRegistry.add(keyframesRule, this.viewName, this.viewTag);
cssKeyframesRegistry.add(
keyframesRule,
this.viewName,
this.viewTag,
this.componentChildName
);
newAnimationNames.add(keyframesRule.name);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ export default class CSSManager implements ICSSManager {
null;
private isFirstUpdate: boolean = true;

constructor({ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo) {
constructor(
{ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo,
componentChildName?: string
) {
const tag = (this.viewTag = viewTag as number);
const wrapper = shadowNodeWrapper as ShadowNodeWrapper;

this.viewName = viewName;
this.propsBuilder = hasPropsBuilder(viewName)
? getPropsBuilder(viewName)
this.propsBuilder = hasPropsBuilder(viewName, componentChildName)
? getPropsBuilder(viewName, componentChildName)
: null;
this.cssAnimationsManager = new CSSAnimationsManager(
wrapper,
viewName,
tag
tag,
componentChildName
);
this.cssTransitionsManager = new CSSTransitionsManager(wrapper, tag);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,12 @@ function processProps(

export function normalizeAnimationKeyframes(
keyframes: CSSAnimationKeyframes,
viewName: string
viewName: string,
componentChildName?: string
): NormalizedCSSAnimationKeyframesConfig {
const propsBuilder = getPropsBuilder(viewName);
const propsBuilder = getPropsBuilder(viewName, componentChildName);
const separatelyInterpolatedNestedProperties =
getSeparatelyInterpolatedNestedProperties(viewName);
getSeparatelyInterpolatedNestedProperties(viewName, componentChildName);
const propKeyframes: PropsWithKeyframes = {};
const timingFunctions: NormalizedCSSKeyframeTimingFunctions = {};

Expand Down