react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
216 lines (194 loc) • 6.18 kB
text/typescript
;
import type { NativePropsBuilder, UnknownRecord } from '../../../common';
import { logger } from '../../../common';
import type { ShadowNodeWrapper } from '../../../commonTypes';
import { NATIVE_PSEUDO_SELECTORS } from '../../constants';
import type {
CSSTransitionProperties,
ICSSPseudoStylesManager,
NativePseudoSelectorKey,
} from '../../types';
import type { PseudoStylesBySelector } from '../../utils';
import { deepEqual } from '../../utils';
import { normalizeCSSTransitionProperties } from '../normalization';
import { registerPseudoStyles, unregisterPseudoStyles } from '../proxy';
import type {
CSSPseudoStyleEntry,
CSSTransitionConfig,
NormalizedCSSTransitionConfig,
} from '../types';
import { validatePseudoStyles } from './validatePseudoStyles';
export default class CSSPseudoStylesManager implements ICSSPseudoStylesManager {
private readonly viewTag: number;
private readonly shadowNodeWrapper: ShadowNodeWrapper;
private readonly propsBuilder: NativePropsBuilder;
private readonly componentName: string;
private prevPseudoStylesBySelector: PseudoStylesBySelector | null = null;
private prevTransitionProperties: CSSTransitionProperties | null = null;
private isRegistered = false;
constructor(
shadowNodeWrapper: ShadowNodeWrapper,
viewTag: number,
propsBuilder: NativePropsBuilder,
componentName: string
) {
this.shadowNodeWrapper = shadowNodeWrapper;
this.viewTag = viewTag;
this.propsBuilder = propsBuilder;
this.componentName = componentName;
}
update(
pseudoStylesBySelector: PseudoStylesBySelector | null,
transitionProperties: CSSTransitionProperties | null
): void {
if (
deepEqual(pseudoStylesBySelector, this.prevPseudoStylesBySelector) &&
deepEqual(transitionProperties, this.prevTransitionProperties)
) {
return;
}
const removedSelector = hasRemovedNativeSelector(
this.prevPseudoStylesBySelector,
pseudoStylesBySelector
);
this.prevPseudoStylesBySelector = pseudoStylesBySelector;
this.prevTransitionProperties = transitionProperties;
if (!pseudoStylesBySelector) {
if (this.isRegistered) {
this.detach();
}
return;
}
// Only a selector removal detaches; re-renders keep the registration alive.
if (this.isRegistered && removedSelector) {
this.detach();
}
const normalizedTransition = transitionProperties
? normalizeCSSTransitionProperties(transitionProperties)
: null;
const mergedDefaultStyle: UnknownRecord = {};
for (const [selector, { defaultStyle }] of Object.entries(
pseudoStylesBySelector
)) {
if (NATIVE_PSEUDO_SELECTORS.has(selector as NativePseudoSelectorKey)) {
Object.assign(mergedDefaultStyle, defaultStyle);
}
}
const builtDefaultStyle = this.propsBuilder.build(mergedDefaultStyle, {
includeUnprocessed: true,
});
nullifyUndefinedValues(builtDefaultStyle);
const selectors: CSSPseudoStyleEntry[] = [];
for (const [selector, { selectorStyle }] of Object.entries(
pseudoStylesBySelector
)) {
if (!NATIVE_PSEUDO_SELECTORS.has(selector as NativePseudoSelectorKey)) {
if (__DEV__) {
logger.warn(
`Pseudo selector "${selector}" is not supported on native and will be ignored.`
);
}
continue;
}
selectors.push(
this.buildPseudoStyleEntry(
selector as NativePseudoSelectorKey,
selectorStyle,
builtDefaultStyle,
normalizedTransition
)
);
}
if (selectors.length > 0) {
if (__DEV__) {
validatePseudoStyles(
pseudoStylesBySelector,
mergedDefaultStyle,
this.componentName
);
}
registerPseudoStyles(this.shadowNodeWrapper, {
defaultStyle: builtDefaultStyle,
selectors,
});
this.isRegistered = true;
}
}
unmountCleanup(): void {
if (this.isRegistered) {
this.detach();
}
this.prevPseudoStylesBySelector = null;
this.prevTransitionProperties = null;
}
private detach() {
unregisterPseudoStyles(this.viewTag);
this.isRegistered = false;
}
private buildPseudoStyleEntry(
selector: NativePseudoSelectorKey,
selectorStyle: UnknownRecord,
mergedDefaultStyle: UnknownRecord,
normalizedTransition: NormalizedCSSTransitionConfig | null
): CSSPseudoStyleEntry {
const builtSelectorStyle = this.propsBuilder.build(selectorStyle, {
includeUnprocessed: true,
});
nullifyUndefinedValues(builtSelectorStyle);
const transition: CSSTransitionConfig = {};
for (const prop of Object.keys(builtSelectorStyle)) {
const settings = getPropertyTransitionSettings(
prop,
normalizedTransition
);
transition[prop] = {
value: [mergedDefaultStyle[prop], builtSelectorStyle[prop]],
duration: settings?.duration ?? 0,
delay: settings?.delay ?? 0,
timingFunction: settings?.timingFunction ?? 'ease',
allowDiscrete: settings?.allowDiscrete ?? false,
};
}
return {
selector,
selectorStyle: builtSelectorStyle,
transition,
};
}
}
function hasRemovedNativeSelector(
prev: PseudoStylesBySelector | null,
next: PseudoStylesBySelector | null
): boolean {
if (!prev) {
return false;
}
const nextKeys = next ? new Set(Object.keys(next)) : new Set<string>();
return Object.keys(prev).some(
(selector) =>
NATIVE_PSEUDO_SELECTORS.has(selector as NativePseudoSelectorKey) &&
!nextKeys.has(selector)
);
}
function nullifyUndefinedValues(style: UnknownRecord): void {
for (const key in style) {
if (style[key] === undefined) {
style[key] = null;
}
}
}
function getPropertyTransitionSettings(
prop: string,
normalized: NormalizedCSSTransitionConfig | null
) {
if (!normalized) {
return null;
}
if (
normalized.specificProperties &&
!normalized.specificProperties.has(prop)
) {
return null;
}
return normalized.settings[prop] ?? normalized.settings.all ?? null;
}