From 56ed08897c0c6eee54ca8e1ba6899a064f1e68d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Wed, 25 Jun 2025 20:13:08 +0530 Subject: [PATCH 1/7] js changes background image example example js nits beautiful multiple gradient example --- .../View/ReactNativeStyleAttributes.js | 20 +- .../NativeComponent/BaseViewConfig.android.js | 9 + .../getNativeComponentAttributes.js | 12 + .../Libraries/StyleSheet/StyleSheetTypes.js | 44 ++ .../processBackgroundPosition-test.js | 478 ++++++++++++++++++ .../__tests__/processBackgroundRepeat-test.js | 243 +++++++++ .../__tests__/processBackgroundSize-test.js | 231 +++++++++ .../StyleSheet/processBackgroundPosition.js | 284 +++++++++++ .../StyleSheet/processBackgroundRepeat.js | 89 ++++ .../StyleSheet/processBackgroundSize.js | 104 ++++ .../BackgroundImage/BackgroundImageExample.js | 435 ++++++++++++++++ .../js/utils/RNTesterList.android.js | 5 + .../rn-tester/js/utils/RNTesterList.ios.js | 5 + 13 files changed, 1958 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js create mode 100644 packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js create mode 100644 packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js create mode 100644 packages/react-native/Libraries/StyleSheet/processBackgroundSize.js create mode 100644 packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 709c71845a58..be5a743023f7 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -14,6 +14,9 @@ import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/Reac import NativeReactNativeFeatureFlags from '../../../src/private/featureflags/specs/NativeReactNativeFeatureFlags'; 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'; @@ -153,10 +156,25 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { }, /** - * Linear Gradient + * BackgroundImage */ experimental_backgroundImage: {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 bdcd439ab472..845791068971 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -183,6 +183,15 @@ const validAttributesForNonEventProps = { experimental_backgroundImage: { 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: NativeReactNativeFeatureFlags != null && ReactNativeFeatureFlags.enableNativeCSSParsing() diff --git a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js index dcfb65b6661d..6f03a4974afb 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 processBackgroundSize = + require('../StyleSheet/processBackgroundSize').default; +const processBackgroundPosition = + require('../StyleSheet/processBackgroundPosition').default; +const processBackgroundRepeat = + require('../StyleSheet/processBackgroundRepeat').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 'BackgroundSize': + return processBackgroundSize; + case 'BackgroundPosition': + return processBackgroundPosition; + case 'BackgroundRepeat': + return processBackgroundRepeat; 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..01f6e9adcdd7 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js @@ -0,0 +1,89 @@ +/** + * 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 {BackgroundRepeatValue} from './StyleSheetTypes'; + +const validValues = ['repeat', 'space', 'round', 'no-repeat'] as const; + +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 (validValues.includes(token1) && validValues.includes(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..74a7fc291ca3 --- /dev/null +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -0,0 +1,435 @@ +/** + * 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 RNTesterText from '../../components/RNTesterText'; +import React from 'react'; +import {StyleSheet, View, ScrollView} from 'react-native'; + +function BackgroundImageBox({style, children, testID}) { + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + box: { + width: 200, + height: 100, + justifyContent: 'center', + alignItems: 'center', + marginVertical: 10, + }, + text: { + color: 'white', + 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: 'Object syntax', + name: 'object-syntax', + render(): React.Node { + return ( + + ); + }, + }, +]; diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index 1977135a57ba..6c5b26ec0b19 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -334,6 +334,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 fa5968d9697b..752c7cbeed0f 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -314,6 +314,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'), From ebbd2763ecc4a97c94b5dae9dc2a33e11a69e425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 26 Jun 2025 12:54:30 +0530 Subject: [PATCH 2/7] fix: js lint errors --- .../getNativeComponentAttributes.js | 8 +-- .../BackgroundImage/BackgroundImageExample.js | 49 +++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js index 6f03a4974afb..58594cc23207 100644 --- a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js +++ b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js @@ -17,12 +17,12 @@ const ReactNativeStyleAttributes = const resolveAssetSource = require('../Image/resolveAssetSource').default; const processBackgroundImage = require('../StyleSheet/processBackgroundImage').default; -const processBackgroundSize = - require('../StyleSheet/processBackgroundSize').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; @@ -209,12 +209,12 @@ function getProcessorForType(typeName: string): ?(nextProp: any) => any { return processFilter; case 'BackgroundImage': return processBackgroundImage; - case 'BackgroundSize': - return processBackgroundSize; case 'BackgroundPosition': return processBackgroundPosition; case 'BackgroundRepeat': return processBackgroundRepeat; + case 'BackgroundSize': + return processBackgroundSize; case 'ImageSource': return resolveAssetSource; case 'BoxShadow': diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index 74a7fc291ca3..b1b32a8204b5 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -10,10 +10,7 @@ 'use strict'; -import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; -import RNTesterText from '../../components/RNTesterText'; -import React from 'react'; -import {StyleSheet, View, ScrollView} from 'react-native'; +import {StyleSheet, Text, View} from 'react-native'; function BackgroundImageBox({style, children, testID}) { return ( @@ -62,7 +59,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(#e66465, #9198e5)', }} testID="background-image-basic"> - Basic + Basic ); }, @@ -74,7 +71,7 @@ exports.examples = [ return ( - 45deg + 45deg - 90deg + 90deg - 180deg + 180deg - 3 colors + 3 colors - 4 colors + 4 colors - Rainbow + Rainbow - Circle + Circle - Ellipse + Ellipse + - repeat + repeat - space + space - round + round - no-repeat + no-repeat - + ); }, }, @@ -243,7 +240,7 @@ exports.examples = [ return ( - center + center - 25% 75% + 25% 75% - right bottom + right bottom - borderRadius + borderRadius - borderWidth + borderColor + borderWidth + borderColor - non uniform borderRadius + non uniform borderRadius - non uniform borderWidth + non uniform borderWidth Date: Thu, 26 Jun 2025 13:11:00 +0530 Subject: [PATCH 3/7] fix: flow types --- .../BackgroundImage/BackgroundImageExample.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index b1b32a8204b5..b75023fe9cc6 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -10,9 +10,19 @@ 'use strict'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; + import {StyleSheet, Text, View} from 'react-native'; -function BackgroundImageBox({style, children, testID}) { +function BackgroundImageBox({ + style, + children, + testID, +}: { + style?: ViewStyleProp, + children?: React.Node, + testID?: string, +}) { return ( {children} @@ -409,8 +419,8 @@ exports.examples = [ ], experimental_backgroundPosition: [ { - x: 'center', - y: 'center', + top: '50%', + left: '50%', }, ], experimental_backgroundSize: [ From 98e03055cef80dc2534b09b2b9d07a6a8513c411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 26 Jun 2025 13:15:25 +0530 Subject: [PATCH 4/7] add missing testID --- .../BackgroundImage/BackgroundImageExample.js | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index b75023fe9cc6..b90c101be008 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -21,7 +21,7 @@ function BackgroundImageBox({ }: { style?: ViewStyleProp, children?: React.Node, - testID?: string, + testID: string, }) { return ( @@ -87,6 +87,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(45deg, #ff6b6b, #4ecdc4)', }} + testID="background-image-angle-1" /> @@ -96,6 +97,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(90deg, #a8edea, #fed6e3)', }} + testID="background-image-angle-2" /> @@ -105,6 +107,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(180deg, #ffecd2, #fcb69f)', }} + testID="background-image-angle-3" /> @@ -124,6 +127,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(to right, #ff6b6b, #4ecdc4, #45b7d1)', }} + testID="background-image-multiple-colors-1" /> @@ -133,6 +137,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(to bottom, #667eea, #764ba2, #f093fb, #f5576c)', }} + testID="background-image-multiple-colors-2" /> @@ -142,6 +147,7 @@ exports.examples = [ experimental_backgroundImage: 'linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet)', }} + testID="background-image-multiple-colors-3" /> @@ -161,6 +167,7 @@ exports.examples = [ experimental_backgroundImage: 'radial-gradient(circle, #ff6b6b, #4ecdc4)', }} + testID="background-image-radial-1" /> @@ -170,6 +177,7 @@ exports.examples = [ experimental_backgroundImage: 'radial-gradient(ellipse, #a8edea, #fed6e3)', }} + testID="background-image-radial-2" /> @@ -194,6 +202,7 @@ exports.examples = [ experimental_backgroundRepeat: 'repeat', experimental_backgroundSize: '20px 20px', }} + testID="background-image-repeat-1" /> @@ -207,6 +216,7 @@ exports.examples = [ experimental_backgroundRepeat: 'space', experimental_backgroundSize: '50px 50px', }} + testID="background-image-repeat-2" /> @@ -222,6 +232,7 @@ exports.examples = [ experimental_backgroundRepeat: 'round', experimental_backgroundSize: '60px 60px', }} + testID="background-image-repeat-3" /> @@ -236,6 +247,7 @@ exports.examples = [ experimental_backgroundSize: '100px 100px', backgroundColor: 'purple', }} + testID="background-image-repeat-4" /> @@ -260,6 +272,7 @@ exports.examples = [ experimental_backgroundSize: '50px 50px', borderWidth: 1, }} + testID="background-image-position-1" /> @@ -273,6 +286,7 @@ exports.examples = [ borderWidth: 1, experimental_backgroundSize: '50px 50px', }} + testID="background-image-position-2" /> @@ -286,6 +300,7 @@ exports.examples = [ borderWidth: 1, experimental_backgroundSize: '50px 50px', }} + testID="background-image-position-3" /> @@ -348,6 +363,7 @@ exports.examples = [ experimental_backgroundSize: 'cover', borderRadius: 20, }} + testID="background-image-borders-1" /> @@ -360,6 +376,7 @@ exports.examples = [ borderWidth: 10, borderColor: 'red', }} + testID="background-image-borders-2" /> @@ -374,6 +391,7 @@ exports.examples = [ borderBottomLeftRadius: 30, borderBottomRightRadius: 40, }} + testID="background-image-borders-3" /> @@ -388,6 +406,7 @@ exports.examples = [ borderBottomWidth: 20, borderBottomColor: 'blue', }} + testID="background-image-borders-4" /> From a4b8709b47e21400c6a197a5cff0637870d3b862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 26 Jun 2025 13:38:34 +0530 Subject: [PATCH 5/7] fix: build error update public api snapshot --- .../__snapshots__/public-api-test.js.snap | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) 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 6145e6ac40ce..b579409dc8fb 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 @@ -7308,6 +7308,39 @@ 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, @@ -7380,6 +7413,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\\", }>; export type ____ViewStyle_InternalCore = $ReadOnly<{ @@ -7707,6 +7747,27 @@ declare export default function processBackgroundImage( " `; +exports[`public API should not change unintentionally Libraries/StyleSheet/processBackgroundPosition.js 1`] = ` +"declare export default function processBackgroundPosition( + backgroundPosition: ?($ReadOnlyArray | string) +): $ReadOnlyArray; +" +`; + +exports[`public API should not change unintentionally Libraries/StyleSheet/processBackgroundRepeat.js 1`] = ` +"declare export default function processBackgroundRepeat( + backgroundRepeat: ?($ReadOnlyArray | string) +): $ReadOnlyArray; +" +`; + +exports[`public API should not change unintentionally Libraries/StyleSheet/processBackgroundSize.js 1`] = ` +"declare export default function processBackgroundSize( + backgroundSize: ?($ReadOnlyArray | string) +): $ReadOnlyArray; +" +`; + exports[`public API should not change unintentionally Libraries/StyleSheet/processBoxShadow.js 1`] = ` "export type ParsedBoxShadow = { offsetX: number, From 2134db48128c2aa9790bf1154842abdab6a26005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Mon, 29 Sep 2025 09:49:53 +0530 Subject: [PATCH 6/7] add space stripped example --- .../BackgroundImage/BackgroundImageExample.js | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index b90c101be008..5a2a06a5df83 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -39,7 +39,7 @@ const styles = StyleSheet.create({ marginVertical: 10, }, text: { - color: 'white', + color: 'black', fontWeight: 'bold', textShadowColor: 'black', textShadowRadius: 2, @@ -360,7 +360,6 @@ exports.examples = [ style={{ experimental_backgroundImage: 'linear-gradient(45deg, #667eea, #764ba2)', - experimental_backgroundSize: 'cover', borderRadius: 20, }} testID="background-image-borders-1" @@ -372,7 +371,6 @@ exports.examples = [ style={{ experimental_backgroundImage: 'radial-gradient(circle, #f093fb, #f5576c)', - experimental_backgroundSize: 'cover', borderWidth: 10, borderColor: 'red', }} @@ -385,7 +383,6 @@ exports.examples = [ style={{ experimental_backgroundImage: 'radial-gradient(circle, #f093fb, #f5576c)', - experimental_backgroundSize: 'cover', borderTopLeftRadius: 10, borderTopRightRadius: 20, borderBottomLeftRadius: 30, @@ -400,7 +397,6 @@ exports.examples = [ style={{ experimental_backgroundImage: 'radial-gradient(circle, #f093fb, #f5576c)', - experimental_backgroundSize: 'cover', borderTopWidth: 10, borderTopColor: 'red', borderBottomWidth: 20, @@ -413,6 +409,30 @@ exports.examples = [ ); }, }, + { + title: 'Complex Striped Pattern', + name: 'striped-pattern', + render() { + return ( + + Striped + + ); + }, + }, { title: 'Object syntax', name: 'object-syntax', From 4ee8d53bba7c0467cb00d0a1b95e391710f896fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 9 Oct 2025 15:57:52 +0530 Subject: [PATCH 7/7] flow fixes --- .../StyleSheet/processBackgroundRepeat.js | 21 ++++++++++++++++--- .../BackgroundImage/BackgroundImageExample.js | 5 +++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js b/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js index 01f6e9adcdd7..8e53e0ccfccc 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundRepeat.js @@ -10,9 +10,21 @@ 'use strict'; -import type {BackgroundRepeatValue} from './StyleSheetTypes'; +import type { + BackgroundRepeatKeyword, + BackgroundRepeatValue, +} from './StyleSheetTypes'; -const validValues = ['repeat', 'space', 'round', 'no-repeat'] as const; +function isBackgroundRepeatKeyword( + value: string, +): value is BackgroundRepeatKeyword { + return ( + value === 'repeat' || + value === 'space' || + value === 'round' || + value === 'no-repeat' + ); +} export default function processBackgroundRepeat( backgroundRepeat: ?($ReadOnlyArray | string), @@ -77,7 +89,10 @@ function parseBackgroundRepeatCSSString( } const token1 = part1.toLowerCase(); const token2 = part2.toLowerCase(); - if (validValues.includes(token1) && validValues.includes(token2)) { + if ( + isBackgroundRepeatKeyword(token1) && + isBackgroundRepeatKeyword(token2) + ) { result.push({x: token1, y: token2}); } else { return []; diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index 5a2a06a5df83..a3588a3d450b 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -10,6 +10,7 @@ 'use strict'; +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import {StyleSheet, Text, View} from 'react-native'; @@ -412,7 +413,7 @@ exports.examples = [ { title: 'Complex Striped Pattern', name: 'striped-pattern', - render() { + render(): React.Node { return ( ;