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

feat: Add support for boxShadow #6749

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6f85a87
add support for boxShadow
patrycjakalinska Nov 22, 2024
2954f56
PR review changes
patrycjakalinska Nov 22, 2024
c8d4615
add named import, ups
patrycjakalinska Nov 22, 2024
481c3ff
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska Nov 25, 2024
fd9d5da
fixes
patrycjakalinska Nov 25, 2024
a68aade
more fixes
patrycjakalinska Nov 25, 2024
96296d8
fix animation not animating bug
patrycjakalinska Dec 12, 2024
3deb7bc
add unit tests for boxShadow
patrycjakalinska Dec 12, 2024
e3b5f76
change processBoxShadow to work in place
patrycjakalinska Dec 12, 2024
dec4ba2
fix colors flickering when using withSpring
patrycjakalinska Dec 12, 2024
1e6d049
add type for object
patrycjakalinska Dec 12, 2024
244e2c0
lint fixes
patrycjakalinska Dec 12, 2024
6f61bb9
add init runtime tests
patrycjakalinska Dec 12, 2024
cba386f
add static runtime test
patrycjakalinska Dec 12, 2024
64a9e7a
add todo to boxShadow runtime test - as it is a newArch prop, the box…
patrycjakalinska Dec 12, 2024
6d05428
small fix for runtime test
patrycjakalinska Dec 12, 2024
5fa603a
Add ViewStyle type
patrycjakalinska Dec 12, 2024
18259dc
smol fix
patrycjakalinska Dec 12, 2024
af7a519
remove misleading comment
patrycjakalinska Dec 13, 2024
9bace78
Add comment explaining behaviour of spreading an animation object
patrycjakalinska Dec 13, 2024
e43a655
move clampRGBA to separate PR
patrycjakalinska Dec 16, 2024
af634fc
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska Dec 16, 2024
deedd17
replace comment with skip directive
patrycjakalinska Dec 19, 2024
346a461
replace array with prop
patrycjakalinska Dec 19, 2024
6bc6fec
Add TODO: to enable test when implemented on Fabric
patrycjakalinska Dec 19, 2024
456a2e3
typescript adjustments
patrycjakalinska Dec 20, 2024
3f7c69e
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska Dec 20, 2024
9f8bd41
Add comment with explaination
patrycjakalinska Dec 20, 2024
b34a22d
add boxShadow to test comparators
patrycjakalinska Dec 20, 2024
2ab6687
Merge branch 'main' into @patrycjakalinska/support-box-shadow
patrycjakalinska Dec 20, 2024
6b3cfd3
Change approach of color process in NestedProps
patrycjakalinska Jan 7, 2025
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
22 changes: 22 additions & 0 deletions packages/react-native-reanimated/src/Colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ export const ColorProperties = makeShareable([
'stroke',
]);

const NestedColorProperties = makeShareable({
boxShadow: ['color'],
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved
});

