diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 5828d9cae031..19e41eef2ef2 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -13,6 +13,9 @@ import type {AnyAttributeType} from '../../Renderer/shims/ReactNativeTypes'; import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import processAspectRatio from '../../StyleSheet/processAspectRatio'; import processBackgroundImage from '../../StyleSheet/processBackgroundImage'; +import processBackgroundPosition from '../../StyleSheet/processBackgroundPosition'; +import processBackgroundRepeat from '../../StyleSheet/processBackgroundRepeat'; +import processBackgroundSize from '../../StyleSheet/processBackgroundSize'; import processBoxShadow from '../../StyleSheet/processBoxShadow'; import processColor from '../../StyleSheet/processColor'; import processFilter from '../../StyleSheet/processFilter'; @@ -144,11 +147,26 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { : {process: processBoxShadow}, /** - * Linear Gradient + * BackgroundImage */ experimental_backgroundImage: ReactNativeFeatureFlags.enableNativeCSSParsing() ? true : {process: processBackgroundImage}, + /** + * BackgroundSize + */ + experimental_backgroundSize: {process: processBackgroundSize}, + + /** + * BackgroundPosition + */ + experimental_backgroundPosition: {process: processBackgroundPosition}, + + /** + * BackgroundRepeat + */ + experimental_backgroundRepeat: {process: processBackgroundRepeat}, + /** * View */ diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 262d2b6ffc9c..e999e185f4fd 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -182,6 +182,15 @@ const validAttributesForNonEventProps = { experimental_backgroundImage: ReactNativeFeatureFlags.enableNativeCSSParsing() ? (true as const) : {process: require('../StyleSheet/processBackgroundImage').default}, + experimental_backgroundSize: { + process: require('../StyleSheet/processBackgroundSize').default, + }, + experimental_backgroundPosition: { + process: require('../StyleSheet/processBackgroundPosition').default, + }, + experimental_backgroundRepeat: { + process: require('../StyleSheet/processBackgroundRepeat').default, + }, boxShadow: ReactNativeFeatureFlags.enableNativeCSSParsing() ? (true as const) : {process: require('../StyleSheet/processBoxShadow').default}, diff --git a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js index dcfb65b6661d..58594cc23207 100644 --- a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js +++ b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js @@ -17,6 +17,12 @@ const ReactNativeStyleAttributes = const resolveAssetSource = require('../Image/resolveAssetSource').default; const processBackgroundImage = require('../StyleSheet/processBackgroundImage').default; +const processBackgroundPosition = + require('../StyleSheet/processBackgroundPosition').default; +const processBackgroundRepeat = + require('../StyleSheet/processBackgroundRepeat').default; +const processBackgroundSize = + require('../StyleSheet/processBackgroundSize').default; const processColor = require('../StyleSheet/processColor').default; const processColorArray = require('../StyleSheet/processColorArray').default; const processFilter = require('../StyleSheet/processFilter').default; @@ -203,6 +209,12 @@ function getProcessorForType(typeName: string): ?(nextProp: any) => any { return processFilter; case 'BackgroundImage': return processBackgroundImage; + case 'BackgroundPosition': + return processBackgroundPosition; + case 'BackgroundRepeat': + return processBackgroundRepeat; + case 'BackgroundSize': + return processBackgroundSize; case 'ImageSource': return resolveAssetSource; case 'BoxShadow': diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 3b49d33a82bc..745362c134a5 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -779,6 +779,43 @@ type RadialGradientValue = { export type BackgroundImageValue = LinearGradientValue | RadialGradientValue; +export type BackgroundSizeValue = + | { + x: string | number, + y: string | number, + } + | 'cover' + | 'contain'; + +export type BackgroundRepeatKeyword = + | 'repeat' + | 'space' + | 'round' + | 'no-repeat'; + +export type BackgroundPositionValue = + | { + top: number | string, + left: number | string, + } + | { + top: number | string, + right: number | string, + } + | { + bottom: number | string, + left: number | string, + } + | { + bottom: number | string, + right: number | string, + }; + +export type BackgroundRepeatValue = { + x: BackgroundRepeatKeyword, + y: BackgroundRepeatKeyword, +}; + export type BoxShadowValue = { offsetX: number | string, offsetY: number | string, @@ -853,6 +890,13 @@ export type ____ViewStyle_InternalBase = $ReadOnly<{ filter?: $ReadOnlyArray | string, mixBlendMode?: ____BlendMode_Internal, experimental_backgroundImage?: $ReadOnlyArray | string, + experimental_backgroundSize?: $ReadOnlyArray | string, + experimental_backgroundPosition?: + | $ReadOnlyArray + | string, + experimental_backgroundRepeat?: + | $ReadOnlyArray + | string, isolation?: 'auto' | 'isolate', }>; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js new file mode 100644 index 000000000000..16b4968233dc --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js @@ -0,0 +1,478 @@ +/** + * 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 + */ + +'use strict'; + +import processBackgroundPosition from '../processBackgroundPosition'; + +describe('processBackgroundPosition', () => { + // Test null/undefined input + it('should handle null input', () => { + expect(processBackgroundPosition(null)).toEqual([]); + }); + + it('should handle undefined input', () => { + expect(processBackgroundPosition(undefined)).toEqual([]); + }); + + // Test empty string input + it('should handle empty string', () => { + expect(processBackgroundPosition('')).toEqual([]); + }); + + // Test array input + it('should handle array input', () => { + expect(processBackgroundPosition([{top: '50%', left: '50%'}])).toEqual([ + {top: '50%', left: '50%'}, + ]); + }); + + it('should handle array with multiple positions', () => { + expect( + processBackgroundPosition([{bottom: '100%', right: '100%'}]), + ).toEqual([{bottom: '100%', right: '100%'}]); + }); + + // Test single keyword values + it('should parse left', () => { + expect(processBackgroundPosition('left')).toEqual([ + {top: '50%', left: '0%'}, + ]); + }); + + it('should parse center', () => { + expect(processBackgroundPosition('center')).toEqual([ + {top: '50%', left: '50%'}, + ]); + }); + + it('should parse right', () => { + expect(processBackgroundPosition('right')).toEqual([ + {top: '50%', left: '100%'}, + ]); + }); + + it('should parse top', () => { + expect(processBackgroundPosition('top')).toEqual([ + {top: '0%', left: '50%'}, + ]); + }); + + it('should parse bottom', () => { + expect(processBackgroundPosition('bottom')).toEqual([ + {top: '100%', left: '50%'}, + ]); + }); + + // Test single length/percentage values + it('should parse single px value', () => { + expect(processBackgroundPosition('100px')).toEqual([ + {top: '50%', left: 100}, + ]); + }); + + it('should parse single percentage value', () => { + expect(processBackgroundPosition('25%')).toEqual([ + {top: '50%', left: '25%'}, + ]); + }); + + it('should parse single decimal px value', () => { + expect(processBackgroundPosition('100.5px')).toEqual([ + {top: '50%', left: 100.5}, + ]); + }); + + it('should parse single decimal percentage value', () => { + expect(processBackgroundPosition('25.5%')).toEqual([ + {top: '50%', left: '25.5%'}, + ]); + }); + + // Test two-value syntax + it('should parse left top', () => { + expect(processBackgroundPosition('left top')).toEqual([ + {top: '0%', left: '0%'}, + ]); + }); + + it('should parse left center', () => { + expect(processBackgroundPosition('left center')).toEqual([ + {top: '50%', left: '0%'}, + ]); + }); + + it('should parse center top', () => { + expect(processBackgroundPosition('center top')).toEqual([ + {top: '0%', left: '50%'}, + ]); + }); + + it('should parse center center', () => { + expect(processBackgroundPosition('center center')).toEqual([ + {top: '50%', left: '50%'}, + ]); + }); + + it('should parse center bottom', () => { + expect(processBackgroundPosition('center bottom')).toEqual([ + {top: '100%', left: '50%'}, + ]); + }); + + it('should parse right center', () => { + expect(processBackgroundPosition('right center')).toEqual([ + {top: '50%', left: '100%'}, + ]); + }); + + it('should parse right bottom', () => { + expect(processBackgroundPosition('right bottom')).toEqual([ + {top: '100%', left: '100%'}, + ]); + }); + + // Test keyword with length/percentage + it('should parse left with px', () => { + expect(processBackgroundPosition('left 100px')).toEqual([ + {top: 100, left: '0%'}, + ]); + }); + + it('should parse left with percentage', () => { + expect(processBackgroundPosition('left 25%')).toEqual([ + {top: '25%', left: '0%'}, + ]); + }); + + it('should parse right with px', () => { + expect(processBackgroundPosition('right 100px')).toEqual([ + {top: 100, left: '100%'}, + ]); + }); + + it('should parse right with percentage', () => { + expect(processBackgroundPosition('right 25%')).toEqual([ + {top: '25%', left: '100%'}, + ]); + }); + + // Test length/percentage with keyword + it('should parse px with top', () => { + expect(processBackgroundPosition('100px top')).toEqual([ + {top: '0%', left: 100}, + ]); + }); + + it('should parse px with bottom', () => { + expect(processBackgroundPosition('100px bottom')).toEqual([ + {top: '100%', left: 100}, + ]); + }); + + it('should parse percentage with top', () => { + expect(processBackgroundPosition('25% top')).toEqual([ + {top: '0%', left: '25%'}, + ]); + }); + + it('should parse percentage with bottom', () => { + expect(processBackgroundPosition('25% bottom')).toEqual([ + {top: '100%', left: '25%'}, + ]); + }); + + // Test two length/percentage values + it('should parse px px', () => { + expect(processBackgroundPosition('100px 200px')).toEqual([ + {top: 200, left: 100}, + ]); + }); + + it('should parse percentage percentage', () => { + expect(processBackgroundPosition('25% 75%')).toEqual([ + {top: '75%', left: '25%'}, + ]); + }); + + it('should parse px percentage', () => { + expect(processBackgroundPosition('100px 75%')).toEqual([ + {top: '75%', left: 100}, + ]); + }); + + it('should parse percentage px', () => { + expect(processBackgroundPosition('25% 200px')).toEqual([ + {top: 200, left: '25%'}, + ]); + }); + + // Test three-value syntax + it('should parse center top with percentage', () => { + expect(processBackgroundPosition('center top 25%')).toEqual([ + {top: '25%', left: '50%'}, + ]); + }); + + it('should parse center bottom with percentage', () => { + expect(processBackgroundPosition('center bottom 25%')).toEqual([ + {bottom: '25%', left: '50%'}, + ]); + }); + + it('should parse left center with percentage', () => { + expect(processBackgroundPosition('left 25% center')).toEqual([ + {top: '50%', left: '25%'}, + ]); + }); + + it('should parse right center with percentage', () => { + expect(processBackgroundPosition('right 25% center')).toEqual([ + {top: '50%', right: '25%'}, + ]); + }); + + it('should parse left with percentage top', () => { + expect(processBackgroundPosition('left 25% top')).toEqual([ + {top: '0%', left: '25%'}, + ]); + }); + + it('should parse left with percentage bottom', () => { + expect(processBackgroundPosition('left 25% bottom')).toEqual([ + {bottom: '0%', left: '25%'}, + ]); + }); + + it('should parse right with percentage top', () => { + expect(processBackgroundPosition('right 25% top')).toEqual([ + {top: '0%', right: '25%'}, + ]); + }); + + it('should parse right with percentage bottom', () => { + expect(processBackgroundPosition('right 25% bottom')).toEqual([ + {bottom: '0%', right: '25%'}, + ]); + }); + + // Test four-value syntax + it('should parse left percentage top percentage', () => { + expect(processBackgroundPosition('left 25% top 75%')).toEqual([ + {top: '75%', left: '25%'}, + ]); + }); + + it('should parse right percentage top percentage', () => { + expect(processBackgroundPosition('right 25% top 75%')).toEqual([ + {top: '75%', right: '25%'}, + ]); + }); + + it('should parse left percentage bottom percentage', () => { + expect(processBackgroundPosition('left 25% bottom 75%')).toEqual([ + {bottom: '75%', left: '25%'}, + ]); + }); + + it('should parse right percentage bottom percentage', () => { + expect(processBackgroundPosition('right 25% bottom 75%')).toEqual([ + {bottom: '75%', right: '25%'}, + ]); + }); + + // Test multiple background positions (comma-separated) + it('should parse multiple background positions', () => { + expect(processBackgroundPosition('left top, right bottom')).toEqual([ + {top: '0%', left: '0%'}, + {top: '100%', left: '100%'}, + ]); + }); + + it('should parse multiple background positions with values', () => { + expect( + processBackgroundPosition('left 25% top 75%, center center'), + ).toEqual([ + {top: '75%', left: '25%'}, + {top: '50%', left: '50%'}, + ]); + }); + + it('should parse multiple background positions with newlines', () => { + expect(processBackgroundPosition('left top,\nright bottom')).toEqual([ + {top: '0%', left: '0%'}, + {top: '100%', left: '100%'}, + ]); + }); + + it('should parse multiple background positions with extra whitespace', () => { + expect(processBackgroundPosition(' left top , right bottom ')).toEqual([ + {top: '0%', left: '0%'}, + {top: '100%', left: '100%'}, + ]); + }); + + // Test case sensitivity + it('should handle mixed case keywords', () => { + expect(processBackgroundPosition('LEFT')).toEqual([ + {top: '50%', left: '0%'}, + ]); + }); + + it('should handle mixed case keywords with spaces', () => { + expect(processBackgroundPosition(' Left ')).toEqual([ + {top: '50%', left: '0%'}, + ]); + }); + + it('should handle mixed case in two values', () => { + expect(processBackgroundPosition('LEFT TOP')).toEqual([ + {top: '0%', left: '0%'}, + ]); + }); + + it('should handle mixed case in unit values', () => { + expect(processBackgroundPosition('LEFT 100PX')).toEqual([ + {top: 100, left: '0%'}, + ]); + }); + + // Test whitespace handling + it('should handle extra spaces between values', () => { + expect(processBackgroundPosition('left top')).toEqual([ + {top: '0%', left: '0%'}, + ]); + }); + + it('should handle tabs and newlines', () => { + expect(processBackgroundPosition('left\ttop\n')).toEqual([ + {top: '0%', left: '0%'}, + ]); + }); + + // Test edge cases and invalid inputs + it('should handle invalid single value', () => { + expect(processBackgroundPosition('invalid')).toEqual([]); + }); + + it('should handle invalid two values', () => { + expect(processBackgroundPosition('left invalid')).toEqual([]); + }); + + it('should handle invalid first value', () => { + expect(processBackgroundPosition('invalid top')).toEqual([]); + }); + + it('should handle too many values', () => { + expect(processBackgroundPosition('left top center')).toEqual([]); + }); + + it('should handle empty values in comma-separated list', () => { + expect(processBackgroundPosition('left top,')).toEqual([]); + }); + + it('should handle only comma', () => { + expect(processBackgroundPosition(',')).toEqual([]); + }); + + it('should handle multiple commas', () => { + expect(processBackgroundPosition('left top,,right bottom')).toEqual([]); + }); + + it('should handle invalid mixed values in multiple backgrounds', () => { + expect(processBackgroundPosition('left top, invalid')).toEqual([]); + }); + + it('should handle negative px values', () => { + expect(processBackgroundPosition('-100px')).toEqual([ + {top: '50%', left: -100}, + ]); + }); + + it('should handle negative percentage values', () => { + expect(processBackgroundPosition('-25%')).toEqual([ + {top: '50%', left: '-25%'}, + ]); + }); + + it('should handle zero px values', () => { + expect(processBackgroundPosition('0px')).toEqual([{top: '50%', left: 0}]); + }); + + it('should handle zero percentage values', () => { + expect(processBackgroundPosition('0%')).toEqual([{top: '50%', left: '0%'}]); + }); + + it('should handle invalid units', () => { + expect(processBackgroundPosition('100em')).toEqual([]); + }); + + it('should handle invalid percentage format', () => { + expect(processBackgroundPosition('50')).toEqual([]); + }); + + it('should handle partial invalid values', () => { + expect(processBackgroundPosition('left-')).toEqual([]); + }); + + it('should handle partial invalid values with valid ones', () => { + expect(processBackgroundPosition('left-, top')).toEqual([]); + }); + + // Test complex scenarios + it('should handle complex multiple background scenario', () => { + expect( + processBackgroundPosition( + 'left top, center center, right 25% bottom 75%', + ), + ).toEqual([ + {top: '0%', left: '0%'}, + {top: '50%', left: '50%'}, + {bottom: '75%', right: '25%'}, + ]); + }); + + it('should handle all valid single keyword combinations', () => { + expect( + processBackgroundPosition('left, center, right, top, bottom'), + ).toEqual([ + {top: '50%', left: '0%'}, + {top: '50%', left: '50%'}, + {top: '50%', left: '100%'}, + {top: '0%', left: '50%'}, + {top: '100%', left: '50%'}, + ]); + }); + + // Test specific edge cases + it('should handle single space character', () => { + expect(processBackgroundPosition(' ')).toEqual([]); + }); + + it('should handle multiple space characters', () => { + expect(processBackgroundPosition(' ')).toEqual([]); + }); + + it('should handle comma with spaces', () => { + expect(processBackgroundPosition(' , ')).toEqual([]); + }); + + it('should handle invalid three value syntax', () => { + expect(processBackgroundPosition('left top invalid')).toEqual([]); + }); + + it('should handle invalid four value syntax', () => { + expect(processBackgroundPosition('left 25% top invalid')).toEqual([]); + }); + + it('should handle 0 as a valid position', () => { + expect(processBackgroundPosition('0 0')).toEqual([{top: 0, left: 0}]); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js new file mode 100644 index 000000000000..586bc433a228 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js @@ -0,0 +1,243 @@ +/** + * 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 + */ + +'use strict'; + +import processBackgroundRepeat from '../processBackgroundRepeat'; + +describe('processBackgroundRepeat', () => { + // Test null/undefined input + it('should handle null input', () => { + expect(processBackgroundRepeat(null)).toEqual([]); + }); + + it('should handle undefined input', () => { + expect(processBackgroundRepeat(undefined)).toEqual([]); + }); + + // Test empty string input + it('should handle empty string', () => { + expect(processBackgroundRepeat('')).toEqual([]); + }); + + // Test array input + it('should handle array input', () => { + expect(processBackgroundRepeat([{x: 'repeat', y: 'space'}])).toEqual([ + {x: 'repeat', y: 'space'}, + ]); + }); + + // Test single keyword values + it('should parse repeat', () => { + expect(processBackgroundRepeat('repeat')).toEqual([ + {x: 'repeat', y: 'repeat'}, + ]); + }); + + it('should parse no-repeat', () => { + expect(processBackgroundRepeat('no-repeat')).toEqual([ + {x: 'no-repeat', y: 'no-repeat'}, + ]); + }); + + it('should parse space', () => { + expect(processBackgroundRepeat('space')).toEqual([ + {x: 'space', y: 'space'}, + ]); + }); + + it('should parse round', () => { + expect(processBackgroundRepeat('round')).toEqual([ + {x: 'round', y: 'round'}, + ]); + }); + + it('should parse repeat-x', () => { + expect(processBackgroundRepeat('repeat-x')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + ]); + }); + + it('should parse repeat-y', () => { + expect(processBackgroundRepeat('repeat-y')).toEqual([ + {x: 'no-repeat', y: 'repeat'}, + ]); + }); + + // Test two-value syntax + it('should parse two repeat values', () => { + expect(processBackgroundRepeat('repeat repeat')).toEqual([ + {x: 'repeat', y: 'repeat'}, + ]); + }); + + it('should parse repeat no-repeat', () => { + expect(processBackgroundRepeat('repeat no-repeat')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + ]); + }); + + it('should parse round space', () => { + expect(processBackgroundRepeat('round space')).toEqual([ + {x: 'round', y: 'space'}, + ]); + }); + + // Test multiple background repeats (comma-separated) + it('should parse multiple background repeats', () => { + expect(processBackgroundRepeat('repeat, no-repeat')).toEqual([ + {x: 'repeat', y: 'repeat'}, + {x: 'no-repeat', y: 'no-repeat'}, + ]); + }); + + it('should parse multiple background repeats with two values', () => { + expect(processBackgroundRepeat('repeat no-repeat, space round')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + {x: 'space', y: 'round'}, + ]); + }); + + it('should parse multiple background repeats with mixed syntax', () => { + expect(processBackgroundRepeat('repeat-x, space round, no-repeat')).toEqual( + [ + {x: 'repeat', y: 'no-repeat'}, + {x: 'space', y: 'round'}, + {x: 'no-repeat', y: 'no-repeat'}, + ], + ); + }); + + it('should parse multiple background repeats with newlines', () => { + expect(processBackgroundRepeat('repeat,\nno-repeat')).toEqual([ + {x: 'repeat', y: 'repeat'}, + {x: 'no-repeat', y: 'no-repeat'}, + ]); + }); + + it('should parse multiple background repeats with extra whitespace', () => { + expect(processBackgroundRepeat(' repeat , no-repeat ')).toEqual([ + {x: 'repeat', y: 'repeat'}, + {x: 'no-repeat', y: 'no-repeat'}, + ]); + }); + + // Test case sensitivity + it('should handle mixed case keywords', () => { + expect(processBackgroundRepeat('REPEAT')).toEqual([ + {x: 'repeat', y: 'repeat'}, + ]); + }); + + it('should handle mixed case keywords with spaces', () => { + expect(processBackgroundRepeat(' Repeat ')).toEqual([ + {x: 'repeat', y: 'repeat'}, + ]); + }); + + it('should handle mixed case in two values', () => { + expect(processBackgroundRepeat('REPEAT NO-REPEAT')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + ]); + }); + + it('should handle mixed case in repeat-x', () => { + expect(processBackgroundRepeat('REPEAT-X')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + ]); + }); + + // Test whitespace handling + it('should handle extra spaces between values', () => { + expect(processBackgroundRepeat('repeat no-repeat')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + ]); + }); + + it('should handle tabs and newlines', () => { + expect(processBackgroundRepeat('repeat\tno-repeat\n')).toEqual([ + {x: 'repeat', y: 'no-repeat'}, + ]); + }); + + // Test edge cases and invalid inputs + it('should handle invalid single value', () => { + expect(processBackgroundRepeat('invalid')).toEqual([]); + }); + + it('should handle invalid two values', () => { + expect(processBackgroundRepeat('repeat invalid')).toEqual([]); + }); + + it('should handle too many values', () => { + expect(processBackgroundRepeat('repeat no-repeat space')).toEqual([]); + }); + + it('should handle empty values in comma-separated list', () => { + expect(processBackgroundRepeat('repeat,')).toEqual([]); + }); + + it('should handle only comma', () => { + expect(processBackgroundRepeat(',')).toEqual([]); + }); + + it('should handle multiple commas', () => { + expect(processBackgroundRepeat('repeat,,no-repeat')).toEqual([]); + }); + + it('should handle invalid mixed values in multiple backgrounds', () => { + expect(processBackgroundRepeat('repeat, invalid')).toEqual([]); + }); + + it('should handle invalid mixed values in multiple backgrounds with two values', () => { + expect(processBackgroundRepeat('repeat no-repeat, invalid round')).toEqual( + [], + ); + }); + + // Test complex scenarios + it('should handle all valid two value combinations', () => { + expect( + processBackgroundRepeat( + 'repeat repeat, repeat no-repeat, repeat space, repeat round, no-repeat repeat, no-repeat no-repeat, no-repeat space, no-repeat round, space repeat, space no-repeat, space space, space round, round repeat, round no-repeat, round space, round round', + ), + ).toEqual([ + {x: 'repeat', y: 'repeat'}, + {x: 'repeat', y: 'no-repeat'}, + {x: 'repeat', y: 'space'}, + {x: 'repeat', y: 'round'}, + {x: 'no-repeat', y: 'repeat'}, + {x: 'no-repeat', y: 'no-repeat'}, + {x: 'no-repeat', y: 'space'}, + {x: 'no-repeat', y: 'round'}, + {x: 'space', y: 'repeat'}, + {x: 'space', y: 'no-repeat'}, + {x: 'space', y: 'space'}, + {x: 'space', y: 'round'}, + {x: 'round', y: 'repeat'}, + {x: 'round', y: 'no-repeat'}, + {x: 'round', y: 'space'}, + {x: 'round', y: 'round'}, + ]); + }); + + // Test specific edge cases + it('should handle single space character', () => { + expect(processBackgroundRepeat(' ')).toEqual([]); + }); + + it('should handle multiple space characters', () => { + expect(processBackgroundRepeat(' ')).toEqual([]); + }); + + it('should handle comma with spaces', () => { + expect(processBackgroundRepeat(' , ')).toEqual([]); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js new file mode 100644 index 000000000000..5733e7529b01 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js @@ -0,0 +1,231 @@ +/** + * 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 + */ + +'use strict'; + +import processBackgroundSize from '../processBackgroundSize'; + +describe('processBackgroundSize', () => { + // Test null/undefined input + it('should handle null input', () => { + expect(processBackgroundSize(null)).toEqual([]); + }); + + it('should handle undefined input', () => { + expect(processBackgroundSize(undefined)).toEqual([]); + }); + + // Test empty string input + it('should handle empty string', () => { + expect(processBackgroundSize('')).toEqual([]); + }); + + // Test array input + it('should handle array input', () => { + expect(processBackgroundSize([{x: 100, y: 200}])).toEqual([ + {x: 100, y: 200}, + ]); + }); + + it('should handle array with cover', () => { + expect(processBackgroundSize(['cover'])).toEqual(['cover']); + }); + + it('should handle array with contain', () => { + expect(processBackgroundSize(['contain'])).toEqual(['contain']); + }); + + it('should handle array with mixed values', () => { + expect(processBackgroundSize([{x: 100, y: 'auto'}, 'cover'])).toEqual([ + {x: 100, y: 'auto'}, + 'cover', + ]); + }); + + // Test string input - single values + it('should parse cover', () => { + expect(processBackgroundSize('cover')).toEqual(['cover']); + }); + + it('should parse contain', () => { + expect(processBackgroundSize('contain')).toEqual(['contain']); + }); + + it('should parse single length value', () => { + expect(processBackgroundSize('100px')).toEqual([{x: 100, y: 'auto'}]); + }); + + it('should parse single percentage value', () => { + expect(processBackgroundSize('50%')).toEqual([{x: '50%', y: 'auto'}]); + }); + + it('should parse single auto value', () => { + expect(processBackgroundSize('auto')).toEqual([{x: 'auto', y: 'auto'}]); + }); + + // Test string input - two values + it('should parse two length values', () => { + expect(processBackgroundSize('100px 200px')).toEqual([{x: 100, y: 200}]); + }); + + it('should parse two percentage values', () => { + expect(processBackgroundSize('50% 75%')).toEqual([{x: '50%', y: '75%'}]); + }); + + it('should parse length and percentage', () => { + expect(processBackgroundSize('100px 50%')).toEqual([{x: 100, y: '50%'}]); + }); + + it('should parse percentage and length', () => { + expect(processBackgroundSize('50% 200px')).toEqual([{x: '50%', y: 200}]); + }); + + it('should parse length and auto', () => { + expect(processBackgroundSize('100px auto')).toEqual([{x: 100, y: 'auto'}]); + }); + + it('should parse auto and length', () => { + expect(processBackgroundSize('auto 200px')).toEqual([{x: 'auto', y: 200}]); + }); + + it('should parse percentage and auto', () => { + expect(processBackgroundSize('50% auto')).toEqual([{x: '50%', y: 'auto'}]); + }); + + it('should parse auto and percentage', () => { + expect(processBackgroundSize('auto 75%')).toEqual([{x: 'auto', y: '75%'}]); + }); + + it('should parse two auto values', () => { + expect(processBackgroundSize('auto auto')).toEqual([ + {x: 'auto', y: 'auto'}, + ]); + }); + + // Test multiple background sizes (comma-separated) + it('should parse multiple background sizes', () => { + expect(processBackgroundSize('100px 200px, cover')).toEqual([ + {x: 100, y: 200}, + 'cover', + ]); + }); + + it('should parse multiple background sizes with spaces', () => { + expect(processBackgroundSize('100px 200px, 50% 75%')).toEqual([ + {x: 100, y: 200}, + {x: '50%', y: '75%'}, + ]); + }); + + it('should parse multiple background sizes with newlines', () => { + expect(processBackgroundSize('100px 200px,\nCover')).toEqual([ + {x: 100, y: 200}, + 'cover', + ]); + }); + + it('should parse multiple background sizes with extra whitespace', () => { + expect(processBackgroundSize(' 100px 200px , cover ')).toEqual([ + {x: 100, y: 200}, + 'cover', + ]); + }); + + // Test edge cases and invalid inputs + it('should handle negative length values', () => { + expect(processBackgroundSize('-100px')).toEqual([]); + }); + + it('should handle negative percentage values', () => { + expect(processBackgroundSize('-50%')).toEqual([]); + }); + + it('should handle zero length values', () => { + expect(processBackgroundSize('0px')).toEqual([{x: 0, y: 'auto'}]); + }); + + it('should handle zero percentage values', () => { + expect(processBackgroundSize('0%')).toEqual([{x: '0%', y: 'auto'}]); + }); + + it('should handle decimal length values', () => { + expect(processBackgroundSize('100.5px')).toEqual([{x: 100.5, y: 'auto'}]); + }); + + it('should handle decimal percentage values', () => { + expect(processBackgroundSize('50.5%')).toEqual([{x: '50.5%', y: 'auto'}]); + }); + + it('should handle invalid units', () => { + expect(processBackgroundSize('100em')).toEqual([]); + }); + + it('should handle invalid percentage format', () => { + expect(processBackgroundSize('50')).toEqual([]); + }); + + it('should handle mixed case keywords', () => { + expect(processBackgroundSize('COVER')).toEqual(['cover']); + }); + + it('should handle mixed case keywords with spaces', () => { + expect(processBackgroundSize(' Cover ')).toEqual(['cover']); + }); + + it('should handle invalid mixed values', () => { + expect(processBackgroundSize('100px invalid')).toEqual([]); + }); + + it('should handle invalid mixed values in multiple backgrounds', () => { + expect(processBackgroundSize('100px 200px, invalid')).toEqual([]); + }); + + it('should handle too many values', () => { + expect(processBackgroundSize('100px 200px 300px')).toEqual([]); + }); + + it('should handle empty values in comma-separated list', () => { + expect(processBackgroundSize('100px 200px,')).toEqual([]); + }); + + it('should handle only comma', () => { + expect(processBackgroundSize(',')).toEqual([]); + }); + + it('should handle multiple commas', () => { + expect(processBackgroundSize('100px 200px,,cover')).toEqual([]); + }); + + // Test whitespace handling + it('should handle extra spaces between values', () => { + expect(processBackgroundSize('100px 200px')).toEqual([{x: 100, y: 200}]); + }); + + it('should handle tabs and newlines', () => { + expect(processBackgroundSize('100px\t200px\n')).toEqual([{x: 100, y: 200}]); + }); + + // Test complex scenarios + it('should handle complex multiple background scenario', () => { + expect( + processBackgroundSize('100px 200px, 50% auto, cover, contain'), + ).toEqual([{x: 100, y: 200}, {x: '50%', y: 'auto'}, 'cover', 'contain']); + }); + + it('should handle all valid single value combinations', () => { + expect(processBackgroundSize('100px, 50%, auto, cover, contain')).toEqual([ + {x: 100, y: 'auto'}, + {x: '50%', y: 'auto'}, + {x: 'auto', y: 'auto'}, + 'cover', + 'contain', + ]); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js b/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js new file mode 100644 index 000000000000..3d8dadaa40a1 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js @@ -0,0 +1,284 @@ +/** + * 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 + */ + +'use strict'; +import type {BackgroundPositionValue} from './StyleSheetTypes'; + +export default function processBackgroundPosition( + backgroundPosition: ?($ReadOnlyArray | string), +): $ReadOnlyArray { + let result: $ReadOnlyArray = []; + + if (backgroundPosition == null) { + return []; + } + if (typeof backgroundPosition === 'string') { + result = parseBackgroundPositionCSSString( + backgroundPosition.replace(/\n/g, ' '), + ); + } else if (Array.isArray(backgroundPosition)) { + result = backgroundPosition; + } + + return result; +} + +// https://www.w3.org/TR/css-backgrounds-3/#typedef-bg-position +const parseBackgroundPositionCSSString = ( + backgroundPosition: string, +): $ReadOnlyArray => { + const result: Array = []; + const positions = backgroundPosition.split(',').map(s => s.trim()); + + for (const position of positions) { + let top: string | number; + let left: string | number; + let right: string | number; + let bottom: string | number; + const parts = position.split(/\s+/).filter(p => p.length > 0); + // 1. Single value syntax [ left | center | right | top | bottom | ] + if (parts.length === 1) { + const t1 = parts[0]; + if (t1 == null) { + return []; + } + const token1 = t1.toLowerCase().trim(); + if (token1 === 'left') { + left = '0%'; + top = '50%'; + } else if (token1 === 'center') { + left = '50%'; + top = '50%'; + } else if (token1 === 'right') { + left = '100%'; + top = '50%'; + } else if (token1 === 'top') { + left = '50%'; + top = '0%'; + } else if (token1 === 'bottom') { + left = '50%'; + top = '100%'; + } else if (isValidPosition(token1)) { + const value = getPositionFromCSSValue(token1); + if (value == null) { + return []; + } + left = value; + top = '50%'; + } + } + + // 2. Two value syntax [ left | center | right | ] [ top | center | bottom | ] + if (parts.length === 2) { + const t1 = parts[0]; + const t2 = parts[1]; + if (t1 == null || t2 == null) { + return []; + } + const token1 = t1.toLowerCase().trim(); + if (token1 === 'left') { + left = '0%'; + } else if (token1 === 'center') { + left = '50%'; + } else if (token1 === 'right') { + left = '100%'; + } else if (token1 === 'top') { + top = '0%'; + } else if (token1 === 'bottom') { + top = '100%'; + } else if (isValidPosition(token1)) { + const value = getPositionFromCSSValue(token1); + if (value == null) { + return []; + } + left = value; + } + + const token2 = t2.toLowerCase().trim(); + if (token2 === 'top') { + top = '0%'; + } else if (token2 === 'center') { + top = '50%'; + } else if (token2 === 'bottom') { + top = '100%'; + } else if (token2 === 'left') { + left = '0%'; + } else if (token2 === 'right') { + left = '100%'; + } else if (isValidPosition(token2)) { + const value = getPositionFromCSSValue(token2); + if (value == null) { + return []; + } + top = value; + } + } + + // 3. Three value syntax [ center | [ left | right ] ? ] && [ center | [ top | bottom ] ? ] + if (parts.length === 3) { + const t1 = parts[0]; + const t2 = parts[1]; + const t3 = parts[2]; + if (t1 == null || t2 == null || t3 == null) { + return []; + } + const token1 = t1.toLowerCase().trim(); + const token2 = t2.toLowerCase().trim(); + const token3 = t3.toLowerCase().trim(); + // e.g. center top 40% + if (token1 === 'center') { + left = '50%'; + const value = getPositionFromCSSValue(token3); + if (value == null) { + return []; + } + if (token2 === 'top') { + top = value; + } else if (token2 === 'bottom') { + bottom = value; + } else { + return []; + } + } + // e.g. left 40% center + else if (token3 === 'center') { + top = '50%'; + const value = getPositionFromCSSValue(token2); + if (value == null) { + return []; + } + if (token1 === 'left') { + left = value; + } else if (token1 === 'right') { + right = value; + } else { + return []; + } + } + // e.g. left 40% top, left top 10% + else { + const tokens = [token1, token2, token3]; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (isValidPosition(token)) { + const value = getPositionFromCSSValue(token); + if (value == null) { + return []; + } + const previousToken = tokens[i - 1]; + if (previousToken === 'left') { + left = value; + } else if (previousToken === 'right') { + right = value; + } else if (previousToken === 'top') { + top = value; + } else if (previousToken === 'bottom') { + bottom = value; + } + } else { + if (token === 'left') { + left = '0%'; + } else if (token === 'right') { + right = '0%'; + } else if (token === 'top') { + top = '0%'; + } else if (token === 'bottom') { + bottom = '0%'; + } else { + return []; + } + } + } + } + } + + // 4. Four value syntax [ center | [ left | right ] ? ] && [ center | [ top | bottom ] ? ] + if (parts.length === 4) { + const t1 = parts.shift(); + const t2 = parts.shift(); + const t3 = parts.shift(); + const t4 = parts.shift(); + if (t1 == null || t2 == null || t3 == null || t4 == null) { + return []; + } + const token1 = t1.toLowerCase().trim(); + const token2 = t2.toLowerCase().trim(); + const token3 = t3.toLowerCase().trim(); + const token4 = t4.toLowerCase().trim(); + const keyword1 = token1; + const value1 = getPositionFromCSSValue(token2); + const keyword2 = token3; + const value2 = getPositionFromCSSValue(token4); + if (value1 == null || value2 == null) { + return []; + } + if (keyword1 === 'left') { + left = value1; + } else if (keyword1 === 'right') { + right = value1; + } + + if (keyword2 === 'top') { + top = value2; + } else if (keyword2 === 'bottom') { + bottom = value2; + } + } + + if (top != null && left != null) { + result.push({ + top, + left, + }); + } else if (bottom != null && right != null) { + result.push({ + bottom, + right, + }); + } else if (top != null && right != null) { + result.push({ + top, + right, + }); + } else if (bottom != null && left != null) { + result.push({ + bottom, + left, + }); + } else { + return []; + } + } + + return result; +}; + +function getPositionFromCSSValue(position: string) { + if (position.endsWith('px')) { + return parseFloat(position); + } + + if (position.endsWith('%')) { + return position; + } + + // CSS length allows 0 as a valid value + if (position === '0') { + return 0; + } +} + +function isValidPosition(position: string) { + if (position.endsWith('px') || position.endsWith('%') || position === '0') { + return true; + } + + return false; +} diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js b/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js new file mode 100644 index 000000000000..8e53e0ccfccc --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js @@ -0,0 +1,104 @@ +/** + * 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 + */ + +'use strict'; + +import type { + BackgroundRepeatKeyword, + BackgroundRepeatValue, +} from './StyleSheetTypes'; + +function isBackgroundRepeatKeyword( + value: string, +): value is BackgroundRepeatKeyword { + return ( + value === 'repeat' || + value === 'space' || + value === 'round' || + value === 'no-repeat' + ); +} + +export default function processBackgroundRepeat( + backgroundRepeat: ?($ReadOnlyArray | string), +): $ReadOnlyArray { + let result: $ReadOnlyArray = []; + if (backgroundRepeat == null) { + return []; + } + + if (Array.isArray(backgroundRepeat)) { + return backgroundRepeat; + } + + if (typeof backgroundRepeat === 'string') { + result = parseBackgroundRepeatCSSString( + backgroundRepeat.replace(/\n/g, ' '), + ); + } + + return result; +} + +// https://www.w3.org/TR/css-backgrounds-3/#typedef-repeat-style +function parseBackgroundRepeatCSSString( + backgroundRepeat: string, +): $ReadOnlyArray { + const result: Array = []; + const bgRepeatArray = backgroundRepeat.split(',').map(s => s.trim()); + + for (const bgRepeat of bgRepeatArray) { + if (bgRepeat.length === 0) { + return []; + } + + const parts = bgRepeat.split(/\s+/).filter(p => p.length > 0); + if (parts.length === 1) { + const part1 = parts[0]; + if (part1 == null) { + return []; + } + const token1 = part1.toLowerCase(); + if (token1 === 'repeat-x') { + result.push({x: 'repeat', y: 'no-repeat'}); + } else if (token1 === 'repeat-y') { + result.push({x: 'no-repeat', y: 'repeat'}); + } else if (token1 === 'repeat') { + result.push({x: 'repeat', y: 'repeat'}); + } else if (token1 === 'space') { + result.push({x: 'space', y: 'space'}); + } else if (token1 === 'round') { + result.push({x: 'round', y: 'round'}); + } else if (token1 === 'no-repeat') { + result.push({x: 'no-repeat', y: 'no-repeat'}); + } else { + return []; + } + } else if (parts.length === 2) { + const part1 = parts[0]; + const part2 = parts[1]; + if (part1 == null || part2 == null) { + return []; + } + const token1 = part1.toLowerCase(); + const token2 = part2.toLowerCase(); + if ( + isBackgroundRepeatKeyword(token1) && + isBackgroundRepeatKeyword(token2) + ) { + result.push({x: token1, y: token2}); + } else { + return []; + } + } + } + + return result; +} diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundSize.js b/packages/react-native/Libraries/StyleSheet/processBackgroundSize.js new file mode 100644 index 000000000000..0c494776d3a8 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundSize.js @@ -0,0 +1,104 @@ +/** + * 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 + */ + +'use strict'; + +import type {BackgroundSizeValue} from './StyleSheetTypes'; + +export default function processBackgroundSize( + backgroundSize: ?($ReadOnlyArray | string), +): $ReadOnlyArray { + let result: $ReadOnlyArray = []; + + if (backgroundSize == null) { + // If the size is invalid, return an empty array and do not apply any background size. Same as web. + return []; + } + + if (typeof backgroundSize === 'string') { + result = parseBackgroundSizeCSSString(backgroundSize.replace(/\n/g, ' ')); + } else if (Array.isArray(backgroundSize)) { + result = backgroundSize; + } + + return result; +} +// https://www.w3.org/TR/css-backgrounds-3/#typedef-bg-size +// = [ | auto ]{1,2} | cover | contain +function parseBackgroundSizeCSSString( + backgroundSize: string, +): $ReadOnlyArray { + const result: Array = []; + const sizes = backgroundSize.split(',').map(s => s.trim()); + + for (const size of sizes) { + if (size.length === 0) { + return []; + } + + const parts = size.split(/\s+/).filter(p => p.length > 0); + + if (parts.length === 2) { + const x = getValidLengthPercentageSizeOrNull(parts[0].toLowerCase()); + const y = getValidLengthPercentageSizeOrNull(parts[1].toLowerCase()); + if (x != null && y != null) { + result.push({ + x, + y, + }); + } else { + return []; + } + } else if (parts.length === 1) { + const part = parts[0].toLowerCase(); + if (part === 'cover' || part === 'contain') { + result.push(part); + } else { + const x = getValidLengthPercentageSizeOrNull(parts[0].toLowerCase()); + if (x != null) { + result.push({ + x, + y: 'auto', + }); + } else { + return []; + } + } + } + } + + return result; +} + +// [ | auto ] +function getValidLengthPercentageSizeOrNull(size: ?string) { + if (size == null) { + return null; + } + + if (size.endsWith('px')) { + const num = parseFloat(size); + if (!Number.isNaN(num) && num >= 0) { + return num; + } + } + + if (size.endsWith('%')) { + if (parseFloat(size) >= 0) { + return size; + } + } + + if (size === 'auto') { + return size; + } + + return null; +} diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js new file mode 100644 index 000000000000..a3588a3d450b --- /dev/null +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -0,0 +1,482 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; + +import {StyleSheet, Text, View} from 'react-native'; + +function BackgroundImageBox({ + style, + children, + testID, +}: { + style?: ViewStyleProp, + children?: React.Node, + testID: string, +}) { + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + box: { + width: 200, + height: 100, + justifyContent: 'center', + alignItems: 'center', + marginVertical: 10, + }, + text: { + color: 'black', + fontWeight: 'bold', + textShadowColor: 'black', + textShadowRadius: 2, + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-evenly', + }, + col: { + alignItems: 'center', + }, +}); + +exports.title = 'BackgroundImage'; +exports.category = 'UI'; +exports.description = 'Examples of background gradients applied to views.'; +exports.examples = [ + { + title: 'Basic Linear Gradient', + description: 'A simple linear gradient from top to bottom.', + name: 'basic', + render(): React.Node { + return ( + + Basic + + ); + }, + }, + { + title: 'Linear Gradient with Angle', + name: 'angle', + render(): React.Node { + return ( + + + 45deg + + + + 90deg + + + + 180deg + + + + ); + }, + }, + { + title: 'Linear Gradient with Multiple Colors', + name: 'multiple-colors', + render(): React.Node { + return ( + + + 3 colors + + + + 4 colors + + + + Rainbow + + + + ); + }, + }, + { + title: 'Radial Gradient', + name: 'radial', + render(): React.Node { + return ( + + + Circle + + + + Ellipse + + + + ); + }, + }, + { + title: 'Gradient with Background Repeat', + name: 'repeat', + render(): React.Node { + return ( + + + + repeat + + + + space + + + + + + round + + + + no-repeat + + + + + ); + }, + }, + { + title: 'Gradient with Background Position', + name: 'position', + render(): React.Node { + return ( + + + center + + + + 25% 75% + + + + right bottom + + + + ); + }, + }, + { + title: 'Multiple Gradients', + name: 'multiple', + render(): React.Node { + return ( + + ); + }, + }, + { + title: 'Gradient with Borders', + name: 'borders', + render(): React.Node { + return ( + + + borderRadius + + + + borderWidth + borderColor + + + + non uniform borderRadius + + + + non uniform borderWidth + + + + ); + }, + }, + { + title: 'Complex Striped Pattern', + name: 'striped-pattern', + render(): React.Node { + return ( + + Striped + + ); + }, + }, + { + title: 'Object syntax', + name: 'object-syntax', + render(): React.Node { + return ( + + ); + }, + }, +] as Array; diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index 3d982eb5d327..b3555cd6667f 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -329,6 +329,11 @@ const APIs: Array = ([ category: 'UI', module: require('../examples/RadialGradient/RadialGradientExample'), }, + { + key: 'BackgroundImageExample', + category: 'UI', + module: require('../examples/BackgroundImage/BackgroundImageExample'), + }, { key: 'MixBlendModeExample', category: 'UI', diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index dbf2e5b91c22..759881e2cc9a 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -309,6 +309,11 @@ const APIs: Array = ([ category: 'UI', module: require('../examples/RadialGradient/RadialGradientExample'), }, + { + key: 'BackgroundImageExample', + category: 'UI', + module: require('../examples/BackgroundImage/BackgroundImageExample'), + }, { key: 'MixBlendModeExample', module: require('../examples/MixBlendMode/MixBlendModeExample'),