From 590740bac83684ecbad93599a4f960f9a85a2223 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Date: Wed, 17 Jul 2024 13:13:55 -0700 Subject: [PATCH 1/2] boxshadow Differential Revision: D57872933 --- .../View/ReactNativeStyleAttributes.js | 6 + .../NativeComponent/BaseViewConfig.android.js | 3 + .../NativeComponent/BaseViewConfig.ios.js | 3 + .../Libraries/StyleSheet/StyleSheetTypes.d.ts | 65 +++-- .../Libraries/StyleSheet/StyleSheetTypes.js | 56 ++-- .../__tests__/processBoxShadow-test.js | 269 ++++++++++++++++++ .../Libraries/StyleSheet/processBoxShadow.js | 208 ++++++++++++++ .../__snapshots__/public-api-test.js.snap | 67 +++-- packages/react-native/types/experimental.d.ts | 13 +- 9 files changed, 622 insertions(+), 68 deletions(-) create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js create mode 100644 packages/react-native/Libraries/StyleSheet/processBoxShadow.js diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 1fda63d5945b..bbbe3c18998c 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -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 */ diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 29f303eec7a5..431c6810e9ac 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -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}, diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index b43e14adbaca..378b5c272dca 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -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}, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index 0130b88580bc..24968554e1be 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -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 */ diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 90e100576a0f..a582a57b2a48 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -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, -}>; +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 ____MixBlendMode_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, + experimental_filter?: $ReadOnlyArray, + experimental_mixBlendMode?: ____BlendMode_Internal, }>; export type ____ViewStyle_Internal = $ReadOnly<{ diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js new file mode 100644 index 000000000000..d65369f62302 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js @@ -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([]); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/processBoxShadow.js b/packages/react-native/Libraries/StyleSheet/processBoxShadow.js new file mode 100644 index 000000000000..adba8cd2ce0d --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/processBoxShadow.js @@ -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 | string, +): Array { + const result: Array = []; + + 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 { + let result: Array = []; + + 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]); +} diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index aad3ddfa0a4d..93f88d5f51a0 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -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, -}>; -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, + experimental_filter?: $ReadOnlyArray, + 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 | string +): Array; +" +`; + exports[`public API should not change unintentionally Libraries/StyleSheet/processColor.js 1`] = ` "export type ProcessedColorValue = number | NativeColorValue; declare function processColor( diff --git a/packages/react-native/types/experimental.d.ts b/packages/react-native/types/experimental.d.ts index 2e9f69d0c842..ab0fafec57dc 100644 --- a/packages/react-native/types/experimental.d.ts +++ b/packages/react-native/types/experimental.d.ts @@ -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 | undefined; + experimental_mixBlendMode?: BlendMode | undefined; + } } From 473c2a76b502056d812e1d47f4b278147a103ccd Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 17 Jul 2024 15:03:04 -0700 Subject: [PATCH 2/2] Camelize filter function names in object notation (#45503) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45503 Kebab case object literals are a pain as an API to give folks. Keep string parsing using the kebab-case web names, like in CSS, but keep object notation camelCase. This is super super hacked up, and we should burn away all these viewconfig processors as soon as we can. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D59793095 --- .../Libraries/StyleSheet/StyleSheetTypes.d.ts | 4 +- .../Libraries/StyleSheet/StyleSheetTypes.js | 4 +- .../__tests__/processFilter-test.js | 56 ++++++++----------- .../Libraries/StyleSheet/processFilter.js | 22 +++++--- .../facebook/react/uimanager/FilterHelper.kt | 7 +-- .../react/renderer/graphics/Filter.h | 4 +- .../js/examples/Filter/FilterExample.js | 8 +-- 7 files changed, 50 insertions(+), 55 deletions(-) diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index 24968554e1be..78a9e732c487 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -232,12 +232,12 @@ export type FilterPrimitive = | {blur: number | string} | {contrast: number | string} | {grayscale: number | string} - | {'hue-rotate': number | string} + | {hueRotate: number | string} | {invert: number | string} | {opacity: number | string} | {saturate: number | string} | {sepia: number | string} - | {'drop-shadow': DropShadowPrimitive | string}; + | {dropShadow: DropShadowPrimitive | string}; export type DropShadowPrimitive = { offsetX: number | string; diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index a582a57b2a48..561cc30cdfad 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -695,12 +695,12 @@ export type FilterPrimitive = | {blur: number | string} | {contrast: number | string} | {grayscale: number | string} - | {'hue-rotate': number | string} + | {hueRotate: number | string} | {invert: number | string} | {opacity: number | string} | {saturate: number | string} | {sepia: number | string} - | {'drop-shadow': DropShadowPrimitive | string}; + | {dropShadow: DropShadowPrimitive | string}; export type DropShadowPrimitive = { offsetX: number | string, diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js index 0819b3044058..1f1357cc3f0b 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js @@ -40,12 +40,12 @@ describe('processFilter', () => { }, ]); - testNumericFilter('hue-rotate', 0, [{'hue-rotate': 0}]); - testUnitFilter('hue-rotate', 90, 'deg', [{'hue-rotate': 90}]); + testNumericFilter('hue-rotate', 0, [{hueRotate: 0}]); + testUnitFilter('hue-rotate', 90, 'deg', [{hueRotate: 90}]); testUnitFilter('hue-rotate', 1.5708, 'rad', [ - {'hue-rotate': (180 * 1.5708) / Math.PI}, + {hueRotate: (180 * 1.5708) / Math.PI}, ]); - testUnitFilter('hue-rotate', -90, 'deg', [{'hue-rotate': -90}]); + testUnitFilter('hue-rotate', -90, 'deg', [{hueRotate: -90}]); testUnitFilter('hue-rotate', 1.5, 'grad', []); testNumericFilter('hue-rotate', 90, []); testUnitFilter('hue-rotate', 50, '%', []); @@ -56,14 +56,9 @@ describe('processFilter', () => { {brightness: 0.5}, {opacity: 0.5}, {blur: 5}, - {'hue-rotate': '90deg'}, + {hueRotate: '90deg'}, ]), - ).toEqual([ - {brightness: 0.5}, - {opacity: 0.5}, - {blur: 5}, - {'hue-rotate': 90}, - ]); + ).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]); }); it('multiple filters one invalid', () => { expect( @@ -71,7 +66,7 @@ describe('processFilter', () => { {brightness: 0.5}, {opacity: 0.5}, {blur: 5}, - {'hue-rotate': '90foo'}, + {hueRotate: '90foo'}, ]), ).toEqual([]); }); @@ -98,7 +93,7 @@ describe('processFilter', () => { ), ).toEqual([ {brightness: 0.5}, - {'hue-rotate': 90}, + {hueRotate: 90}, {brightness: 0.5}, {brightness: 0.5}, ]); @@ -118,12 +113,7 @@ describe('processFilter', () => { it('string multiple filters', () => { expect( processFilter('brightness(0.5) opacity(0.5) blur(5) hue-rotate(90deg)'), - ).toEqual([ - {brightness: 0.5}, - {opacity: 0.5}, - {blur: 5}, - {'hue-rotate': 90}, - ]); + ).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]); }); it('string multiple filters one invalid', () => { expect( @@ -220,7 +210,7 @@ function createFilterPrimitive( case 'grayscale': return {grayscale: value}; case 'hue-rotate': - return {'hue-rotate': value}; + return {hueRotate: value}; case 'invert': return {invert: value}; case 'opacity': @@ -238,7 +228,7 @@ function testDropShadow() { it('should parse string drop-shadow', () => { expect(processFilter('drop-shadow(4px 4 10px red)')).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, color: processColor('red'), @@ -251,7 +241,7 @@ function testDropShadow() { it('should parse string negative offsets drop-shadow', () => { expect(processFilter('drop-shadow(-4 -4)')).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: -4, offsetY: -4, }, @@ -264,19 +254,19 @@ function testDropShadow() { processFilter('drop-shadow(4 4) drop-shadow(4 4) drop-shadow(4 4)'), ).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, }, }, { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, }, }, { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, }, @@ -289,7 +279,7 @@ function testDropShadow() { processFilter(' drop-shadow(4px 4 10px red) '), ).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, color: processColor('red'), @@ -306,7 +296,7 @@ function testDropShadow() { ), ).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, color: processColor('red'), @@ -321,7 +311,7 @@ function testDropShadow() { it('should parse string drop-shadow with color', () => { expect(processFilter('drop-shadow(50 50 purple)')).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 50, offsetY: 50, color: processColor('purple'), @@ -333,7 +323,7 @@ function testDropShadow() { it('should parse string with mixed case drop-shadow', () => { expect(processFilter('DroP-sHaDOw(50 50 purple)')).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 50, offsetY: 50, color: processColor('purple'), @@ -346,7 +336,7 @@ function testDropShadow() { expect( processFilter([ { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, color: '#FFFFFF', @@ -356,7 +346,7 @@ function testDropShadow() { ]), ).toEqual([ { - 'drop-shadow': { + dropShadow: { offsetX: 4, offsetY: 4, standardDeviation: 10, @@ -390,7 +380,7 @@ function testDropShadow() { expect( // $FlowExpectedError[incompatible-call] processFilter([ - {'drop-shadow': {offsetX: 4, offsetY: 5, invalid: 'invalid arg'}}, + {dropShadow: {offsetX: 4, offsetY: 5, invalid: 'invalid arg'}}, ]), ).toEqual([]); }); @@ -398,7 +388,7 @@ function testDropShadow() { it('should fail on invalid argument for drop-shadow object', () => { expect( // $FlowExpectedError[incompatible-call] - processFilter([{'drop-shadow': 8}]), + processFilter([{dropShadow: 8}]), ).toEqual([]); }); } diff --git a/packages/react-native/Libraries/StyleSheet/processFilter.js b/packages/react-native/Libraries/StyleSheet/processFilter.js index d952f3b17ca6..45328eb9335f 100644 --- a/packages/react-native/Libraries/StyleSheet/processFilter.js +++ b/packages/react-native/Libraries/StyleSheet/processFilter.js @@ -21,12 +21,12 @@ type ParsedFilter = | {blur: number} | {contrast: number} | {grayscale: number} - | {'hue-rotate': number} + | {hueRotate: number} | {invert: number} | {opacity: number} | {saturate: number} | {sepia: number} - | {'drop-shadow': ParsedDropShadow}; + | {dropShadow: ParsedDropShadow}; type ParsedDropShadow = { offsetX: number, @@ -49,17 +49,23 @@ export default function processFilter( if (filterName === 'drop-shadow') { const dropShadow = parseDropShadow(matches[2]); if (dropShadow != null) { - result.push({'drop-shadow': dropShadow}); + result.push({dropShadow}); } else { return []; } } else { - const amount = _getFilterAmount(filterName, matches[2]); + const camelizedName = + filterName === 'drop-shadow' + ? 'dropShadow' + : filterName === 'hue-rotate' + ? 'hueRotate' + : filterName; + const amount = _getFilterAmount(camelizedName, matches[2]); if (amount != null) { const filterPrimitive = {}; // $FlowFixMe The key will be the correct one but flow can't see that. - filterPrimitive[filterName] = amount; + filterPrimitive[camelizedName] = amount; // $FlowFixMe The key will be the correct one but flow can't see that. result.push(filterPrimitive); } else { @@ -73,13 +79,13 @@ export default function processFilter( } else { for (const filterPrimitive of filter) { const [filterName, filterValue] = Object.entries(filterPrimitive)[0]; - if (filterName === 'drop-shadow') { + if (filterName === 'dropShadow') { // $FlowFixMe const dropShadow = parseDropShadow(filterValue); if (dropShadow == null) { return []; } - result.push({'drop-shadow': dropShadow}); + result.push({dropShadow}); } else { const amount = _getFilterAmount(filterName, filterValue); @@ -125,7 +131,7 @@ function _getFilterAmount(filterName: string, filterArgs: mixed): ?number { switch (filterName) { // Hue rotate takes some angle that can have a unit and can be // negative. Additionally, 0 with no unit is allowed. - case 'hue-rotate': + case 'hueRotate': if (filterArgAsNumber === 0) { return 0; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt index 09abb4c17cb1..1707fcb36a8e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt @@ -39,12 +39,11 @@ internal object FilterHelper { "grayscale" -> createGrayscaleEffect((filter.value as Double).toFloat(), chainedEffects) "sepia" -> createSepiaEffect((filter.value as Double).toFloat(), chainedEffects) "saturate" -> createSaturateEffect((filter.value as Double).toFloat(), chainedEffects) - "hue-rotate" -> - createHueRotateEffect((filter.value as Double).toFloat(), chainedEffects) + "hueRotate" -> createHueRotateEffect((filter.value as Double).toFloat(), chainedEffects) "invert" -> createInvertEffect((filter.value as Double).toFloat(), chainedEffects) "blur" -> createBlurEffect((filter.value as Double).toFloat(), chainedEffects) "opacity" -> createOpacityEffect((filter.value as Double).toFloat(), chainedEffects) - "drop-shadow" -> + "dropShadow" -> parseAndCreateDropShadowEffect(filter.value as ReadableMap, chainedEffects) else -> throw IllegalArgumentException("Invalid filter name: $filterName") } @@ -69,7 +68,7 @@ internal object FilterHelper { "grayscale" -> createGrayscaleColorMatrix(amount) "sepia" -> createSepiaColorMatrix(amount) "saturate" -> createSaturateColorMatrix(amount) - "hue-rotate" -> createHueRotateColorMatrix(amount) + "hueRotate" -> createHueRotateColorMatrix(amount) "invert" -> createInvertColorMatrix(amount) "opacity" -> createOpacityColorMatrix(amount) else -> throw IllegalArgumentException("Invalid color matrix filter: $filterName") diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h b/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h index 8eec8fafa6c9..c4a7f18b068d 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h @@ -44,7 +44,7 @@ inline FilterType filterTypeFromString(std::string_view filterName) { return FilterType::Contrast; } else if (filterName == "grayscale") { return FilterType::Grayscale; - } else if (filterName == "hue-rotate") { + } else if (filterName == "hueRotate") { return FilterType::HueRotate; } else if (filterName == "invert") { return FilterType::Invert; @@ -54,7 +54,7 @@ inline FilterType filterTypeFromString(std::string_view filterName) { return FilterType::Saturate; } else if (filterName == "sepia") { return FilterType::Sepia; - } else if (filterName == "drop-shadow") { + } else if (filterName == "dropShadow") { return FilterType::DropShadow; } else { throw std::invalid_argument(std::string(filterName)); diff --git a/packages/rn-tester/js/examples/Filter/FilterExample.js b/packages/rn-tester/js/examples/Filter/FilterExample.js index 36ed2d74c713..d527f2008ba7 100644 --- a/packages/rn-tester/js/examples/Filter/FilterExample.js +++ b/packages/rn-tester/js/examples/Filter/FilterExample.js @@ -174,13 +174,13 @@ exports.examples = [ }, { title: 'Hue Rotate', - description: 'hue-rotate(-90deg)', - name: 'hue-rotate', + description: 'hueRotate(-90deg)', + name: 'hueRotate', platform: 'android', render(): React.Node { return ( ); }, @@ -219,7 +219,7 @@ exports.examples = [ return (