#tint'); object-fit: cover;"
+/>
`;
exports[`components/Image prop "style" supports "resizeMode" property 1`] = `
-
`;
exports[`components/Image prop "style" supports "shadow" properties (convert to filter) 1`] = `
-
`;
exports[`components/Image prop "style" supports "tintcolor" property (convert to filter) 1`] = `
-
-
#tint');"
- />
-

-
+

#tint'); object-fit: cover;"
+/>
`;
exports[`components/Image prop "testID" 1`] = `
-
+ decoding="async"
+ draggable="false"
+ loading="lazy"
+ style="object-fit: cover;"
+/>
`;
diff --git a/packages/react-native-web/src/exports/Image/__tests__/index-test.js b/packages/react-native-web/src/exports/Image/__tests__/index-test.js
index c0b5c590..41b91946 100644
--- a/packages/react-native-web/src/exports/Image/__tests__/index-test.js
+++ b/packages/react-native-web/src/exports/Image/__tests__/index-test.js
@@ -4,10 +4,11 @@
import { act } from 'react-dom/test-utils';
import * as AssetRegistry from '../../../modules/AssetRegistry';
import Image from '../';
-import ImageLoader, { ImageUriCache } from '../../../modules/ImageLoader';
+import { ImageUriCache } from '../../../modules/ImageLoader';
import PixelRatio from '../../PixelRatio';
import React from 'react';
import { render } from '@testing-library/react';
+import { createEventTarget } from 'dom-event-testing-library';
const originalImage = window.Image;
@@ -29,20 +30,48 @@ describe('components/Image', () => {
expect(container.firstChild).toMatchSnapshot();
});
+ describe('prop "alternativeText"', () => {
+ test('set to empty string', () => {
+ const { container } = render(
);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('set to value', () => {
+ const { container } = render(
+
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ });
+
test('prop "blurRadius"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const { container } = render(
);
expect(container.firstChild).toMatchSnapshot();
});
+ describe('prop "crossOrigin"', () => {
+ test('sets value', () => {
+ const { container } = render(
);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ });
+
+ describe('prop "decoding"', () => {
+ test('sets value', () => {
+ const { container } = render(
);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ });
+
describe('prop "defaultSource"', () => {
- test('sets background image when value is an object', () => {
+ test('sets image when value is an object', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const { container } = render(
);
expect(container.firstChild).toMatchSnapshot();
});
- test('sets background image when value is a string', () => {
+ test('sets image when value is a string', () => {
// emulate require-ed asset
const defaultSource = 'https://google.com/favicon.ico';
const { container } = render(
);
@@ -83,151 +112,173 @@ describe('components/Image', () => {
expect(container.firstChild).toMatchSnapshot();
});
+ describe('prop "loading"', () => {
+ test('sets value', () => {
+ const { container } = render(
);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ });
+
test('prop "nativeID"', () => {
const { container } = render(
);
expect(container.firstChild).toMatchSnapshot();
});
- describe('prop "onLoad"', () => {
- const originalLoad = ImageLoader.load;
-
- beforeEach(() => {
- ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
- onLoad();
- });
- });
-
- afterEach(() => {
- ImageLoader.load = originalLoad;
- });
-
- test('is not called again if callback changes', () => {
- const onLoadStub = jest.fn();
- const onLoadReplacementStub = jest.fn();
-
- const { rerender } = render(
-
- );
+ describe('prop "onError"', () => {
+ test('is called when image fails to load and replaces src with placeholder', () => {
+ const onError = jest.fn();
+ const ref = React.createRef();
+ let container;
act(() => {
- rerender(
);
+ ({ container } = render(
+
+ ));
});
- expect(onLoadStub.mock.calls.length).toBe(1);
- expect(onLoadReplacementStub.mock.calls.length).toBe(0);
+ const image = createEventTarget(ref.current);
+ act(() => {
+ image.error();
+ });
+ expect(onError).toBeCalledTimes(1);
+ expect(container.firstChild).toMatchSnapshot();
});
+ });
- test('is called after image is loaded from network', () => {
- jest.useFakeTimers();
- const onLoadStartStub = jest.fn();
- const onLoadStub = jest.fn();
- const onLoadEndStub = jest.fn();
- render(
-
+ describe('prop "onLoad"', () => {
+ test('is called once when image loads', () => {
+ const onLoad = jest.fn();
+ const onLoadEnd = jest.fn();
+ const onLoadStart = jest.fn();
+ const ref = React.createRef();
+ let container;
+ act(() => {
+ ({ container } = render(
+
+ ));
+ });
+ const image = createEventTarget(ref.current);
+ act(() => {
+ image.load({
+ target: {
+ width: 100,
+ height: 100,
+ naturalWidth: 200,
+ naturalHeight: 200,
+ currentSrc: 'https://google.com/favicon.ico',
+ src: 'https://google.com/favicon.ico'
+ }
+ });
+ });
+ expect(onLoad).toBeCalledTimes(1);
+ expect(onLoadEnd).toBeCalledTimes(1);
+ expect(onLoadStart).toBeCalledTimes(1);
+ expect(onLoad).toBeCalledWith(
+ expect.objectContaining({
+ nativeEvent: expect.objectContaining({
+ source: expect.objectContaining({
+ width: expect.any(Number),
+ height: expect.any(Number),
+ url: 'https://google.com/favicon.ico'
+ })
+ })
+ })
);
- jest.runOnlyPendingTimers();
- expect(onLoadStub).toBeCalled();
- });
-
- test('is called after image is loaded from cache', () => {
- jest.useFakeTimers();
- const onLoadStartStub = jest.fn();
- const onLoadStub = jest.fn();
- const onLoadEndStub = jest.fn();
- const uri = 'https://test.com/img.jpg';
- ImageUriCache.add(uri);
- render(
-
- );
- jest.runOnlyPendingTimers();
- expect(onLoadStub).toBeCalled();
- ImageUriCache.remove(uri);
+ expect(container.firstChild).toMatchSnapshot();
});
test('is called on update if "uri" is different', () => {
const onLoadStartStub = jest.fn();
const onLoadStub = jest.fn();
const onLoadEndStub = jest.fn();
- const { rerender } = render(
-
- );
+ const ref = React.createRef();
act(() => {
- rerender(
-
- );
- });
- expect(onLoadStub.mock.calls.length).toBe(2);
- expect(onLoadEndStub.mock.calls.length).toBe(2);
- });
-
- test('is not called on update if "uri" is the same', () => {
- const onLoadStartStub = jest.fn();
- const onLoadStub = jest.fn();
- const onLoadEndStub = jest.fn();
- const { rerender } = render(
-
- );
- act(() => {
- rerender(
+ render(
);
});
+ const image = createEventTarget(ref.current);
+ act(() => {
+ image.load({
+ target: {
+ width: 100,
+ height: 100,
+ naturalWidth: 200,
+ naturalHeight: 200,
+ currentSrc: 'https://test.com/img.jpg',
+ src: 'https://test.com/img.jpg'
+ }
+ });
+ });
+
expect(onLoadStub.mock.calls.length).toBe(1);
expect(onLoadEndStub.mock.calls.length).toBe(1);
- });
+ expect(onLoadStartStub.mock.calls.length).toBe(1);
- test('is not called on update if "uri" is the same and given as an object', () => {
- const onLoadStartStub = jest.fn();
- const onLoadStub = jest.fn();
- const onLoadEndStub = jest.fn();
- const { rerender } = render(
-
- );
act(() => {
- rerender(
+ render(
);
});
- expect(onLoadStub.mock.calls.length).toBe(1);
- expect(onLoadEndStub.mock.calls.length).toBe(1);
+ act(() => {
+ image.load({
+ target: {
+ width: 100,
+ height: 100,
+ naturalWidth: 200,
+ naturalHeight: 200,
+ currentSrc: 'https://blah.com/img.png',
+ src: 'https://blah.com/img.png'
+ }
+ });
+ });
+
+ expect(onLoadStub.mock.calls.length).toBe(2);
+ expect(onLoadEndStub.mock.calls.length).toBe(2);
+ expect(onLoadStartStub.mock.calls.length).toBe(2);
+ });
+
+ test('is not called when placeholder src is used after error', () => {
+ const onError = jest.fn();
+ const onLoad = jest.fn();
+ const ref = React.createRef();
+ act(() => {
+ render(
+
+ );
+ });
+ const image = createEventTarget(ref.current);
+ act(() => {
+ // Results in placeholder being "loaded"
+ image.error();
+ });
+ expect(onError).toBeCalledTimes(1);
+ act(() => {
+ // Emulate the native "load" event for the placeholder
+ image.load();
+ });
+ expect(onLoad).toBeCalledTimes(0);
});
});
@@ -257,9 +308,9 @@ describe('components/Image', () => {
test('is set immediately if the image was preloaded', () => {
const uri = 'https://yahoo.com/favicon.ico';
- ImageLoader.load = jest.fn().mockImplementationOnce((_, onLoad, onError) => {
- onLoad();
- });
+ //ImageLoader.load = jest.fn().mockImplementationOnce((_, onLoad, onError) => {
+ // onLoad();
+ //});
return Image.prefetch(uri).then(() => {
const source = { uri };
const { container } = render(
, { disableLifecycleMethods: true });
@@ -298,25 +349,21 @@ describe('components/Image', () => {
test('is correctly updated only when loaded if defaultSource provided', () => {
const defaultUri = 'https://testing.com/preview.jpg';
const uri = 'https://testing.com/fullSize.jpg';
- let loadCallback;
- ImageLoader.load = jest.fn().mockImplementationOnce((_, onLoad, onError) => {
- loadCallback = onLoad;
- });
const { container } = render(
);
expect(container.firstChild).toMatchSnapshot();
act(() => {
- loadCallback();
+ // loadCallback();
});
expect(container.firstChild).toMatchSnapshot();
});
test('it correctly selects the source scale', () => {
- AssetRegistry.getAssetByID = jest.fn(() => ({
+ AssetRegistry.getAssetByID = () => ({
httpServerLocation: 'static',
name: 'img',
scales: [1, 2, 3],
type: 'png'
- }));
+ });
PixelRatio.get = jest.fn(() => 1.0);
let { container } = render(
);
diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js
index c2c50a36..643bca9c 100644
--- a/packages/react-native-web/src/exports/Image/index.js
+++ b/packages/react-native-web/src/exports/Image/index.js
@@ -8,38 +8,58 @@
* @flow
*/
-import type { ImageProps } from './types';
+import type { ImageProps, ImageStatics } from './types';
+import type { PlatformMethods } from '../../types';
import * as React from 'react';
import createElement from '../createElement';
import css from '../StyleSheet/css';
-import { getAssetByID } from '../../modules/AssetRegistry';
+import * as forwardedProps from '../../modules/forwardedProps';
+import pick from '../../modules/pick';
+import processColor from '../processColor';
import resolveShadowValue from '../StyleSheet/resolveShadowValue';
+import useElementLayout from '../../modules/useElementLayout';
+import useMergeRefs from '../../modules/useMergeRefs';
+import usePlatformMethods from '../../modules/usePlatformMethods';
+import useResponderEvents from '../../modules/useResponderEvents';
import ImageLoader from '../../modules/ImageLoader';
import PixelRatio from '../PixelRatio';
import StyleSheet from '../StyleSheet';
import TextAncestorContext from '../Text/TextAncestorContext';
import View from '../View';
-import processColor from '../processColor';
-export type { ImageProps };
+import { getAssetByID, getAssetUriByID } from '../../modules/AssetRegistry';
-const ERRORED = 'ERRORED';
-const LOADED = 'LOADED';
-const LOADING = 'LOADING';
-const IDLE = 'IDLE';
+const emptyObject = {};
+const forwardPropsList = {
+ ...forwardedProps.defaultProps,
+ ...forwardedProps.accessibilityProps,
+ ...forwardedProps.clickProps,
+ ...forwardedProps.focusProps,
+ ...forwardedProps.keyboardProps,
+ ...forwardedProps.mouseProps,
+ ...forwardedProps.touchProps,
+ ...forwardedProps.styleProps,
+ crossOrigin: true,
+ decoding: true,
+ draggable: true,
+ loading: true,
+ referrerPolicy: true
+};
+
+const pickProps = (props) => pick(props, forwardPropsList);
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
-function getFlatStyle(style, blurRadius) {
- const flatStyle = { ...StyleSheet.flatten(style) };
+function getFlatStyle(style, blurRadius, resizeModeProp) {
+ const initialStyle = StyleSheet.flatten(style) || emptyObject;
+ const objectFitStyle = resizeModeStyles[initialStyle.resizeMode || resizeModeProp || 'cover'];
+ const flatStyle = { ...initialStyle, ...objectFitStyle };
const { filter, resizeMode, shadowOffset, tintColor } = flatStyle;
// Add CSS filters
// React Native exposes these features as props and proprietary styles
const filters = [];
- let _filter = null;
-
if (filter) {
filters.push(filter);
}
@@ -74,10 +94,6 @@ function getFlatStyle(style, blurRadius) {
}
}
- if (filters.length > 0) {
- _filter = filters.join(' ');
- }
-
// These styles are converted to CSS filters applied to the
// element displaying the background image.
delete flatStyle.blurRadius;
@@ -90,40 +106,32 @@ function getFlatStyle(style, blurRadius) {
delete flatStyle.overlayColor;
delete flatStyle.resizeMode;
- return [flatStyle, resizeMode, _filter];
+ if (filters.length > 0) {
+ flatStyle.filter = filters.join(' ');
+ }
+
+ return flatStyle;
}
-function resolveAssetDimensions(source) {
- if (typeof source === 'number') {
- const { height, width } = getAssetByID(source);
- return { height, width };
- } else if (source != null && !Array.isArray(source) && typeof source === 'object') {
- const { height, width } = source;
- return { height, width };
+function getImageData(image: HTMLImageElement) {
+ const { width, height, currentSrc } = image;
+ return {
+ source: { height, width, url: currentSrc },
+ target: image
}
}
function resolveAssetUri(source): ?string {
let uri = null;
if (typeof source === 'number') {
- // get the URI from the packager
- const asset = getAssetByID(source);
- let scale = asset.scales[0];
- if (asset.scales.length > 1) {
- const preferredScale = PixelRatio.get();
- // Get the scale which is closest to the preferred scale
- scale = asset.scales.reduce((prev, curr) =>
- Math.abs(curr - preferredScale) < Math.abs(prev - preferredScale) ? curr : prev
- );
- }
- const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
- uri = asset ? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}` : '';
+ uri = getAssetUriByID(source);
} else if (typeof source === 'string') {
uri = source;
+ } else if (Array.isArray(source)) {
+ uri = source[0].uri;
} else if (source && typeof source.uri === 'string') {
uri = source.uri;
}
-
if (uri) {
const match = uri.match(svgDataUriPattern);
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
@@ -133,36 +141,42 @@ function resolveAssetUri(source): ?string {
return `${prefix}${encodedSvg}`;
}
}
-
return uri;
}
-interface ImageStatics {
- getSize: (
- uri: string,
- success: (width: number, height: number) => void,
- failure: () => void
- ) => void;
- prefetch: (uri: string) => Promise
;
- queryCache: (uris: Array) => Promise<{| [uri: string]: 'disk/memory' |}>;
-}
+const ERROR_PLACEHOLDER =
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
-const Image: React.AbstractComponent> = React.forwardRef(
- (props, ref) => {
+const Image: React.AbstractComponent = React.forwardRef(
+ (props, forwardedRef) => {
const {
- accessibilityLabel,
blurRadius,
defaultSource,
- draggable,
onError,
onLayout,
onLoad,
onLoadEnd,
onLoadStart,
+ onMoveShouldSetResponder,
+ onMoveShouldSetResponderCapture,
+ onResponderEnd,
+ onResponderGrant,
+ onResponderMove,
+ onResponderReject,
+ onResponderRelease,
+ onResponderStart,
+ onResponderTerminate,
+ onResponderTerminationRequest,
+ onScrollShouldSetResponder,
+ onScrollShouldSetResponderCapture,
+ onSelectionChangeShouldSetResponder,
+ onSelectionChangeShouldSetResponderCapture,
+ onStartShouldSetResponder,
+ onStartShouldSetResponderCapture,
pointerEvents,
+ resizeMode,
source,
- style,
- ...rest
+ style
} = props;
if (process.env.NODE_ENV !== 'production') {
@@ -173,135 +187,171 @@ const Image: React.AbstractComponent>
}
}
- const [state, updateState] = React.useState(() => {
- const uri = resolveAssetUri(source);
- if (uri != null) {
- const isLoaded = ImageLoader.has(uri);
- if (isLoaded) {
- return LOADED;
- }
- }
- return IDLE;
+ const supportedProps = pickProps(props);
+
+ const src = resolveAssetUri(source) || resolveAssetUri(defaultSource);
+ let srcSet;
+ if (Array.isArray(source)) {
+ srcSet = source.map(({ uri, scale }) => `${uri} ${scale || 1}x`);
+ }
+
+ const [managedSrc, setManagedSrc] = React.useState(src);
+ const flatStyle = getFlatStyle(style, blurRadius, resizeMode);
+
+ const hostRef = React.useRef(null);
+ const hasTextAncestor = React.useContext(TextAncestorContext);
+ useElementLayout(hostRef, onLayout);
+ useResponderEvents(hostRef, {
+ onMoveShouldSetResponder,
+ onMoveShouldSetResponderCapture,
+ onResponderEnd,
+ onResponderGrant,
+ onResponderMove,
+ onResponderReject,
+ onResponderRelease,
+ onResponderStart,
+ onResponderTerminate,
+ onResponderTerminationRequest,
+ onScrollShouldSetResponder,
+ onScrollShouldSetResponderCapture,
+ onSelectionChangeShouldSetResponder,
+ onSelectionChangeShouldSetResponderCapture,
+ onStartShouldSetResponder,
+ onStartShouldSetResponderCapture
});
- const [layout, updateLayout] = React.useState({});
- const hasTextAncestor = React.useContext(TextAncestorContext);
- const hiddenImageRef = React.useRef(null);
- const requestRef = React.useRef(null);
- const shouldDisplaySource = state === LOADED || (state === LOADING && defaultSource == null);
- const [flatStyle, _resizeMode, filter] = getFlatStyle(style, blurRadius);
- const resizeMode = props.resizeMode || _resizeMode || 'cover';
- const selectedSource = shouldDisplaySource ? source : defaultSource;
- const displayImageUri = resolveAssetUri(selectedSource);
- const imageSizeStyle = resolveAssetDimensions(selectedSource);
- const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
- const backgroundSize = getBackgroundSize();
-
- // Accessibility image allows users to trigger the browser's image context menu
- const hiddenImage = displayImageUri
- ? createElement('img', {
- alt: accessibilityLabel || '',
- classList: [classes.accessibilityImage],
- draggable: draggable || false,
- ref: hiddenImageRef,
- src: displayImageUri
- })
- : null;
-
- function getBackgroundSize(): ?string {
- if (hiddenImageRef.current != null && (resizeMode === 'center' || resizeMode === 'repeat')) {
- const { naturalHeight, naturalWidth } = hiddenImageRef.current;
- const { height, width } = layout;
- if (naturalHeight && naturalWidth && height && width) {
- const scaleFactor = Math.min(1, width / naturalWidth, height / naturalHeight);
- const x = Math.ceil(scaleFactor * naturalWidth);
- const y = Math.ceil(scaleFactor * naturalHeight);
- return `${x}px ${y}px`;
+ const internalImageRef = React.useCallback((target) => {
+ const errorListener = function (e) {
+ // If the image fails to load, browsers will display a "broken" icon.
+ // To avoid this we replace the image with a transparent gif.
+ setManagedSrc(ERROR_PLACEHOLDER);
+ if (onError != null) {
+ onError({
+ nativeEvent: {
+ error: `Failed to load resource ${e.target.src} (404)`
+ }
+ });
}
- }
- }
-
- function handleLayout(e) {
- if (resizeMode === 'center' || resizeMode === 'repeat' || onLayout) {
- const { layout } = e.nativeEvent;
- onLayout && onLayout(e);
- updateLayout(layout);
- }
- }
-
- // Image loading
- const uri = resolveAssetUri(source);
- React.useEffect(() => {
- abortPendingRequest();
-
- if (uri != null) {
- updateState(LOADING);
- if (onLoadStart) {
- onLoadStart();
+ if (onLoadEnd != null) {
+ onLoadEnd({ nativeEvent: { target }});
}
+ };
- requestRef.current = ImageLoader.load(
- uri,
- function load(e) {
- updateState(LOADED);
- if (onLoad) {
- onLoad(e);
- }
- if (onLoadEnd) {
- onLoadEnd();
- }
- },
- function error() {
- updateState(ERRORED);
- if (onError) {
- onError({
- nativeEvent: {
- error: `Failed to load resource ${uri} (404)`
- }
- });
- }
- if (onLoadEnd) {
- onLoadEnd();
- }
+ const loadListener = function (e) {
+ const { target: image } = e;
+ if (image.src === ERROR_PLACEHOLDER) {
+ // Prevent the placeholder from triggering a 'load' event that event
+ // listeners would otherwise receive.
+ e.stopImmediatePropagation();
+ } else {
+ if (onLoad != null) {
+ onLoad({
+ nativeEvent: getImageData(image)
+ });
+ }
+ if (onLoadEnd != null) {
+ onLoadEnd({ nativeEvent: { target }});
}
- );
- }
-
- function abortPendingRequest() {
- if (requestRef.current != null) {
- ImageLoader.abort(requestRef.current);
- requestRef.current = null;
}
+ };
+
+ if (target !== null) {
+ // If the image is loaded before JS loads (e.g., SSR), then we manually
+ // call onLoad
+// console.log(target.complete)
+ if (onLoad != null && target.complete) {
+ onLoad({
+ nativeEvent: getImageData(target)
+ });
+ return;
+ }
+
+ hostRef.current = target;
+ if (onLoadStart != null) {
+ onLoadStart({ nativeEvent: { target }});
+ }
+ target.addEventListener('error', errorListener);
+ target.addEventListener('load', loadListener);
+ } else if (hostRef.current != null) {
+ const node = hostRef.current;
+ node.removeEventListener('error', errorListener);
+ node.removeEventListener('load', loadListener);
+ hostRef.current = null;
}
+ },
+ [onError, onLoad, onLoadEnd]
+ );
- return abortPendingRequest;
- }, [uri, requestRef, updateState, onError, onLoad, onLoadEnd, onLoadStart]);
+ const platformMethodsRef = usePlatformMethods(supportedProps);
- return (
-
-
- {hiddenImage}
-
+ const ref = useMergeRefs(
+ hostRef,
+ internalImageRef,
+ platformMethodsRef,
+ forwardedRef
);
+
+ supportedProps.alt = props.alternativeText;
+ supportedProps.classList = hasTextAncestor ? inlineClassList : defaultClassList;
+ supportedProps.decoding = props.decoding || 'async';
+ supportedProps.draggable = props.draggable || false;
+ supportedProps.loading = props.loading || 'lazy';
+ supportedProps.ref = ref;
+ supportedProps.src = managedSrc;
+ supportedProps.srcSet =
+ srcSet != null && managedSrc !== ERROR_PLACEHOLDER
+ ? srcSet.join(',')
+ : null;
+ supportedProps.style = flatStyle;
+
+ return createElement('img', supportedProps);
}
);
Image.displayName = 'Image';
+const classes = css.create({
+ image: {
+ backgroundColor: 'transparent',
+ border: '0 solid black',
+ boxSizing: 'border-box',
+ display: 'flex',
+ flexDirection: 'column',
+ flexShrink: 0,
+ margin: 0,
+ minHeight: 0,
+ minWidth: 0,
+ objectFit: 'cover',
+ padding: 0,
+ position: 'relative',
+ zIndex: 0
+ },
+ inlineImage: {
+ display: 'inline-flex'
+ }
+});
+
+const defaultClassList = [classes.image];
+const inlineClassList = [classes.image, classes.inlineImage];
+
+const resizeModeStyles = {
+ center: {
+ objectFit: 'scale-down'
+ },
+ contain: {
+ objectFit: 'contain'
+ },
+ cover: {
+ objectFit: 'cover'
+ },
+ none: {
+ objectFit: 'none'
+ },
+ stretch: {
+ objectFit: 'fill'
+ }
+};
+
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
const ImageWithStatics = (Image: React.AbstractComponent<
ImageProps,
@@ -310,7 +360,7 @@ const ImageWithStatics = (Image: React.AbstractComponent<
ImageStatics);
ImageWithStatics.getSize = function (uri, success, failure) {
- ImageLoader.getSize(uri, success, failure);
+ return ImageLoader.getSize(uri, success, failure);
};
ImageWithStatics.prefetch = function (uri) {
@@ -321,59 +371,6 @@ ImageWithStatics.queryCache = function (uris) {
return ImageLoader.queryCache(uris);
};
-const classes = css.create({
- accessibilityImage: {
- ...StyleSheet.absoluteFillObject,
- height: '100%',
- opacity: 0,
- width: '100%',
- zIndex: -1
- }
-});
-
-const styles = StyleSheet.create({
- root: {
- flexBasis: 'auto',
- overflow: 'hidden',
- zIndex: 0
- },
- inline: {
- display: 'inline-flex'
- },
- image: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: 'transparent',
- backgroundPosition: 'center',
- backgroundRepeat: 'no-repeat',
- backgroundSize: 'cover',
- height: '100%',
- width: '100%',
- zIndex: -1
- }
-});
-
-const resizeModeStyles = StyleSheet.create({
- center: {
- backgroundSize: 'auto'
- },
- contain: {
- backgroundSize: 'contain'
- },
- cover: {
- backgroundSize: 'cover'
- },
- none: {
- backgroundPosition: '0 0',
- backgroundSize: 'auto'
- },
- repeat: {
- backgroundPosition: '0 0',
- backgroundRepeat: 'repeat',
- backgroundSize: 'auto'
- },
- stretch: {
- backgroundSize: '100% 100%'
- }
-});
+export type { ImageProps };
export default ImageWithStatics;
diff --git a/packages/react-native-web/src/exports/Image/types.js b/packages/react-native-web/src/exports/Image/types.js
index 17495813..e99d481a 100644
--- a/packages/react-native-web/src/exports/Image/types.js
+++ b/packages/react-native-web/src/exports/Image/types.js
@@ -98,16 +98,36 @@ export type ImageStyle = {
export type ImageProps = {
...ViewProps,
+ alternativeText?: ?string,
blurRadius?: number,
+ crossOrigin?: 'anonymous' | 'use-credentials',
+ decoding?: 'auto' | 'async' | 'sync',
defaultSource?: Source,
draggable?: boolean,
+ loading?: 'eager' | 'lazy',
onError?: (e: any) => void,
onLayout?: (e: any) => void,
onLoad?: (e: any) => void,
onLoadEnd?: (e: any) => void,
onLoadStart?: (e: any) => void,
onProgress?: (e: any) => void,
+ referrerPolicy?:
+ | 'no-referrer'
+ | 'no-referrer-when-downgrade'
+ | 'origin'
+ | 'origin-when-cross-origin'
+ | 'unsafe-url',
resizeMode?: ResizeMode,
source?: Source,
style?: GenericStyleProp
};
+
+export interface ImageStatics {
+ getSize: (
+ uri: string,
+ success: (width: number, height: number) => void,
+ failure: () => void
+ ) => void;
+ prefetch: (uri: string) => Promise;
+ queryCache: (uris: Array) => Promise<{| [uri: string]: 'disk/memory' |}>;
+}
diff --git a/packages/react-native-web/src/modules/AssetRegistry/index.js b/packages/react-native-web/src/modules/AssetRegistry/index.js
index 69d1ec49..f7ed8a9b 100644
--- a/packages/react-native-web/src/modules/AssetRegistry/index.js
+++ b/packages/react-native-web/src/modules/AssetRegistry/index.js
@@ -7,6 +7,8 @@
* @flow
*/
+import PixelRatio from '../../exports/PixelRatio';
+
export type PackagerAsset = {
__packager_asset: boolean,
fileSystemLocation: string,
@@ -30,3 +32,18 @@ export function registerAsset(asset: PackagerAsset): number {
export function getAssetByID(assetId: number): PackagerAsset {
return assets[assetId - 1];
}
+
+export function getAssetUriByID(assetId: number): string {
+ // get the URI from the packager
+ const asset = getAssetByID(assetId);
+ let scale = asset.scales[0];
+ if (asset.scales.length > 1) {
+ const preferredScale = PixelRatio.get();
+ // Get the scale which is closest to the preferred scale
+ scale = asset.scales.reduce((prev, curr) =>
+ Math.abs(curr - preferredScale) < Math.abs(prev - preferredScale) ? curr : prev
+ );
+ }
+ const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
+ return asset ? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}` : '';
+}