Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 107 additions & 63 deletions packages/react-native/Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import ImageViewNativeComponent from './ImageViewNativeComponent';
import NativeImageLoaderIOS from './NativeImageLoaderIOS';
import resolveAssetSource from './resolveAssetSource';
import * as React from 'react';
import {use} from 'react';

export type ImageInstance = HostInstance;

Expand Down Expand Up @@ -102,6 +103,12 @@ async function queryCache(
return NativeImageLoaderIOS.queryCache(urls);
}

const EMPTY_IMAGE_SOURCE = {
uri: undefined,
width: undefined,
height: undefined,
};

/**
* A React component for displaying different types of images,
* including network images, static resources, temporary local images, and
Expand All @@ -111,90 +118,127 @@ async function queryCache(
*/
let BaseImage: AbstractImageIOS = ({
ref: forwardedRef,
...props
accessible,
accessibilityLabel,
accessibilityState,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-hidden': ariaHidden,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
alt,
children,
crossOrigin,
height,
referrerPolicy,
resizeMode,
source,
src,
srcSet,
style,
tintColor,
width,
...restProps
}: {
ref?: React.RefSetter<ImageInstance>,
...ImageProps,
}) => {
const source = getImageSourcesFromImageProps(props) || {
uri: undefined,
width: undefined,
height: undefined,
};
const resolvedSource =
getImageSourcesFromImageProps({
crossOrigin,
height,
referrerPolicy,
source,
src,
srcSet,
width,
}) || EMPTY_IMAGE_SOURCE;

let style: ImageStyleProp;
let resolvedStyle: ImageStyleProp;
let sources;
if (Array.isArray(source)) {
style = [styles.base, props.style];
sources = source;
if (Array.isArray(resolvedSource)) {
resolvedStyle = [styles.base, style];
sources = resolvedSource;
} else {
const {uri} = source;
const {uri} = resolvedSource;
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
const width = source.width ?? props.width;
const height = source.height ?? props.height;
style = [{width, height}, styles.base, props.style];
sources = [source];
resolvedStyle = [
{
width: resolvedSource.width ?? width,
height: resolvedSource.height ?? height,
},
styles.base,
style,
];
sources = [resolvedSource];
}

const flattenedStyle = flattenStyle<ImageStyleProp>(style);
const objectFit = convertObjectFitToResizeMode(flattenedStyle?.objectFit);
const resizeMode =
objectFit || props.resizeMode || flattenedStyle?.resizeMode || 'cover';
const tintColor = props.tintColor ?? flattenedStyle?.tintColor;

if (props.children != null) {
if (children != null) {
throw new Error(
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
);
}
const {
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-selected': ariaSelected,
'aria-hidden': ariaHidden,
src,
...restProps
} = props;

const _accessibilityState = {
busy: ariaBusy ?? props.accessibilityState?.busy,
checked: ariaChecked ?? props.accessibilityState?.checked,
disabled: ariaDisabled ?? props.accessibilityState?.disabled,
expanded: ariaExpanded ?? props.accessibilityState?.expanded,
selected: ariaSelected ?? props.accessibilityState?.selected,

const resolvedProps = restProps as {
...React.PropsOf<ImageViewNativeComponent>,
};

// In order for `aria-hidden` to work on iOS we must set `accessible` to false (`accessibilityElementsHidden` is not enough).
const accessible =
ariaHidden !== true && (props.alt !== undefined ? true : props.accessible);
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;
resolvedProps.style = resolvedStyle;
resolvedProps.source = sources;

const flattenedStyle = flattenStyle<ImageStyleProp>(resolvedStyle);
const objectFit = convertObjectFitToResizeMode(flattenedStyle?.objectFit);
resolvedProps.resizeMode =
objectFit || resizeMode || flattenedStyle?.resizeMode || 'cover';
resolvedProps.tintColor = tintColor ?? flattenedStyle?.tintColor;

if (ariaLabel != null) {
resolvedProps.accessibilityLabel = ariaLabel;
} else if (accessibilityLabel != null) {
resolvedProps.accessibilityLabel = accessibilityLabel;
} else if (alt != null) {
resolvedProps.accessibilityLabel = alt;
}

if (ariaHidden === true) {
// In order for `aria-hidden` to work on iOS we must set `accessible` to
// false (`accessibilityElementsHidden` is not enough).
resolvedProps.accessible = false;
} else if (alt != null) {
resolvedProps.accessible = true;
} else if (accessible != null) {
resolvedProps.accessible = accessible;
}

if (
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
resolvedProps.accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
}

const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);

return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
return (
<ImageViewNativeComponent
accessibilityState={_accessibilityState}
{...restProps}
accessible={accessible}
accessibilityLabel={accessibilityLabel ?? props.alt}
ref={actualRef}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={sources}
internal_analyticTag={analyticTag}
/>
);
}}
</ImageAnalyticsTagContext.Consumer>
);
const analyticTag = use(ImageAnalyticsTagContext);
if (analyticTag != null) {
resolvedProps.internal_analyticTag = analyticTag;
}

return <ImageViewNativeComponent {...resolvedProps} ref={actualRef} />;
};

const imageComponentDecorator = unstable_getImageComponentDecorator();
Expand Down
82 changes: 82 additions & 0 deletions packages/react-native/Libraries/Image/__tests__/Image-itest.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,88 @@ describe('<Image>', () => {
});
});

describe('accessibilityState', () => {
function getAccessibilityState(element: React.MixedElement) {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(element);
});

return root
.getRenderedOutput({props: ['accessibilityState']})
.toJSONObject().props.accessibilityState;
}

it('is not set when no state props are provided', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<Image />);
});

expect(
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
).toEqual(<rn-image />);
});

it('maps \'aria-busy\' to "busy"', () => {
expect(getAccessibilityState(<Image aria-busy={true} />)).toContain(
'busy:true',
);
});

it('maps \'aria-disabled\' to "disabled"', () => {
expect(getAccessibilityState(<Image aria-disabled={true} />)).toContain(
'disabled:true',
);
});

it('maps \'aria-expanded\' to "expanded"', () => {
expect(getAccessibilityState(<Image aria-expanded={true} />)).toContain(
'expanded:true',
);
});

it('maps \'aria-selected\' to "selected"', () => {
expect(getAccessibilityState(<Image aria-selected={true} />)).toContain(
'selected:true',
);
});

describe('maps \'aria-checked\' to "checked"', () => {
it('when set to true', () => {
expect(
getAccessibilityState(<Image aria-checked={true} />),
).toContain('checked:Checked');
});

it('when set to false', () => {
expect(
getAccessibilityState(<Image aria-checked={false} />),
).toContain('checked:Unchecked');
});

it("when set to 'mixed'", () => {
expect(
getAccessibilityState(<Image aria-checked="mixed" />),
).toContain('checked:Mixed');
});
});

it('gives `aria-*` precedence over the matching field', () => {
const accessibilityState = getAccessibilityState(
<Image
accessibilityState={{busy: false, disabled: true}}
aria-busy={true}
/>,
);

expect(accessibilityState).toContain('busy:true');
expect(accessibilityState).toContain('disabled:true');
});
});

component TestComponent(testID?: ?string, ...props: AccessibilityProps) {
return <Image {...props} testID={testID} source={LOGO_SOURCE} />;
}
Expand Down
Loading