[MM-47086] Migrate "components/size_aware_image.jsx" and tests to Typescript (#25230)

Этот коммит содержится в:
Sudhanva-Nadiger
2023-12-30 18:22:27 +05:30
коммит произвёл GitHub
родитель fb1038548e
Коммит ef0c5e2125
9 изменённых файлов: 142 добавлений и 109 удалений

Просмотреть файл

@@ -41,10 +41,10 @@ exports[`components/SizeAwareImage should load download and copy link buttons wh
onKeyDown={[Function]}
onLoad={[Function]}
src="https://example.com/image.png"
tabIndex="0"
tabIndex={0}
/>
<span
className="image-preview-utility-buttons-container"
className="image-preview-utility-buttons-container image-preview-utility-buttons-container--small-image"
>
<OverlayTrigger
className="hidden-xs"
@@ -167,10 +167,10 @@ exports[`components/SizeAwareImage should match snapshot when handleSmallImageCo
onKeyDown={[Function]}
onLoad={[Function]}
src="https://example.com/image.png"
tabIndex="0"
tabIndex={0}
/>
<span
className="image-preview-utility-buttons-container"
className="image-preview-utility-buttons-container image-preview-utility-buttons-container--small-image"
>
<OverlayTrigger
className="hidden-xs"
@@ -306,10 +306,10 @@ exports[`components/SizeAwareImage should render a placeholder and has loader wh
onKeyDown={[Function]}
onLoad={[Function]}
src="https://example.com/image.png"
tabIndex="0"
tabIndex={0}
/>
<span
className="image-preview-utility-buttons-container"
className="image-preview-utility-buttons-container image-preview-utility-buttons-container--small-image"
>
<OverlayTrigger
className="hidden-xs"

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import type {MouseEvent} from 'react';
import type {KeyboardEvent, MouseEvent} from 'react';
import type {Post, PostImage} from '@mattermost/types/posts';
@@ -86,7 +86,7 @@ export default class MarkdownImage extends PureComponent<Props, State> {
return index > 0 ? url.substring(index + 1) : null;
};
showModal = (e: MouseEvent<HTMLImageElement>, link: string) => {
showModal = (e: KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLElement>, link = '') => {
const extension = this.getFileExtensionFromUrl(link);
if (!this.props.imageIsLink && extension) {

Просмотреть файл

@@ -114,7 +114,7 @@ describe('components/post_view/MessageAttachment', () => {
<MessageAttachment {...props}/>,
);
wrapper.instance().showModal({preventDefault: () => {}}, 'https://example.com/image.png');
wrapper.instance().showModal({preventDefault: () => {}} as unknown as React.KeyboardEvent<HTMLImageElement> | React.MouseEvent<HTMLElement, MouseEvent>, 'https://example.com/image.png');
expect(props.actions.openModal).toHaveBeenCalledTimes(1);
});

Просмотреть файл

@@ -3,7 +3,7 @@
import truncate from 'lodash/truncate';
import React from 'react';
import type {CSSProperties} from 'react';
import type {KeyboardEvent, MouseEvent, CSSProperties} from 'react';
import type {PostAction, PostActionOption} from '@mattermost/types/integration_actions';
import type {
@@ -312,14 +312,14 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
);
};
handleFormattedTextClick = (e: React.MouseEvent) => Utils.handleFormattedTextClick(e, this.props.currentRelativeTeamUrl);
handleFormattedTextClick = (e: MouseEvent) => Utils.handleFormattedTextClick(e, this.props.currentRelativeTeamUrl);
getFileExtensionFromUrl = (url: string) => {
const index = url.lastIndexOf('.');
return index > 0 ? url.substring(index + 1) : null;
};
showModal = (e: {preventDefault: () => void}, link: string) => {
showModal = (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLElement>), link = '') => {
e.preventDefault();
const extension = this.getFileExtensionFromUrl(link);

Просмотреть файл

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import type {KeyboardEvent, MouseEvent} from 'react';
import type {Post, PostImage as PostImageMetadata} from '@mattermost/types/posts';
@@ -23,7 +24,7 @@ interface Props {
}
export default class PostImage extends React.PureComponent<Props> {
showModal = (e: React.MouseEvent, link: string) => {
showModal = (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLElement>), link = '') => {
e.preventDefault();
this.props.actions.openModal({

Просмотреть файл

@@ -85,7 +85,7 @@ describe('components/SingleImageView', () => {
<SingleImageView {...baseProps}/>,
);
expect(wrapper.state('loaded')).toEqual(false);
wrapper.find(SizeAwareImage).prop('onImageLoaded')();
wrapper.find(SizeAwareImage).prop('onImageLoaded')?.({height: 0, width: 0});
expect(wrapper.state('loaded')).toEqual(true);
expect(wrapper).toMatchSnapshot();
});

Просмотреть файл

@@ -3,6 +3,7 @@
import classNames from 'classnames';
import React from 'react';
import type {KeyboardEvent, MouseEvent} from 'react';
import type {FileInfo} from '@mattermost/types/files';
@@ -82,7 +83,7 @@ export default class SingleImageView extends React.PureComponent<Props, State> {
}
};
handleImageClick = (e: React.MouseEvent<HTMLDivElement>) => {
handleImageClick = (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLDivElement | HTMLImageElement>)) => {
e.preventDefault();
this.props.actions.openModal({

Просмотреть файл

@@ -7,11 +7,13 @@ import {Provider} from 'react-redux';
import LoadingImagePreview from 'components/loading_image_preview';
import SizeAwareImage from 'components/size_aware_image';
import type {Props} from 'components/size_aware_image';
import mockStore from 'tests/test_store';
import {TestHelper} from 'utils/test_helper';
describe('components/SizeAwareImage', () => {
const baseProps = {
const baseProps: Props = {
dimensions: {
height: 200,
width: 300,
@@ -21,9 +23,9 @@ describe('components/SizeAwareImage', () => {
getFilePublicLink: jest.fn().mockReturnValue(Promise.resolve({data: {link: 'https://example.com/image.png'}})),
src: 'https://example.com/image.png',
className: 'class',
fileInfo: {
fileInfo: TestHelper.getFileInfoMock({
name: 'photo-1533709752211-118fcaf03312',
},
}),
enablePublicLink: true,
};
@@ -69,11 +71,11 @@ describe('components/SizeAwareImage', () => {
test('should render a mini preview when showLoader is true and preview is set', () => {
const props = {
...baseProps,
fileInfo: {
fileInfo: TestHelper.getFileInfoMock({
...baseProps.fileInfo,
mime_type: 'mime_type',
mini_preview: 'mini_preview',
},
}),
};
const wrapper = mount(<Provider store={store}><SizeAwareImage {...props}/></Provider>);
@@ -108,17 +110,22 @@ describe('components/SizeAwareImage', () => {
const height = 123;
const width = 1234;
const wrapper = shallow(<SizeAwareImage {...baseProps}/>);
const wrapper = shallow<SizeAwareImage>(<SizeAwareImage {...baseProps}/>);
wrapper.find('img').prop('onLoad')({target: {naturalHeight: height, naturalWidth: width}});
wrapper.find('img')?.prop('onLoad')?.({target: {naturalHeight: height, naturalWidth: width}} as unknown as React.SyntheticEvent<HTMLImageElement>);
expect(wrapper.state('loaded')).toBe(true);
expect(baseProps.onImageLoaded).toHaveBeenCalledWith({height, width});
});
test('should call onImageLoadFail when image load fails and should have svg', () => {
const wrapper = mount(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
wrapper.find(SizeAwareImage).find('img').prop('onError')();
const errorEvent = {
target: {},
currentTarget: {},
preventDefault: () => {},
stopPropagation: () => {},
} as React.SyntheticEvent<HTMLImageElement>;
wrapper.find(SizeAwareImage).find('img').prop('onError')?.(errorEvent);
expect(wrapper.find(SizeAwareImage).state('error')).toBe(true);
expect(wrapper.find(SizeAwareImage).find('svg').exists()).toEqual(true);
@@ -147,7 +154,7 @@ describe('components/SizeAwareImage', () => {
expect(wrapper.find('div.small-image__container').exists()).toEqual(true);
expect(wrapper.find('div.small-image__container').prop('className')).
toEqual('small-image__container cursor--pointer a11y--active');
toEqual('small-image__container cursor--pointer a11y--active small-image__container--min-width');
});
test('should properly set container div width', () => {

Просмотреть файл

@@ -4,12 +4,15 @@
/* eslint-disable @mattermost/use-external-link */
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import type {KeyboardEvent, MouseEvent, SyntheticEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import {DownloadOutlineIcon, LinkVariantIcon, CheckIcon} from '@mattermost/compass-icons/components';
import type {FileInfo} from '@mattermost/types/files';
import type {PostImage} from '@mattermost/types/posts';
import type {ActionFunc} from 'mattermost-redux/types/actions';
import {getFileMiniPreviewUrl} from 'mattermost-redux/utils/file_utils';
import LoadingImagePreview from 'components/loading_image_preview';
@@ -23,83 +26,103 @@ const MIN_IMAGE_SIZE = 48;
const MIN_IMAGE_SIZE_FOR_INTERNAL_BUTTONS = 100;
const MAX_IMAGE_HEIGHT = 350;
export type Props = {
/*
* The source URL of the image
*/
src: string;
/*
* dimensions object to create empty space required to prevent scroll pop
*/
dimensions?: Partial<PostImage>;
fileInfo?: FileInfo;
/**
* fileURL of the original image
*/
fileURL?: string;
alt?: string;
height?: string;
width?: string;
title?: string;
/*
* Boolean value to pass for showing a loader when image is being loaded
*/
showLoader?: boolean;
/*
* A callback that is called as soon as the image component has a height value
*/
onImageLoaded?: ({height, width}: {height: number; width: number}) => void;
/*
* A callback that is called when image load fails
*/
onImageLoadFail?: () => void;
/*
* Fetch the onClick function
*/
onClick?: (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLImageElement | HTMLDivElement>), link?: string) => void;
/*
* css classes that can added to the img as well as parent div on svg for placeholder
*/
className?: string;
/*
* Enables the logic of surrounding small images with a bigger container div for better click/tap targeting
*/
handleSmallImageContainer?: boolean;
/**
* Enables copy URL functionality through a button on image hover.
*/
enablePublicLink?: boolean;
/**
* Action to fetch public link of an image from server.
*/
getFilePublicLink?: () => ActionFunc;
/*
* Prevents display of utility buttons when image in a location that makes them inappropriate
*/
hideUtilities?: boolean;
}
type State = {
loaded: boolean;
isSmallImage: boolean;
linkCopiedRecently: boolean;
linkCopyInProgress: boolean;
error: boolean;
imageWidth: number;
}
// SizeAwareImage is a component used for rendering images where the dimensions of the image are important for
// ensuring that the page is laid out correctly.
export default class SizeAwareImage extends React.PureComponent {
static propTypes = {
export default class SizeAwareImage extends React.PureComponent<Props, State> {
public heightTimeout = 0;
public mounted = false;
public timeout: NodeJS.Timeout|null = null;
/*
* The source URL of the image
*/
src: PropTypes.string.isRequired,
/*
* dimensions object to create empty space required to prevent scroll pop
*/
dimensions: PropTypes.object,
fileInfo: PropTypes.object,
/**
* fileURL of the original image
*/
fileURL: PropTypes.string,
/*
* Boolean value to pass for showing a loader when image is being loaded
*/
showLoader: PropTypes.bool,
/*
* A callback that is called as soon as the image component has a height value
*/
onImageLoaded: PropTypes.func,
/*
* A callback that is called when image load fails
*/
onImageLoadFail: PropTypes.func,
/*
* Fetch the onClick function
*/
onClick: PropTypes.func,
/*
* css classes that can added to the img as well as parent div on svg for placeholder
*/
className: PropTypes.string,
/*
* Enables the logic of surrounding small images with a bigger container div for better click/tap targeting
*/
handleSmallImageContainer: PropTypes.bool,
/**
* Enables copy URL functionality through a button on image hover.
*/
enablePublicLink: PropTypes.bool,
/**
* Action to fetch public link of an image from server.
*/
getFilePublicLink: PropTypes.func,
/*
* Prevents display of utility buttons when image in a location that makes them inappropriate
*/
hideUtilities: PropTypes.bool,
};
constructor(props) {
constructor(props: Props) {
super(props);
const {dimensions} = props;
this.state = {
loaded: false,
isSmallImage: this.dimensionsAvailable(dimensions) ? this.isSmallImage(
dimensions.width, dimensions.height) : false,
dimensions?.width ?? 0, dimensions?.height ?? 0) : false,
linkCopiedRecently: false,
linkCopyInProgress: false,
error: false,
imageWidth: 0,
};
this.heightTimeout = 0;
@@ -113,17 +136,17 @@ export default class SizeAwareImage extends React.PureComponent {
this.mounted = false;
}
dimensionsAvailable = (dimensions) => {
dimensionsAvailable = (dimensions?: Partial<PostImage>) => {
return dimensions && dimensions.width && dimensions.height;
};
isSmallImage = (width, height) => {
isSmallImage = (width: number, height: number) => {
return width < MIN_IMAGE_SIZE || height < MIN_IMAGE_SIZE;
};
handleLoad = (event) => {
handleLoad = (event: SyntheticEvent<HTMLImageElement, Event>) => {
if (this.mounted) {
const image = event.target;
const image = event.target as HTMLImageElement;
const isSmallImage = this.isSmallImage(image.naturalWidth, image.naturalHeight);
this.setState({
loaded: true,
@@ -147,13 +170,13 @@ export default class SizeAwareImage extends React.PureComponent {
}
};
handleImageClick = (e) => {
handleImageClick = (e: MouseEvent<HTMLImageElement>) => {
this.props.onClick?.(e, this.props.src);
};
onEnterKeyDown = (e) => {
onEnterKeyDown = (e: KeyboardEvent<HTMLImageElement>) => {
if (e.key === 'Enter') {
this.handleImageClick(e);
this.props.onClick?.(e, this.props.src);
}
};
@@ -197,7 +220,7 @@ export default class SizeAwareImage extends React.PureComponent {
<img
{...props}
aria-label={ariaLabelImage}
tabIndex='0'
tabIndex={0}
onClick={this.handleImageClick}
onKeyDown={this.onEnterKeyDown}
className={
@@ -387,9 +410,9 @@ export default class SizeAwareImage extends React.PureComponent {
let fallback;
if (this.dimensionsAvailable(dimensions) && !this.state.loaded) {
const ratio = dimensions.height > MAX_IMAGE_HEIGHT ? MAX_IMAGE_HEIGHT / dimensions.height : 1;
const height = dimensions.height * ratio;
const width = dimensions.width * ratio;
const ratio = (dimensions?.height ?? 0) > MAX_IMAGE_HEIGHT ? MAX_IMAGE_HEIGHT / (dimensions?.height ?? 1) : 1;
const height = (dimensions?.height ?? 0) * ratio;
const width = (dimensions?.width ?? 0) * ratio;
const miniPreview = getFileMiniPreviewUrl(fileInfo);
@@ -397,13 +420,13 @@ export default class SizeAwareImage extends React.PureComponent {
fallback = (
<div
className={`image-loading__container ${this.props.className}`}
style={{maxWidth: dimensions.width}}
style={{maxWidth: dimensions?.width}}
>
<img
aria-label={ariaLabelImage}
className={this.props.className}
src={miniPreview}
tabIndex='0'
tabIndex={0}
height={height}
width={width}
/>
@@ -454,7 +477,7 @@ export default class SizeAwareImage extends React.PureComponent {
}, 1500);
};
copyLinkToAsset = () => {
copyLinkToAsset = async () => {
// if linkCopyInProgress is true return
if (this.state.linkCopyInProgress !== true) {
// set linkCopyInProgress to true to prevent multiple api calls
@@ -468,11 +491,12 @@ export default class SizeAwareImage extends React.PureComponent {
}
// copying public link to clipboard
this.props.getFilePublicLink().then((data) => {
const fileURL = data.data.link;
if (this.props.getFilePublicLink) {
const data: any = await this.props.getFilePublicLink();
const fileURL = data.data?.link;
copyToClipboard(fileURL ?? '');
this.startCopyTimer();
});
}
}
};