// // ts-prune-ignore-next Exported for the purpose of tests only
export function normalizeColor(color: unknown): number | null {
'worklet';
Expand Down Expand Up @@ -675,6 +679,24 @@ export function processColorsInProps(props: StyleProps) {
for (const key in props) {
if (ColorProperties.includes(key)) {
props[key] = processColor(props[key]);
} else if (
NestedColorProperties[key as keyof typeof NestedColorProperties]
) {
const nestedPropGroup = props[key] as StyleProps;
// most of the time there is only one nested prop in boxShadow array
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved
for (const groupKey in nestedPropGroup) {
const nestedProp = nestedPropGroup[groupKey] as StyleProps;

for (const propName in nestedProp) {
if (
NestedColorProperties[
key as keyof typeof NestedColorProperties
].includes(propName)
) {
nestedProp[propName] = processColor(nestedProp[propName]);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of iterating over the entire prop object, let's iterate over NestedColorProperties and check if those properties exist in the prop object because the prop object can potentially have more fields than those listed in NestedColorProperties

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prop object being the general object with all the props passed (backgroundColor, width, boxShadow etc.)? Because in current approach I use the loop that was there before (loop through keys in props), and if there is a boxShadow in props (boxShadow being an array by default), I iterate through objects in array (most cases one object), and check for if there is a color prop in boxShadow object

Can you elaborate more? Do you want to move checking the NestedProp outside the parent loop?

}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-reanimated/src/hook/useAnimatedStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
AnimatedStyle,
} from '../commonTypes';
import { isWorkletFunction } from '../commonTypes';
import { processBoxShadow } from '../processBoxShadow';
import { ReanimatedError } from '../errors';

const SHOULD_BE_USE_WEB = shouldBeUseWeb();
Expand Down Expand Up @@ -191,6 +192,9 @@ function styleUpdater(
let hasAnimations = false;
let frameTimestamp: number | undefined;
let hasNonAnimatedValues = false;
if (typeof newValues.boxShadow === 'string') {
processBoxShadow(newValues);
}
for (const key in newValues) {
const value = newValues[key];
if (isAnimated(value)) {
Expand Down
186 changes: 186 additions & 0 deletions packages/react-native-reanimated/src/processBoxShadow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/* based on:
* https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/StyleSheet/processBoxShadow.js
*/
'use strict';

import type { BoxShadowValue, OpaqueColorValue } from 'react-native';
import type { StyleProps } from '.';

function parseBoxShadowString(rawBoxShadows: string): Array<BoxShadowValue> {
'worklet';
const result: Array<BoxShadowValue> = [];

for (const rawBoxShadow of rawBoxShadows
.split(/,(?![^()]*\))/) // split by comma that is not in parenthesis
.map((bS) => bS.trim())
.filter((bS) => bS !== '')) {
const boxShadow: BoxShadowValue = {
offsetX: 0,
offsetY: 0,
};
let offsetX: number | string | null = null;
let offsetY: number | string | null = null;
let keywordDetectedAfterLength = false;

let lengthCount = 0;

// split rawBoxShadow string by all whitespaces that are not in parenthesis
const args = rawBoxShadow.split(/\s+(?![^(]*\))/);
for (const arg of args) {
if (arg === 'inset') {
if (boxShadow.inset != null) {
return [];
}
if (offsetX != null) {
keywordDetectedAfterLength = true;
}
boxShadow.inset = true;
continue;
}

switch (lengthCount) {
case 0:
offsetX = arg;
lengthCount++;
break;
case 1:
if (keywordDetectedAfterLength) {
return [];
}
offsetY = arg;
lengthCount++;
break;
case 2:
if (keywordDetectedAfterLength) {
return [];
}
boxShadow.blurRadius = arg;
lengthCount++;
break;
case 3:
if (keywordDetectedAfterLength) {
return [];
}
boxShadow.spreadDistance = arg;
lengthCount++;
break;
case 4:
if (keywordDetectedAfterLength) {
return [];
}
boxShadow.color = arg;
lengthCount++;
break;
default:
return [];
}
}

if (offsetX == null || offsetY == null) {
return [];
}

boxShadow.offsetX = offsetX;
boxShadow.offsetY = offsetY;

result.push(boxShadow);
}
return result;
}

function parseLength(length: string): number | null {
'worklet';
// matches on args with units like "1.5 5% -80deg"
const argsWithUnitsRegex = /([+-]?\d*(\.\d+)?)([\w\W]+)?/g;
const match = argsWithUnitsRegex.exec(length);

if (!match || Number.isNaN(match[1])) {
return null;
}

if (match[3] != null && match[3] !== 'px') {
return null;
}

return Number(match[1]);
}

type ParsedBoxShadow = {
offsetX: number;
offsetY: number;
blurRadius?: number | OpaqueColorValue;
spreadDistance?: number;
inset?: boolean;
color?: string;
};

export function processBoxShadow(props: StyleProps) {
'worklet';
const result: Array<ParsedBoxShadow> = [];

const boxShadowList = parseBoxShadowString(props.boxShadow as string);

for (const rawBoxShadow of boxShadowList) {
const parsedBoxShadow: ParsedBoxShadow = {
offsetX: 0,
offsetY: 0,
};

let value;
for (const arg in rawBoxShadow) {
switch (arg) {
case 'offsetX':
value =
typeof rawBoxShadow.offsetX === 'string'
? parseLength(rawBoxShadow.offsetX)
: rawBoxShadow.offsetX;
if (value == null) {
return [];
}

parsedBoxShadow.offsetX = value;
break;
case 'offsetY':
value =
typeof rawBoxShadow.offsetY === 'string'
? parseLength(rawBoxShadow.offsetY)
: rawBoxShadow.offsetY;
if (value == null) {
return [];
}

parsedBoxShadow.offsetY = value;
break;
case 'spreadDistance':
value =
typeof rawBoxShadow.spreadDistance === 'string'
? parseLength(rawBoxShadow.spreadDistance)
: rawBoxShadow.spreadDistance;
if (value == null) {
return [];
}

parsedBoxShadow.spreadDistance = value;
break;
case 'blurRadius':
value =
typeof rawBoxShadow.blurRadius === 'string'
? parseLength(rawBoxShadow.blurRadius)
: (rawBoxShadow.blurRadius as number);
if (value == null || value < 0) {
return [];
}

parsedBoxShadow.blurRadius = value;
break;
case 'color':
parsedBoxShadow.color = rawBoxShadow.color;
break;
case 'inset':
parsedBoxShadow.inset = rawBoxShadow.inset;
}
}
result.push(parsedBoxShadow);
}
props.boxShadow = result;
}
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/propsAllowlists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const PropsAllowlists: AllowlistsHolder = {
borderTopWidth: true,
borderWidth: true,
bottom: true,
boxShadow: true,
flex: true,
flexGrow: true,
flexShrink: true,
Expand Down