JS plumbing to get boxShadow into native (#45452)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/45452

With this we enable <View> to use BoxShadow.

BoxShadow property can be a string as defined on MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow

Or it can also be a list of BoxShadow primitives:
```
[
{
  offsetX: 10,
  offsetY: 5,
  color: 'red',
  inset: true,
},
{
  ...
},
]
```

The diff includes:

* Style sheet changes so typing is valid
* Process function to turn boxShadow format into parsed boxShadow primitive
* Test for process function
* View config changes on Android, iOS and ReactNativeStyleAttributes

Changelog: [Internal]

Reviewed By: joevilches

Differential Revision: D57872933

fbshipit-source-id: 2c5732709959bd996cce2f979549fc95cf2410e2
This commit is contained in:
Nick Gerleman
2024-07-17 20:35:17 -07:00
committed by Facebook GitHub Bot
parent 66465fb69d
commit 27c6fcbe72
9 changed files with 622 additions and 68 deletions
@@ -11,6 +11,7 @@
import type {AnyAttributeType} from '../../Renderer/shims/ReactNativeTypes';
import processAspectRatio from '../../StyleSheet/processAspectRatio';
import processBoxShadow from '../../StyleSheet/processBoxShadow';
import processColor from '../../StyleSheet/processColor';
import processFilter from '../../StyleSheet/processFilter';
import processFontVariant from '../../StyleSheet/processFontVariant';
@@ -125,6 +126,11 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
*/
experimental_mixBlendMode: true,
/*
* BoxShadow
*/
experimental_boxShadow: {process: processBoxShadow},
/**
* View
*/
@@ -170,6 +170,9 @@ const validAttributesForNonEventProps = {
process: require('../StyleSheet/processFilter').default,
},
experimental_mixBlendMode: true,
experimental_boxShadow: {
process: require('../StyleSheet/processBoxShadow').default,
},
opacity: true,
elevation: true,
shadowColor: {process: require('../StyleSheet/processColor').default},
@@ -223,6 +223,9 @@ const validAttributesForNonEventProps = {
experimental_filter: {
process: require('../StyleSheet/processFilter').default,
},
experimental_boxShadow: {
process: require('../StyleSheet/processBoxShadow').default,
},
borderTopWidth: true,
borderTopColor: {process: require('../StyleSheet/processColor').default},
@@ -11,25 +11,6 @@ import {Animated} from '../Animated/Animated';
import {ImageResizeMode} from '../Image/ImageResizeMode';
import {ColorValue} from './StyleSheet';
export type FilterPrimitive =
| {brightness: number | string}
| {blur: number | string}
| {contrast: number | string}
| {grayscale: number | string}
| {'hue-rotate': number | string}
| {invert: number | string}
| {opacity: number | string}
| {saturate: number | string}
| {sepia: number | string}
| {'drop-shadow': DropShadowPrimitive | string};
export type DropShadowPrimitive = {
offsetX: number | string;
offsetY: number | string;
standardDeviation?: number | string | undefined;
color?: ColorValue | number | undefined;
};
type FlexAlignType =
| 'flex-start'
| 'flex-end'
@@ -246,6 +227,52 @@ export interface TransformsStyle {
translateY?: AnimatableNumericValue | undefined;
}
export type FilterPrimitive =
| {brightness: number | string}
| {blur: number | string}
| {contrast: number | string}
| {grayscale: number | string}
| {'hue-rotate': number | string}
| {invert: number | string}
| {opacity: number | string}
| {saturate: number | string}
| {sepia: number | string}
| {'drop-shadow': DropShadowPrimitive | string};
export type DropShadowPrimitive = {
offsetX: number | string;
offsetY: number | string;
standardDeviation?: number | string | undefined;
color?: ColorValue | number | undefined;
};
export type BoxShadowPrimitive = {
offsetX: number | string;
offsetY: number | string;
color?: string | undefined;
blurRadius?: ColorValue | number | undefined;
spreadDistance?: number | string | undefined;
inset?: boolean | undefined;
};
export type BlendMode =
| 'normal'
| 'multiply'
| 'screen'
| 'overlay'
| 'darken'
| 'lighten'
| 'color-dodge'
| 'color-burn'
| 'hard-light'
| 'soft-light'
| 'difference'
| 'exclusion'
| 'hue'
| 'saturation'
| 'color'
| 'luminosity';
/**
* @see https://reactnative.dev/docs/view#style
*/
+31 -25
View File
@@ -34,25 +34,6 @@ export type EdgeInsetsValue = {
bottom: number,
};
export type FilterPrimitive =
| {brightness: number | string}
| {blur: number | string}
| {contrast: number | string}
| {grayscale: number | string}
| {'hue-rotate': number | string}
| {invert: number | string}
| {opacity: number | string}
| {saturate: number | string}
| {sepia: number | string}
| {'drop-shadow': DropShadowPrimitive | string};
export type DropShadowPrimitive = {
offsetX: number | string,
offsetY: number | string,
standardDeviation?: number | string,
color?: ____ColorValue_Internal | number,
};
export type DimensionValue = number | string | 'auto' | AnimatedNode | null;
export type AnimatableNumericValue = number | AnimatedNode;
@@ -709,11 +690,35 @@ export type ____ShadowStyle_Internal = $ReadOnly<{
...____ShadowStyle_InternalOverrides,
}>;
type ____FilterStyle_Internal = $ReadOnly<{
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
}>;
export type FilterPrimitive =
| {brightness: number | string}
| {blur: number | string}
| {contrast: number | string}
| {grayscale: number | string}
| {'hue-rotate': number | string}
| {invert: number | string}
| {opacity: number | string}
| {saturate: number | string}
| {sepia: number | string}
| {'drop-shadow': DropShadowPrimitive | string};
export type ____MixBlendMode_Internal =
export type DropShadowPrimitive = {
offsetX: number | string,
offsetY: number | string,
standardDeviation?: number | string,
color?: ____ColorValue_Internal,
};
export type BoxShadowPrimitive = {
offsetX: number | string,
offsetY: number | string,
color?: ____ColorValue_Internal,
blurRadius?: number | string,
spreadDistance?: number | string,
inset?: boolean,
};
type ____BlendMode_Internal =
| 'normal'
| 'multiply'
| 'screen'
@@ -735,8 +740,6 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
...$Exact<____LayoutStyle_Internal>,
...$Exact<____ShadowStyle_Internal>,
...$Exact<____TransformStyle_Internal>,
...____FilterStyle_Internal,
experimental_mixBlendMode?: ____MixBlendMode_Internal,
backfaceVisibility?: 'visible' | 'hidden',
backgroundColor?: ____ColorValue_Internal,
borderColor?: ____ColorValue_Internal,
@@ -775,6 +778,9 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
elevation?: number,
pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only',
cursor?: CursorValue,
experimental_boxShadow?: $ReadOnlyArray<BoxShadowPrimitive>,
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
experimental_mixBlendMode?: ____BlendMode_Internal,
}>;
export type ____ViewStyle_Internal = $ReadOnly<{
@@ -0,0 +1,269 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import processBoxShadow from '../processBoxShadow';
import processColor from '../processColor';
// js1 test processBoxShadow
describe('processBoxShadow', () => {
it('should parse basic string', () => {
expect(processBoxShadow('10px 5')).toEqual([
{
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse basic string with multiple whitespaces', () => {
expect(processBoxShadow('10px 5')).toEqual([
{
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse string with color', () => {
expect(processBoxShadow('red 10 5')).toEqual([
{
color: processColor('red'),
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse string with color function rgba', () => {
expect(processBoxShadow('rgba(255, 255, 255, 0.5) 10 5')).toEqual([
{
color: processColor('rgba(255, 255, 255, 0.5)'),
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse string with color function hsl', () => {
expect(processBoxShadow('hsl(318, 69%, 55%) 10 5')).toEqual([
{
color: processColor('hsl(318, 69%, 55%)'),
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse string with hex color', () => {
expect(processBoxShadow('#FFFFFF 10 5')).toEqual([
{
color: processColor('#FFFFFF'),
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse string with blurRadius', () => {
expect(processBoxShadow('red 10 5 2')).toEqual([
{
color: processColor('red'),
blurRadius: 2,
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse string with spreadDistance', () => {
expect(processBoxShadow('red 10 5 2 3')).toEqual([
{
color: processColor('red'),
blurRadius: 2,
offsetX: 10,
offsetY: 5,
spreadDistance: 3,
},
]);
});
it('should parse string arguments with units', () => {
expect(processBoxShadow('5px 2px')).toEqual([
{
offsetX: 5,
offsetY: 2,
},
]);
});
it('should parse string with inset', () => {
expect(processBoxShadow('5px 2px inset')).toEqual([
{
offsetX: 5,
offsetY: 2,
inset: true,
},
]);
});
it('should parse string with inset and color before and after lengths', () => {
expect(processBoxShadow('red 10 10 inset')).toEqual([
{
color: processColor('red'),
offsetX: 10,
offsetY: 10,
inset: true,
},
]);
});
it('should parse multiple box shadow strings', () => {
expect(
processBoxShadow('10 5 red, 5 12 inset, inset 10 45 13 red'),
).toEqual([
{
offsetX: 10,
offsetY: 5,
color: processColor('red'),
},
{
offsetX: 5,
offsetY: 12,
inset: true,
},
{
offsetX: 10,
offsetY: 45,
blurRadius: 13,
inset: true,
color: processColor('red'),
},
]);
});
it('should fail to parse string with invalid units', () => {
expect(processBoxShadow('red 10em 5$ 2| 3rp')).toEqual([]);
});
it('should fail to parse too many lengths', () => {
expect(processBoxShadow('10 5 2 3 10 10')).toEqual([]);
});
it('should fail to parse inset between lengths', () => {
expect(processBoxShadow('10 inset 5 2 3,')).toEqual([]);
});
it('should fail to parse double color', () => {
expect(processBoxShadow('red red 10 5')).toEqual([]);
});
it('should fail to parse double inset', () => {
expect(processBoxShadow('10 5 inset inset')).toEqual([]);
});
it('should fail to parse color between lengths', () => {
expect(processBoxShadow('10 red 5 2 3,')).toEqual([]);
});
it('should fail to parse invalid unit', () => {
expect(processBoxShadow('red 10foo 5 2 3,')).toEqual([]);
});
it('should fail to parse invalid argument', () => {
expect(processBoxShadow('red asf 5 2 3')).toEqual([]);
});
it('should fail to parse negative blur', () => {
expect(processBoxShadow('red 5 2 -3')).toEqual([]);
});
it('should parse simple object', () => {
expect(
processBoxShadow([
{
offsetX: 10,
offsetY: 5,
},
]),
).toEqual([
{
offsetX: 10,
offsetY: 5,
},
]);
});
it('should parse object with color', () => {
expect(
processBoxShadow([
{
offsetX: 10,
offsetY: 5,
color: 'red',
},
]),
).toEqual([
{
offsetX: 10,
offsetY: 5,
color: processColor('red'),
},
]);
});
it('should parse complex box shadow', () => {
expect(
processBoxShadow([
{
offsetX: '10px',
offsetY: 5,
blurRadius: 2,
spreadDistance: 3,
inset: true,
color: '#FFFFFF',
},
]),
).toEqual([
{
offsetX: 10,
offsetY: 5,
blurRadius: 2,
spreadDistance: 3,
inset: true,
color: processColor('#FFFFFF'),
},
]);
});
it('should fail to parse object with negative blur', () => {
expect(
processBoxShadow([
{
offsetX: 10,
offsetY: 5,
color: 'red',
blurRadius: -3,
},
]),
).toEqual([]);
});
it('should fail to parse object with invalid argument', () => {
expect(
processBoxShadow([
{
offsetX: 10,
offsetY: 'asdf',
},
]),
).toEqual([]);
});
});
@@ -0,0 +1,208 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react-native
*/
import type {ProcessedColorValue} from './processColor';
import type {BoxShadowPrimitive} from './StyleSheetTypes';
import processColor from './processColor';
export type ParsedBoxShadow = {
offsetX: number,
offsetY: number,
color?: ProcessedColorValue,
blurRadius?: number,
spreadDistance?: number,
inset?: boolean,
};
export default function processBoxShadow(
rawBoxShadows: $ReadOnlyArray<BoxShadowPrimitive> | string,
): Array<ParsedBoxShadow> {
const result: Array<ParsedBoxShadow> = [];
const boxShadowList =
typeof rawBoxShadows === 'string'
? parseBoxShadowString(rawBoxShadows)
: rawBoxShadows;
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;
if (value == null || value < 0) {
return [];
}
parsedBoxShadow.blurRadius = value;
break;
case 'color':
const color = processColor(rawBoxShadow.color);
if (color == null) {
return [];
}
parsedBoxShadow.color = color;
break;
case 'inset':
parsedBoxShadow.inset = rawBoxShadow.inset;
}
}
result.push(parsedBoxShadow);
}
return result;
}
function parseBoxShadowString(
rawBoxShadows: string,
): Array<BoxShadowPrimitive> {
let result: Array<BoxShadowPrimitive> = [];
for (const rawBoxShadow of rawBoxShadows
.split(/,(?![^()]*\))/) // split by comma that is not in parenthesis
.map(bS => bS.trim())
.filter(bS => bS !== '')) {
const boxShadow: BoxShadowPrimitive = {
offsetX: 0,
offsetY: 0,
};
let offsetX: number | string;
let offsetY: number | string;
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) {
const processedColor = processColor(arg);
if (processedColor != null) {
if (boxShadow.color != null) {
return [];
}
if (offsetX != null) {
keywordDetectedAfterLength = true;
}
boxShadow.color = arg;
continue;
}
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;
default:
return [];
}
}
if (offsetX == null || offsetY == null) {
return [];
}
boxShadow.offsetX = offsetX;
boxShadow.offsetY = offsetY;
result.push(boxShadow);
}
return result;
}
function parseLength(length: string): ?number {
// 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]);
}
@@ -7571,23 +7571,6 @@ export type EdgeInsetsValue = {
right: number,
bottom: number,
};
export type FilterPrimitive =
| { brightness: number | string }
| { blur: number | string }
| { contrast: number | string }
| { grayscale: number | string }
| { \\"hue-rotate\\": number | string }
| { invert: number | string }
| { opacity: number | string }
| { saturate: number | string }
| { sepia: number | string }
| { \\"drop-shadow\\": DropShadowPrimitive | string };
export type DropShadowPrimitive = {
offsetX: number | string,
offsetY: number | string,
standardDeviation?: number | string,
color?: ____ColorValue_Internal | number,
};
export type DimensionValue = number | string | \\"auto\\" | AnimatedNode | null;
export type AnimatableNumericValue = number | AnimatedNode;
export type CursorValue = \\"auto\\" | \\"pointer\\";
@@ -7700,10 +7683,32 @@ export type ____ShadowStyle_Internal = $ReadOnly<{
...____ShadowStyle_InternalCore,
...____ShadowStyle_InternalOverrides,
}>;
type ____FilterStyle_Internal = $ReadOnly<{
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
}>;
export type ____MixBlendMode_Internal =
export type FilterPrimitive =
| { brightness: number | string }
| { blur: number | string }
| { contrast: number | string }
| { grayscale: number | string }
| { \\"hue-rotate\\": number | string }
| { invert: number | string }
| { opacity: number | string }
| { saturate: number | string }
| { sepia: number | string }
| { \\"drop-shadow\\": DropShadowPrimitive | string };
export type DropShadowPrimitive = {
offsetX: number | string,
offsetY: number | string,
standardDeviation?: number | string,
color?: ____ColorValue_Internal,
};
export type BoxShadowPrimitive = {
offsetX: number | string,
offsetY: number | string,
color?: ____ColorValue_Internal,
blurRadius?: number | string,
spreadDistance?: number | string,
inset?: boolean,
};
type ____BlendMode_Internal =
| \\"normal\\"
| \\"multiply\\"
| \\"screen\\"
@@ -7724,8 +7729,6 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
...$Exact<____LayoutStyle_Internal>,
...$Exact<____ShadowStyle_Internal>,
...$Exact<____TransformStyle_Internal>,
...____FilterStyle_Internal,
experimental_mixBlendMode?: ____MixBlendMode_Internal,
backfaceVisibility?: \\"visible\\" | \\"hidden\\",
backgroundColor?: ____ColorValue_Internal,
borderColor?: ____ColorValue_Internal,
@@ -7764,6 +7767,9 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
elevation?: number,
pointerEvents?: \\"auto\\" | \\"none\\" | \\"box-none\\" | \\"box-only\\",
cursor?: CursorValue,
experimental_boxShadow?: $ReadOnlyArray<BoxShadowPrimitive>,
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
experimental_mixBlendMode?: ____BlendMode_Internal,
}>;
export type ____ViewStyle_Internal = $ReadOnly<{
...____ViewStyle_InternalCore,
@@ -8005,6 +8011,21 @@ declare module.exports: processAspectRatio;
"
`;
exports[`public API should not change unintentionally Libraries/StyleSheet/processBoxShadow.js 1`] = `
"export type ParsedBoxShadow = {
offsetX: number,
offsetY: number,
color?: ProcessedColorValue,
blurRadius?: number,
spreadDistance?: number,
inset?: boolean,
};
declare export default function processBoxShadow(
rawBoxShadows: $ReadOnlyArray<BoxShadowPrimitive> | string
): Array<ParsedBoxShadow>;
"
`;
exports[`public API should not change unintentionally Libraries/StyleSheet/processColor.js 1`] = `
"export type ProcessedColorValue = number | NativeColorValue;
declare function processColor(
+12 -1
View File
@@ -32,7 +32,12 @@
* Either the import or the reference only needs to appear once, anywhere in the project.
*/
import {DimensionValue} from 'react-native/Libraries/StyleSheet/StyleSheetTypes';
import {
BlendMode,
BoxShadowPrimitive,
DimensionValue,
FilterPrimitive,
} from 'react-native/Libraries/StyleSheet/StyleSheetTypes';
export {};
@@ -142,4 +147,10 @@ declare module '.' {
*/
experimental_layoutConformance?: 'strict' | 'classic' | undefined;
}
export interface ViewStyle {
experimental_boxShadow?: BoxShadowPrimitive | undefined;
experimental_filter?: ReadonlyArray<FilterPrimitive> | undefined;
experimental_mixBlendMode?: BlendMode | undefined;
}
}