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..78a9e732c487 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} + | {hueRotate: number | string} + | {invert: number | string} + | {opacity: number | string} + | {saturate: number | string} + | {sepia: number | string} + | {dropShadow: 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..561cc30cdfad 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} + | {hueRotate: number | string} + | {invert: number | string} + | {opacity: number | string} + | {saturate: number | string} + | {sepia: number | string} + | {dropShadow: 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, +}; -export type ____MixBlendMode_Internal = +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/__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/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/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/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/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/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; + } } 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 (