Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse transition shorthand #6968

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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
19 changes: 16 additions & 3 deletions apps/common-app/src/apps/reanimated/examples/EmptyExample.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { Text, StyleSheet, View } from 'react-native';
import { StyleSheet, View, Button } from 'react-native';

import React from 'react';
import React, { useReducer } from 'react';
import Animated from 'react-native-reanimated';

export default function EmptyExample() {
const [state, toggleState] = useReducer((s) => !s, false);

return (
<View style={styles.container}>
<Text>Hello world!</Text>
<Animated.View
style={{
width: state ? 200 : 100,
height: state ? 200 : 100,
// eslint-disable-next-line no-inline-styles/no-inline-styles
backgroundColor: 'red',
// eslint-disable-next-line no-inline-styles/no-inline-styles
transition: 'all 0.5s 0.1s ease-in-out, height 1s steps(5)',
}}
/>
<Button title="Toggle width" onPress={toggleState} />
</View>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
normalizeDelay,
} from '../common';
import { normalizeTransitionBehavior } from './settings';
import { parseTransitionShorthand } from './shorthand';

export const ERROR_MESSAGES = {
invalidTransitionProperty: (
Expand All @@ -34,13 +35,28 @@ const hasNoTransitionProperties = (properties: string[]) =>
export function normalizeCSSTransitionProperties(
config: CSSTransitionProperties
): NormalizedCSSTransitionConfig | null {
const {
let {
transitionProperty = ['all'],
transitionDuration,
transitionTimingFunction,
transitionDelay,
} = convertConfigPropertiesToArrays(config);

if (config.transition) {
const parsed = parseTransitionShorthand(config.transition);
// @ts-ignore blabla
transitionProperty = parsed.map(
(transition) => transition.property ?? 'all'
);
transitionDuration = parsed.map((transition) => transition.duration ?? 0);
transitionDelay = parsed.map((transition) => transition.delay ?? 0);
// @ts-ignore blabla
transitionTimingFunction = parsed.map(
(transition) => transition.timingFunction ?? 'ease'
);
// TODO: respect order of keys in config
}

if (hasNoTransitionProperties(transitionProperty)) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { cubicBezier, linear, steps } from '../../../../easings';
import type { CSSTimingFunction } from '../../../../easings';
import { ReanimatedError } from '../../../../errors';

type TimingFunctionArgument = number | string | (number | string)[];

type TimingFunction = CSSTimingFunction;

type TransitionBehavior = 'normal' | 'allow-discrete';

type Transition = {
property?: string;
duration?: number;
timingFunction?: TimingFunction;
delay?: number;
behavior?: TransitionBehavior;
};

export function parseTransitionShorthand(value: string): Transition[] {
return splitByComma(value).map(parseSingleTransitionShorthand);
}

function parseSingleTransitionShorthand(value: string): Transition {
const transition: Transition = {};
const parts = splitByWhitespace(value);
for (const part of parts) {
if (part === 'all') {
transition.property = 'all';
continue;
}
if (part === 'normal' || part === 'allow-discrete') {
transition.behavior = parseTransitionBehavior(part);
continue;
}
if (smellsLikeTimeUnit(part)) {
const timeUnit = parseTimeUnit(part);
if (transition.duration === undefined) {
transition.duration = timeUnit;
continue;
}
if (transition.delay === undefined) {
transition.delay = timeUnit;
continue;
}
}
if (
transition.timingFunction === undefined &&
smellsLikeTimingFunction(part)
) {
transition.timingFunction = parseTimingFunction(part);
continue;
}
if (transition.property === undefined) {
transition.property = kebabCaseToCamelCase(part);
continue;
}
throw new ReanimatedError(`Invalid transition shorthand: ${value}`);
}
return transition;
}

function splitByComma(str: string) {
// split by comma not enclosed in parentheses
const parts: string[] = [];
let current = '';
let depth = 0;
for (const char of str) {
if (char === '(') {
depth++;
} else if (char === ')') {
depth--;
} else if (char === ',' && depth === 0) {
parts.push(current.trim());
current = '';
continue;
}
current += char;
}
parts.push(current.trim());
return parts;
}

function splitByWhitespace(str: string) {
// split by whitespace not enclosed in parentheses
return str.split(/\s+(?![^()]*\))/);
}

function parseTransitionBehavior(value: string): TransitionBehavior {
switch (value) {
case 'allow-discrete':
case 'normal':
return value;
}
throw new ReanimatedError(`Unsupported transition behavior: ${value}`);
}

function kebabCaseToCamelCase(str: string) {
return str.replace(/-./g, (x) => x[1].toUpperCase());
}

function smellsLikeTimingFunction(value: string) {
// TODO: implement more strict check
return [
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'linear',
'step-start',
'step-end',
'steps',
'cubic-bezier',
].includes(value.trim().split('(')[0]);
}

function smellsLikeTimeUnit(value: string) {
// TODO: implement more strict check
return /^-?(\d+)?(\.\d+)?(ms|s)$/.test(value);
}

function parseTimeUnit(value: string): number {
// TODO: implement more strict check
if (value.endsWith('ms')) {
return parseFloat(value); // already in ms
}
if (value.endsWith('s')) {
return parseFloat(value) * 1000; // convert to ms
}
throw new ReanimatedError(`Unsupported time unit: ${value}`);
}

function isFloat(str: string) {
return str !== '' && /^-?\d*(\.\d+)?$/.test(str);
}

function isPercent(str: string) {
return str.endsWith('%') && isFloat(str.slice(0, -1));
}

function parseTimingFunctionArgument(arg: string): TimingFunctionArgument {
const parts = splitByWhitespace(arg);
if (parts.length > 1) {
return parts.map(parseTimingFunctionArgument) as TimingFunctionArgument;
}
if (isFloat(arg)) {
return parseFloat(arg);
}
if (isPercent(arg)) {
return arg;
}
// TODO: throw error for unsupported values
return arg;
}

function parseTimingFunction(value: string): TimingFunction {
switch (value) {
case 'ease':
case 'ease-in':
case 'ease-out':
case 'ease-in-out':
case 'linear':
case 'step-start':
case 'step-end':
return value;
}

// TODO: implement more strict check
const regex = /^(.+)\((.+)\)$/;
if (!regex.test(value)) {
throw new ReanimatedError(`Unsupported timing function: ${value}`);
}

const [, name, args] = value.match(regex)!;

const parsedArgs = splitByComma(args).map(parseTimingFunctionArgument);

switch (name) {
case 'cubic-bezier':
// @ts-ignore blabla
return cubicBezier(...parsedArgs);
case 'linear':
// @ts-ignore blabla
return linear(...parsedArgs);
case 'steps':
// @ts-ignore blabla
return steps(...parsedArgs);
default:
throw new ReanimatedError(`Unsupported timing function: ${value}`);
}
}
2 changes: 2 additions & 0 deletions packages/react-native-reanimated/src/css/types/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type CSSTransitionDuration = TimeUnit;
export type CSSTransitionTimingFunction = CSSTimingFunction;
export type CSSTransitionDelay = TimeUnit;
export type CSSTransitionBehavior = 'normal' | 'allow-discrete';
export type CSSTransition = string;

type SingleCSSTransitionSettings = {
transitionDuration?: CSSTransitionDuration;
Expand All @@ -33,6 +34,7 @@ export type CSSTransitionProperties<S extends object = PlainStyle> =
AddArrayPropertyTypes<SingleCSSTransitionSettings> & {
transitionProperty?: CSSTransitionProperty<S>;
transitionBehavior?: CSSTransitionBehavior;
transition?: CSSTransition;
};

export type CSSTransitionProp = keyof CSSTransitionProperties;
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/css/utils/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const TRANSITION_PROPS: CSSTransitionProp[] = [
'transitionTimingFunction',
'transitionDelay',
'transitionBehavior',
'transition',
];

const ANIMATION_SETTINGS_SET = new Set<string>(ANIMATION_SETTINGS);
Expand Down
Loading