[MM-60213] Migrate tooltips of "components/toast" to WithTooltip (#28001)

Этот коммит содержится в:
M-ZubairAhmed
2024-08-22 14:12:25 +00:00
коммит произвёл GitHub
родитель 21b923b554
Коммит c67a5b10e6
12 изменённых файлов: 333 добавлений и 376 удалений

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

@@ -71,7 +71,6 @@ export function TeamProfile({team, isArchived, onToggleArchive, isDisabled, save
hint={defineMessage({id: 'workspace_limits.teams_limit_reached.tool_tip', defaultMessage: 'You\'ve reached the team limit for your current plan. Consider upgrading to unarchive this team or archive your other teams'})}
placement='bottom'
>
{/* OverlayTrigger doesn't play nicely with `disabled` buttons, because the :hover events don't fire. This is a workaround to ensure the popover appears see: https://github.com/react-bootstrap/react-bootstrap/issues/1588*/}
<div
className={'disabled-overlay-wrapper'}
>

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

@@ -24,6 +24,8 @@ import type {LinkInfo} from '../types';
import './file_preview_modal_main_actions.scss';
const COPIED_TOOLTIP_DURATION = 2000;
interface Props {
usedInside?: 'Header' | 'Footer';
showOnlyClose?: boolean;
@@ -52,6 +54,15 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
dispatch(getFilePublicLink(props.fileInfo.id));
}
}, [props.fileInfo, props.enablePublicLink]);
useEffect(() => {
if (publicLinkCopied) {
setTimeout(() => {
setPublicLinkCopied(false);
}, COPIED_TOOLTIP_DURATION);
}
}, [publicLinkCopied]);
const copyPublicLink = () => {
copyToClipboard(selectedFilePublicLink ?? '');
setPublicLinkCopied(true);
@@ -96,8 +107,6 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
key='filePreviewPublicLink'
placement={tooltipPlacement}
title={publicTooltipMessage}
shouldUpdatePosition={true}
onExit={() => setPublicLinkCopied(false)}
>
<a
href='#'

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

@@ -17,12 +17,6 @@ type State = {
reactedClass: 'Reaction--reacted' | 'Reaction--reacting' | 'Reaction--unreacted' | 'Reaction--unreacting';
};
declare module 'react-bootstrap/lib/OverlayTrigger' {
interface OverlayTriggerProps {
shouldUpdatePosition?: boolean;
}
}
type Props = {
/*

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

@@ -1,265 +1,267 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/Toast should match snapshot for hiding toast 1`] = `
<div
className="toast"
>
<div>
<div
className="toast__message toast__pointer"
onClick={[MockFunction]}
class="toast"
>
<div
className="toast__jump"
class="toast__message toast__pointer"
>
<UnreadBelowIcon />
Jump to recents
<div
class="toast__jump"
>
<span>
<svg
aria-label="Down Arrow Icon"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.696 2H7.184V11L3.062 6.878L2 7.94L7.94 13.88L13.88 7.94L12.818 6.878L8.696 11V2Z"
/>
</svg>
</span>
Jump to recents
</div>
<span>
child
</span>
</div>
<span>
child
</span>
</div>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={<div />}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="toast__dismiss"
class="toast__dismiss"
data-testid="dismissToast"
onClick={[Function]}
>
<CloseIcon
className="close-btn"
<span
class="close-btn"
id="dismissToast"
/>
>
<svg
aria-label="Close Icon"
height="24px"
role="img"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M18 7.209L16.791 6 12 10.791 7.209 6 6 7.209 10.791 12 6 16.791 7.209 18 12 13.209 16.791 18 18 16.791 13.209 12z"
fill-rule="nonzero"
/>
</svg>
</span>
</div>
</OverlayTrigger>
</div>
</div>
`;
exports[`components/Toast should match snapshot for showing toast 1`] = `
<div
className="toast toast__visible"
>
<div>
<div
className="toast__message toast__pointer"
onClick={[MockFunction]}
class="toast toast__visible"
>
<div
className="toast__jump"
class="toast__message toast__pointer"
>
<UnreadBelowIcon />
Jump to recents
</div>
<span>
child
</span>
</div>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="toast-close__tooltip"
<div
class="toast__jump"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="general_button.close"
/>
<div
className="tooltip__shortcut--txt"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="esc"
id="general_button.esc"
/>
</div>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="toast__dismiss"
data-testid="dismissToast"
onClick={[Function]}
>
<CloseIcon
className="close-btn"
id="dismissToast"
/>
<span>
<svg
aria-label="Down Arrow Icon"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.696 2H7.184V11L3.062 6.878L2 7.94L7.94 13.88L13.88 7.94L12.818 6.878L8.696 11V2Z"
/>
</svg>
</span>
Jump to recents
</div>
<span>
child
</span>
</div>
</OverlayTrigger>
<div
class="toast__dismiss"
data-testid="dismissToast"
>
<span
class="close-btn"
id="dismissToast"
>
<svg
aria-label="Close Icon"
height="24px"
role="img"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M18 7.209L16.791 6 12 10.791 7.209 6 6 7.209 10.791 12 6 16.791 7.209 18 12 13.209 16.791 18 18 16.791 13.209 12z"
fill-rule="nonzero"
/>
</svg>
</span>
</div>
</div>
</div>
`;
exports[`components/Toast should match snapshot for toast width less than 780px 1`] = `
<div
className="toast toast__visible"
>
<div>
<div
className="toast__message toast__pointer"
onClick={[MockFunction]}
class="toast toast__visible"
>
<div
className="toast__jump"
class="toast__message toast__pointer"
>
<UnreadBelowIcon />
Jump to recents
</div>
<span>
child
</span>
</div>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="toast-close__tooltip"
<div
class="toast__jump"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="general_button.close"
/>
<div
className="tooltip__shortcut--txt"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="esc"
id="general_button.esc"
/>
</div>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="toast__dismiss"
data-testid="dismissToast"
onClick={[Function]}
>
<CloseIcon
className="close-btn"
id="dismissToast"
/>
<span>
<svg
aria-label="Down Arrow Icon"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.696 2H7.184V11L3.062 6.878L2 7.94L7.94 13.88L13.88 7.94L12.818 6.878L8.696 11V2Z"
/>
</svg>
</span>
Jump to recents
</div>
<span>
child
</span>
</div>
</OverlayTrigger>
<div
class="toast__dismiss"
data-testid="dismissToast"
>
<span
class="close-btn"
id="dismissToast"
>
<svg
aria-label="Close Icon"
height="24px"
role="img"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M18 7.209L16.791 6 12 10.791 7.209 6 6 7.209 10.791 12 6 16.791 7.209 18 12 13.209 16.791 18 18 16.791 13.209 12z"
fill-rule="nonzero"
/>
</svg>
</span>
</div>
</div>
</div>
`;
exports[`components/Toast should match snapshot to have extraClasses 1`] = `
<div
className="toast extraClasses toast__visible"
>
<div>
<div
className="toast__message toast__pointer"
onClick={[MockFunction]}
class="toast toast__visible extraClasses"
>
<div
className="toast__jump"
class="toast__message toast__pointer"
>
<UnreadBelowIcon />
Jump to recents
</div>
<span>
child
</span>
</div>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="toast-close__tooltip"
<div
class="toast__jump"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="general_button.close"
/>
<div
className="tooltip__shortcut--txt"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="esc"
id="general_button.esc"
/>
</div>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="toast__dismiss"
data-testid="dismissToast-extraClasses"
onClick={[Function]}
>
<CloseIcon
className="close-btn"
id="dismissToast"
/>
<span>
<svg
aria-label="Down Arrow Icon"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.696 2H7.184V11L3.062 6.878L2 7.94L7.94 13.88L13.88 7.94L12.818 6.878L8.696 11V2Z"
/>
</svg>
</span>
Jump to recents
</div>
<span>
child
</span>
</div>
</OverlayTrigger>
<div
class="toast__dismiss"
data-testid="dismissToast-extraClasses"
>
<span
class="close-btn"
id="dismissToast"
>
<svg
aria-label="Close Icon"
height="24px"
role="img"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M18 7.209L16.791 6 12 10.791 7.209 6 6 7.209 10.791 12 6 16.791 7.209 18 12 13.209 16.791 18 18 16.791 13.209 12z"
fill-rule="nonzero"
/>
</svg>
</span>
</div>
</div>
</div>
`;
exports[`components/Toast should match snapshot to not have actions 1`] = `
<div
className="toast toast__visible"
>
<div>
<div
className="toast__message"
>
<span>
child
</span>
</div>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={<div />}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
class="toast toast__visible"
>
<div
className="toast__dismiss"
data-testid="dismissToast"
onClick={[Function]}
class="toast__message"
>
<CloseIcon
className="close-btn"
id="dismissToast"
/>
<span>
child
</span>
</div>
</OverlayTrigger>
<div
class="toast__dismiss"
data-testid="dismissToast"
>
<span
class="close-btn"
id="dismissToast"
>
<svg
aria-label="Close Icon"
height="24px"
role="img"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M18 7.209L16.791 6 12 10.791 7.209 6 6 7.209 10.791 12 6 16.791 7.209 18 12 13.209 16.791 18 18 16.791 13.209 12z"
fill-rule="nonzero"
/>
</svg>
</span>
</div>
</div>
</div>
`;

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

@@ -1,11 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import * as Utils from 'utils/utils';
import Toast from './toast';
import type {Props} from './toast';
@@ -15,30 +14,34 @@ describe('components/Toast', () => {
onClick: jest.fn(),
show: true,
showActions: true,
onClickMessage: Utils.localizeMessage('postlist.toast.scrollToBottom', 'Jump to recents'),
onClickMessage: (
<FormattedMessage
id='postlist.toast.scrollToBottom'
defaultMessage='Jump to recents'
/>
),
width: 1000,
};
test('should match snapshot for showing toast', () => {
const wrapper = shallow<Toast>(<Toast {...defaultProps}><span>{'child'}</span></Toast>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<Toast {...defaultProps}><span>{'child'}</span></Toast>);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('dismissToast')).toBeInTheDocument();
});
test('should match snapshot for hiding toast', () => {
const wrapper = shallow<Toast>(<Toast {...{...defaultProps, show: false}}><span>{'child'}</span></Toast>);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.toast__visible').length).toBe(0);
const {container} = renderWithContext(<Toast {...{...defaultProps, show: false}}><span>{'child'}</span></Toast>);
expect(container).toMatchSnapshot();
});
test('should match snapshot for toast width less than 780px', () => {
const wrapper = shallow<Toast>(<Toast {...{...defaultProps, width: 779}}><span>{'child'}</span></Toast>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<Toast {...{...defaultProps, width: 779}}><span>{'child'}</span></Toast>);
expect(container).toMatchSnapshot();
});
test('should match snapshot to not have actions', () => {
const wrapper = shallow<Toast>(<Toast {...{...defaultProps, showActions: false}}><span>{'child'}</span></Toast>);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.toast__pointer').length).toBe(0);
const {container} = renderWithContext(<Toast {...{...defaultProps, showActions: false}}><span>{'child'}</span></Toast>);
expect(container).toMatchSnapshot();
});
test('should dismiss', () => {
@@ -56,7 +59,7 @@ describe('components/Toast', () => {
});
test('should match snapshot to have extraClasses', () => {
const wrapper = shallow<Toast>(<Toast {...{...defaultProps, extraClasses: 'extraClasses'}}><span>{'child'}</span></Toast>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<Toast {...{...defaultProps, extraClasses: 'extraClasses'}}><span>{'child'}</span></Toast>);
expect(container).toMatchSnapshot();
});
});

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

@@ -1,16 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import type {ReactNode, MouseEventHandler} from 'react';
import type {OverlayTriggerProps} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import CloseIcon from 'components/widgets/icons/close_icon';
import UnreadAboveIcon from 'components/widgets/icons/unread_above_icon';
import UnreadBelowIcon from 'components/widgets/icons/unread_below_icon';
import WithTooltip from 'components/with_tooltip';
import Constants from 'utils/constants';
@@ -18,111 +17,88 @@ import './toast.scss';
export type Props = {
onClick?: MouseEventHandler<HTMLDivElement>;
onClickMessage?: string;
onClickMessage?: ReactNode;
onDismiss?: () => void;
children?: ReactNode;
show: boolean;
showActions?: boolean; //used for showing jump actions
width: number;
extraClasses?: string;
overlayPlacement?: OverlayTriggerProps['placement'];
overlayPlacement?: string;
jumpDirection?: 'up' | 'down';
}
};
export default class Toast extends React.PureComponent<Props> {
private mounted!: boolean;
static defaultProps = {
overlayPlacement: 'bottom',
jumpDirection: 'down',
};
componentDidMount() {
this.mounted = true;
export default function Toast({
onClick,
onClickMessage,
onDismiss,
children,
show,
showActions,
width,
extraClasses = '',
overlayPlacement = 'bottom',
jumpDirection = 'down',
}: Props) {
function handleDismiss() {
if (typeof onDismiss === 'function') {
onDismiss();
}
}
componentWillUnmount() {
this.mounted = false;
}
const toastClass = classNames('toast', {
toast__visible: show,
[extraClasses]: extraClasses.length > 0,
});
handleDismiss = () => {
if (typeof this.props.onDismiss == 'function') {
this.props.onDismiss();
}
};
const toastActionClass = classNames('toast__message', {
toast__pointer: showActions,
});
render() {
let toastClass = 'toast';
const {show, extraClasses, showActions, width, overlayPlacement, jumpDirection} = this.props;
if (extraClasses) {
toastClass += ` ${extraClasses}`;
}
if (show) {
toastClass += ' toast__visible';
}
let toastActionClass = 'toast__message';
if (showActions) {
toastActionClass += ' toast__pointer';
}
const jumpSection = () => {
return (
<div
className='toast__jump'
>
{jumpDirection === 'down' ? <UnreadBelowIcon/> : <UnreadAboveIcon/>}
{width > Constants.MOBILE_SCREEN_WIDTH && this.props.onClickMessage}
</div>
);
};
let closeTooltip = (<div/>);
if (showActions && show) {
closeTooltip = (
<Tooltip id='toast-close__tooltip'>
<FormattedMessage
id='general_button.close'
defaultMessage='Close'
/>
<div className='tooltip__shortcut--txt'>
<FormattedMessage
id='general_button.esc'
defaultMessage='esc'
/>
return (
<div className={toastClass}>
<div
className={toastActionClass}
onClick={showActions ? onClick : undefined}
>
{showActions && (
<div className='toast__jump'>
{jumpDirection === 'down' ? (<UnreadBelowIcon/>) : (<UnreadAboveIcon/>)}
{width > Constants.MOBILE_SCREEN_WIDTH && onClickMessage}
</div>
</Tooltip>
);
}
return (
<div className={toastClass}>
<div
className={toastActionClass}
onClick={showActions ? this.props.onClick : undefined}
>
{showActions && jumpSection()}
{this.props.children}
</div>
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
placement={overlayPlacement}
overlay={closeTooltip}
>
<div
className='toast__dismiss'
onClick={this.handleDismiss}
data-testid={extraClasses ? `dismissToast-${extraClasses}` : 'dismissToast'}
>
<CloseIcon
className='close-btn'
id='dismissToast'
/>
</div>
</OverlayTrigger>
)}
{children}
</div>
);
}
<WithTooltip
id='toast-close__tooltip'
placement={overlayPlacement}
title={
<>
<FormattedMessage
id='general_button.close'
defaultMessage='Close'
/>
<div className='tooltip__shortcut--txt'>
<FormattedMessage
id='general_button.esc'
defaultMessage='esc'
/>
</div>
</>
}
disabled={!showActions || !show}
>
<div
className='toast__dismiss'
onClick={handleDismiss}
data-testid={extraClasses ? `dismissToast-${extraClasses}` : 'dismissToast'}
>
<CloseIcon
className='close-btn'
id='dismissToast'
/>
</div>
</WithTooltip>
</div>
);
}

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

@@ -393,7 +393,12 @@ export class ToastWrapperClass extends React.PureComponent<Props, State> {
width,
onDismiss: this.hideUnreadToast,
onClick: this.scrollToLatestMessages,
onClickMessage: localizeMessage('postlist.toast.scrollToBottom', 'Jump to recents'),
onClickMessage: (
<FormattedMessage
id='postlist.toast.scrollToBottom'
defaultMessage='Jump to recents'
/>
),
showActions: !atLatestPost || (atLatestPost && (atBottom === false)),
};
@@ -410,7 +415,12 @@ export class ToastWrapperClass extends React.PureComponent<Props, State> {
width,
onDismiss: this.hideUnreadWithBottomStartToast,
onClick: this.scrollToUnreadMessages,
onClickMessage: localizeMessage('postlist.toast.scrollToUnread', 'Jump to unreads'),
onClickMessage: (
<FormattedMessage
id='postlist.toast.scrollToUnread'
defaultMessage='Jump to unreads'
/>
),
showActions: true,
jumpDirection: 'up' as const,
};
@@ -427,7 +437,12 @@ export class ToastWrapperClass extends React.PureComponent<Props, State> {
const showNewMessagesToastOverrides = {
onDismiss: this.hideNewMessagesToast,
onClick: this.scrollToNewMessage,
onClickMessage: localizeMessage('postlist.toast.scrollToLatest', 'Jump to new messages'),
onClickMessage: (
<FormattedMessage
id='postlist.toast.scrollToLatest'
defaultMessage='Jump to new messages'
/>
),
};
return (

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

@@ -3,5 +3,4 @@
export {
default,
useSynchronizedImmediate,
} from './simple_tooltip';

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useMemo} from 'react';
import React from 'react';
import type {ReactNode, ComponentProps, CSSProperties} from 'react';
import OverlayTrigger from 'components/overlay_trigger';
@@ -48,16 +48,3 @@ const SimpleTooltip = ({
};
export default SimpleTooltip;
export function useSynchronizedImmediate(): [Partial<ComponentProps<typeof SimpleTooltip>>, (isImmediate: boolean) => void] {
const [isImmediate, setImmediate] = useState(false);
return [
useMemo((): Partial<ComponentProps<typeof SimpleTooltip>> => ({
onEntered: () => setImmediate(true),
animation: !isImmediate,
delayShow: isImmediate ? 0 : undefined,
}), [isImmediate, setImmediate]),
setImmediate,
];
}

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

@@ -20,7 +20,6 @@ export type CommonTooltipProps = {
shortcut?: ShortcutDefinition;
emoji?: string;
emojiStyle?: EmojiStyle;
shouldUpdatePosition?: boolean;
}
export function createTooltip(commonTooltipProps: CommonTooltipProps) {

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

@@ -18,7 +18,6 @@ type WithTooltipProps = {
placement: OverlayTriggerProps['placement'];
onShow?: () => void;
delayHide?: number;
onExit?: () => void;
disabled?: boolean;
} & CommonTooltipProps;
const WithTooltip = ({
@@ -32,8 +31,6 @@ const WithTooltip = ({
onShow,
delayHide,
children,
onExit,
shouldUpdatePosition,
disabled = false,
}: WithTooltipProps) => {
const ThisTooltip = useMemo(() => createTooltip({
@@ -52,8 +49,6 @@ const WithTooltip = ({
placement={placement}
onEnter={onShow}
delayHide={delayHide}
onExit={onExit}
shouldUpdatePosition={shouldUpdatePosition}
disabled={disabled}
>
{children}

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

@@ -1,21 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Here we are extending the Interface defined by @types/react-bootstrap to include out own
// additional types, this soon will be not needed when we upgrade React-Bootstrap version to latest
// Until that happens this is the fix
import * as React from 'react';
import type {OverlayTriggerProps} from 'react-bootstrap';
export interface AdditionalOverlayTriggerProps extends React.ComponentPropsWithRef<typeof OverlayTriggerProps> {
className?: string;
overlay: any;
}
declare class OverlayTrigger extends React.Component<AdditionalOverlayTriggerProps> {}
declare module 'react-bootstrap' {
export {OverlayTrigger};
}