[MM-47086] Migrate "components/size_aware_image.jsx" and tests to Typescript (#25230)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
fb1038548e
Коммит
ef0c5e2125
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ссылка в новой задаче
Block a user