react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
150 lines (130 loc) • 4.81 kB
text/typescript
;
import { logger } from '../../../common';
import { type WebPropsBuilder, webPropsBuilder } from '../../../common/web';
import type { ReanimatedHTMLElement } from '../../../ReanimatedModule/js-reanimated';
import {
ANIMATION_NAME_PREFIX,
NATIVE_PSEUDO_SELECTORS,
NATIVE_PSEUDO_SELECTORS_PRIORITY,
} from '../../constants';
import { getWebSvgPropsBuilder } from '../../svg/web';
import type { NativePseudoSelectorKey } from '../../types/pseudo';
import type { PseudoStylesBySelector } from '../../utils';
import { deepEqual } from '../../utils';
import { insertPseudoSelectorCSS, removePseudoSelectorCSS } from '../domUtils';
let pseudoSelectorCounter = 0;
const VIEW_ATTRIBUTE = `data-${ANIMATION_NAME_PREFIX}rps`;
const ACTIVE_MARKER_ATTRIBUTE = `data-${ANIMATION_NAME_PREFIX}rps-active`;
// Known selectors first (in cascade-priority order), then any arbitrary ones the
// web layer passes through unchanged. Order matters: later rules win on overlap.
function orderSelectors(
pseudoStylesBySelector: PseudoStylesBySelector
): string[] {
const known = NATIVE_PSEUDO_SELECTORS_PRIORITY.filter(
(sel) => sel in pseudoStylesBySelector
);
const unknown = Object.keys(pseudoStylesBySelector).filter(
(sel) => !NATIVE_PSEUDO_SELECTORS.has(sel as NativePseudoSelectorKey)
);
return [...known, ...unknown];
}
const SELECTOR_INJECTION_PATTERN = /[{};,]/;
function buildSelectorRule(
viewId: string,
selector: string,
pseudoStylesBySelector: PseudoStylesBySelector,
propsBuilder: WebPropsBuilder
): string | null {
if (SELECTOR_INJECTION_PATTERN.test(selector)) {
if (__DEV__) {
logger.warn(`Ignoring unsupported pseudo-selector "${selector}".`);
}
return null;
}
// !important is required so the pseudo styles override the element's inline
// styles (where the default values live).
const declarations = propsBuilder.build(
pseudoStylesBySelector[selector].selectorStyle,
{ important: true, includeUnprocessed: true }
);
if (!declarations) {
return null;
}
const base = `[${VIEW_ATTRIBUTE}="${viewId}"]`;
if (selector === ':active-deepest') {
return `${base}:active:not(:has([${ACTIVE_MARKER_ATTRIBUTE}="true"]:active)) { ${declarations} }`;
}
return `${base}${selector} { ${declarations} }`;
}
function buildRules(
viewId: string,
pseudoStylesBySelector: PseudoStylesBySelector,
propsBuilder: WebPropsBuilder
): string[] {
return orderSelectors(pseudoStylesBySelector)
.map((selector) =>
buildSelectorRule(viewId, selector, pseudoStylesBySelector, propsBuilder)
)
.filter((rule): rule is string => rule !== null);
}
export default class CSSPseudoSelectorsManager {
private readonly element: ReanimatedHTMLElement;
private readonly svgElementTag: string;
private viewId: string | null = null;
private prevPseudoStylesBySelector: PseudoStylesBySelector | null = null;
constructor(element: ReanimatedHTMLElement, svgElementTag = '') {
this.element = element;
this.svgElementTag = svgElementTag;
}
update(pseudoStylesBySelector: PseudoStylesBySelector | null): void {
if (deepEqual(pseudoStylesBySelector, this.prevPseudoStylesBySelector)) {
return;
}
this.prevPseudoStylesBySelector = pseudoStylesBySelector;
if (!pseudoStylesBySelector) {
this.detach();
return;
}
const propsBuilder =
getWebSvgPropsBuilder(this.svgElementTag) ?? webPropsBuilder;
const viewId = this.ensureViewId();
this.syncActiveMarker(pseudoStylesBySelector);
insertPseudoSelectorCSS(
viewId,
buildRules(viewId, pseudoStylesBySelector, propsBuilder)
);
}
unmountCleanup(): void {
this.detach();
}
private ensureViewId(): string {
if (this.viewId === null) {
this.viewId = String(pseudoSelectorCounter++);
this.element.setAttribute(VIEW_ATTRIBUTE, this.viewId);
}
return this.viewId;
}
private syncActiveMarker(
pseudoStylesBySelector: PseudoStylesBySelector
): void {
// The marker is set for both :active and :active-deepest registrants, so
// an ancestor's :active-deepest yields to a pressed descendant that has
// either of them (matches the iOS arbitration in REAPseudoSelectorObserver).
const hasAnyActive =
':active' in pseudoStylesBySelector ||
':active-deepest' in pseudoStylesBySelector;
if (hasAnyActive) {
this.element.setAttribute(ACTIVE_MARKER_ATTRIBUTE, 'true');
} else {
this.element.removeAttribute(ACTIVE_MARKER_ATTRIBUTE);
}
}
private detach(): void {
if (this.viewId !== null) {
this.element.removeAttribute(VIEW_ATTRIBUTE);
this.element.removeAttribute(ACTIVE_MARKER_ATTRIBUTE);
removePseudoSelectorCSS(this.viewId);
this.viewId = null;
}
}
}