From 47a05bc26ab76add640183c1d9d8cbba39d7d0d2 Mon Sep 17 00:00:00 2001 From: dhruvtailor7 Date: Wed, 7 Sep 2022 13:13:08 -0700 Subject: [PATCH] feat: added `crossOrigin`, `referrerPolicy`, `srcSet`, `width`, `height` and `src` props to the Image Component. (#34481) Summary: This PR is for adding the support for `crossOrigin`, `referrerPolicy`, `width`, `height` and `srcSet` props to Image Component and mapping the `src` prop to `source.uri` of Image Component for the issue https://github.com/facebook/react-native/issues/34424. An example is also added in the RNTester ImageExample. ## Changelog [General] [Changed] - Map the `src` prop to `source.uri` prop in Image Component. [General] [Added] - added `crossOrigin`, `referrerPolicy`, `width`, `height` and `srcSet` props to Image Component. Pull Request resolved: https://github.com/facebook/react-native/pull/34481 Test Plan: 1. Navigate to Image Component Example in the RNTester app. 2. Contains an example of the Image component using the `src` and `srcSet` prop. 3. For headers, inspect the Image request using Flipper. Reviewed By: christophpurrer Differential Revision: D38982041 Pulled By: cipolleschi fbshipit-source-id: dd6594e39b8f3b36cfcdafa35695254034f1fb7f --- Libraries/Image/Image.android.js | 30 ++-- Libraries/Image/Image.ios.js | 15 +- Libraries/Image/ImageProps.js | 55 ++++++- Libraries/Image/ImageSourceUtils.js | 80 ++++++++++ .../Image/__tests__/ImageSourceUtils-test.js | 145 ++++++++++++++++++ .../js/examples/Image/ImageExample.js | 28 +++- 6 files changed, 323 insertions(+), 30 deletions(-) create mode 100644 Libraries/Image/ImageSourceUtils.js create mode 100644 Libraries/Image/__tests__/ImageSourceUtils-test.js diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js index eac8a56c63eba7..4c9605514eda41 100644 --- a/Libraries/Image/Image.android.js +++ b/Libraries/Image/Image.android.js @@ -23,6 +23,7 @@ import TextInlineImageNativeComponent from './TextInlineImageNativeComponent'; import type {ImageProps as ImagePropsType} from './ImageProps'; import type {RootTag} from '../Types/RootTagTypes'; +import {getImageSourcesFromImageProps} from './ImageSourceUtils'; let _requestId = 1; function generateRequestId() { @@ -126,25 +127,12 @@ export type ImageComponentStatics = $ReadOnly<{| /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const BaseImage = (props: ImagePropsType, forwardedRef) => { - let source = resolveAssetSource(props.source); + let source = getImageSourcesFromImageProps(props); const defaultSource = resolveAssetSource(props.defaultSource); const loadingIndicatorSource = resolveAssetSource( props.loadingIndicatorSource, ); - if (source) { - const uri = source.uri; - if (uri === '') { - console.warn('source.uri should not be an empty string'); - } - } - - if (props.src) { - console.warn( - 'The component requires a `source` property rather than `src`.', - ); - } - if (props.children) { throw new Error( 'The component cannot contain children. If you want to render content on top of the image, consider using the component or absolute positioning.', @@ -163,24 +151,28 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => { let style; let sources; - if (source?.uri != null) { - const {width, height} = source; + if (!Array.isArray(source) && source?.uri != null) { + const {width = props.width, height = props.height, uri} = source; style = flattenStyle([{width, height}, styles.base, props.style]); - sources = [{uri: source.uri}]; + sources = [{uri: uri, width: width, height: height}]; + if (uri === '') { + console.warn('source.uri should not be an empty string'); + } } else { style = flattenStyle([styles.base, props.style]); sources = source; } + const {height, width, ...restProps} = props; const {onLoadStart, onLoad, onLoadEnd, onError} = props; const nativeProps = { - ...props, + ...restProps, style, shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError), src: sources, /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found * when making Flow check .android.js files. */ - headers: source?.headers, + headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}), defaultSrc: defaultSource ? defaultSource.uri : null, loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 98631a33555bc5..2c0119f75b23e0 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -23,6 +23,7 @@ import NativeImageLoaderIOS from './NativeImageLoaderIOS'; import ImageViewNativeComponent from './ImageViewNativeComponent'; import type {RootTag} from 'react-native/Libraries/Types/RootTagTypes'; +import {getImageSourcesFromImageProps} from './ImageSourceUtils'; function getSize( uri: string, @@ -105,7 +106,7 @@ export type ImageComponentStatics = $ReadOnly<{| /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const BaseImage = (props: ImagePropsType, forwardedRef) => { - const source = resolveAssetSource(props.source) || { + const source = getImageSourcesFromImageProps(props) || { uri: undefined, width: undefined, height: undefined, @@ -117,7 +118,7 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => { style = flattenStyle([styles.base, props.style]) || {}; sources = source; } else { - const {width, height, uri} = source; + const {width = props.width, height = props.height, uri} = source; style = flattenStyle([{width, height}, styles.base, props.style]) || {}; sources = [source]; @@ -131,24 +132,20 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => { // $FlowFixMe[prop-missing] const tintColor = props.tintColor || style.tintColor; - if (props.src != null) { - console.warn( - 'The component requires a `source` property rather than `src`.', - ); - } - if (props.children != null) { throw new Error( 'The component cannot contain children. If you want to render content on top of the image, consider using the component or absolute positioning.', ); } + const {src, width, height, ...restProps} = props; + return ( {analyticTag => { return ( { + let source = resolveAssetSource(imageProps.source); + + let sources; + + const {crossOrigin, referrerPolicy, src, srcSet, width, height} = imageProps; + + const headers: {[string]: string} = {}; + if (crossOrigin === 'use-credentials') { + headers['Access-Control-Allow-Credentials'] = 'true'; + } + if (referrerPolicy != null) { + headers['Referrer-Policy'] = referrerPolicy; + } + if (srcSet != null) { + const sourceList = []; + const srcSetList = srcSet.split(', '); + // `src` prop should be used with default scale if `srcSet` does not have 1x scale. + let shouldUseSrcForDefaultScale = true; + srcSetList.forEach(imageSrc => { + const [uri, xScale = '1x'] = imageSrc.split(' '); + if (!xScale.endsWith('x')) { + console.warn( + 'The provided format for scale is not supported yet. Please use scales like 1x, 2x, etc.', + ); + } else { + const scale = parseInt(xScale.split('x')[0], 10); + if (!isNaN(scale)) { + // 1x scale is provided in `srcSet` prop so ignore the `src` prop if provided. + shouldUseSrcForDefaultScale = + scale === 1 ? false : shouldUseSrcForDefaultScale; + sourceList.push({headers: headers, scale, uri, width, height}); + } + } + }); + + if (shouldUseSrcForDefaultScale && src != null) { + sourceList.push({ + headers: headers, + scale: 1, + uri: src, + width, + height, + }); + } + if (sourceList.length === 0) { + console.warn('The provided value for srcSet is not valid.'); + } + + sources = sourceList; + } else if (src != null) { + sources = [{uri: src, headers: headers, width, height}]; + } else { + sources = source; + } + return sources; +} diff --git a/Libraries/Image/__tests__/ImageSourceUtils-test.js b/Libraries/Image/__tests__/ImageSourceUtils-test.js new file mode 100644 index 00000000000000..23ca3bbf58d8b0 --- /dev/null +++ b/Libraries/Image/__tests__/ImageSourceUtils-test.js @@ -0,0 +1,145 @@ +/** + * 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. + * + * @format + * @emails oncall+react_native + */ + +const {getImageSourcesFromImageProps} = require('../ImageSourceUtils'); + +describe('ImageSourceUtils', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('source prop provided', () => { + const imageProps = {source: require('./img/img1.png')}; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + }); + + it('should ignore source when src is provided', () => { + let uri = 'imageURI'; + const imageProps = {source: require('./img/img1.png'), src: uri}; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(1); + expect(sources[0].uri).toBe(uri); + }); + + it('should ignore source and src when srcSet is provided', () => { + let uri = 'imageURI'; + + let uri1 = 'uri1'; + let scale1 = '1x'; + + let uri2 = 'uri2'; + let scale2 = '2x'; + + const imageProps = { + source: require('./img/img1.png'), + src: uri, + srcSet: `${uri1} ${scale1}, ${uri2} ${scale2}`, + }; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(2); + expect(sources[0]).toEqual(expect.objectContaining({uri: uri1, scale: 1})); + expect(sources[1]).toEqual(expect.objectContaining({uri: uri2, scale: 2})); + }); + + it('should use src as default when 1x scale is not provided in srcSet', () => { + let uri = 'imageURI'; + + let uri1 = 'uri1'; + let scale1 = '3x'; + + let uri2 = 'uri2'; + let scale2 = '2x'; + + const imageProps = { + src: uri, + srcSet: `${uri1} ${scale1}, ${uri2} ${scale2}`, + }; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(3); + expect(sources[0]).toEqual(expect.objectContaining({uri: uri1, scale: 3})); + expect(sources[1]).toEqual(expect.objectContaining({uri: uri2, scale: 2})); + expect(sources[2]).toEqual(expect.objectContaining({uri: uri, scale: 1})); + }); + + it('should use 1x as default scale if only url is provided in srcSet', () => { + let uri1 = 'uri1'; + let scale1 = '2x'; + + let uri2 = 'uri2'; + + const imageProps = { + srcSet: `${uri1} ${scale1}, ${uri2}`, + }; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(2); + expect(sources[0]).toEqual(expect.objectContaining({uri: uri1, scale: 2})); + expect(sources[1]).toEqual(expect.objectContaining({uri: uri2, scale: 1})); + }); + + it('should warn when an unsupported scale is provided in srcSet', () => { + const mockWarn = jest.spyOn(console, 'warn'); + let uri1 = 'uri1'; + let scale1 = '300w'; + + let uri2 = 'uri2'; + + const imageProps = { + srcSet: `${uri1} ${scale1}, ${uri2}`, + }; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(1); + expect(mockWarn).toHaveBeenCalled(); + }); + + it('should contain crossorigin headers when provided with src', () => { + let uri = 'imageURI'; + + const imageProps = { + src: uri, + crossOrigin: 'use-credentials', + }; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(1); + expect(sources[0]).toHaveProperty('headers', { + ['Access-Control-Allow-Credentials']: 'true', + }); + }); + + it('should contain referrerPolicy headers when provided with src', () => { + let uri = 'imageURI'; + + let referrerPolicy = 'origin-when-cross-origin'; + const imageProps = { + src: uri, + referrerPolicy: referrerPolicy, + }; + const sources = getImageSourcesFromImageProps(imageProps); + + expect(sources).toBeDefined(); + expect(sources).toHaveLength(1); + expect(sources[0]).toHaveProperty('headers', { + ['Referrer-Policy']: referrerPolicy, + }); + }); +}); diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 5c257c81f44787..cddfc87a83517d 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -635,13 +635,39 @@ exports.description = exports.examples = [ { - title: 'Plain Network Image', + title: 'Plain Network Image with `source` prop.', description: ('If the `source` prop `uri` property is prefixed with ' + '"http", then it will be downloaded from the network.': string), render: function (): React.Node { return ; }, }, + { + title: 'Plain Network Image with `src` prop.', + description: ('If the `src` prop is defined with ' + + '"http", then it will be downloaded from the network.': string), + render: function (): React.Node { + return ; + }, + }, + { + title: 'Multiple Image Source using the `srcSet` prop.', + description: + ('A list of comma seperated uris along with scale are provided in `srcSet`.' + + 'An appropriate value will be used based on the scale of the device.': string), + render: function (): React.Node { + return ( + + ); + }, + }, { title: 'Plain Blob Image', description: ('If the `source` prop `uri` property is an object URL, ' +