[MM-58441] Create a floating-ui Tooltip which will swap out ReactBootstrap tooltip (#29464)
Added a new tooltip - components/team_sidebar/components/team_button.tsx - components/post_view/reaction/reaction_tooltip/reaction_tooltip.tsx - components/post_view/post_recent_reactions/post_recent_reactions.tsx - components/common/comment_icon.tsx
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
cf219b0e06
Коммит
f80afad1b6
@@ -35,7 +35,7 @@ describe('Messaging', () => {
|
|||||||
|
|
||||||
it('MM-T133 Visual verification of tooltips on post hover menu', () => {
|
it('MM-T133 Visual verification of tooltips on post hover menu', () => {
|
||||||
cy.getLastPostId().then((postId) => {
|
cy.getLastPostId().then((postId) => {
|
||||||
verifyToolTip(postId, `#CENTER_button_${postId}`, 'More');
|
verifyToolTip(postId, `#CENTER_flagIcon_${postId}`, 'Save Message');
|
||||||
|
|
||||||
verifyToolTip(postId, `#CENTER_reaction_${postId}`, 'Add Reaction');
|
verifyToolTip(postId, `#CENTER_reaction_${postId}`, 'Add Reaction');
|
||||||
|
|
||||||
@@ -46,10 +46,10 @@ describe('Messaging', () => {
|
|||||||
function verifyToolTip(postId, targetElement, label) {
|
function verifyToolTip(postId, targetElement, label) {
|
||||||
cy.get(`#post_${postId}`).trigger('mouseover');
|
cy.get(`#post_${postId}`).trigger('mouseover');
|
||||||
|
|
||||||
cy.get(targetElement).trigger('mouseover', {force: true});
|
cy.get(targetElement).trigger('mouseenter', {force: true});
|
||||||
cy.findByText(label).should('be.visible');
|
cy.findByText(label).should('be.visible');
|
||||||
|
|
||||||
cy.get(targetElement).trigger('mouseout', {force: true});
|
cy.get(targetElement).trigger('mouseleave', {force: true});
|
||||||
cy.findByText(label).should('not.exist');
|
cy.findByText(label).should('not.exist');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "0.26.6",
|
"@floating-ui/react": "0.26.28",
|
||||||
"@giphy/js-fetch-api": "5.1.0",
|
"@giphy/js-fetch-api": "5.1.0",
|
||||||
"@giphy/react-components": "8.1.0",
|
"@giphy/react-components": "8.1.0",
|
||||||
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
|
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import WithTooltip from 'components/with_tooltip';
|
|||||||
import {AnnouncementBarTypes} from 'utils/constants';
|
import {AnnouncementBarTypes} from 'utils/constants';
|
||||||
import {isStringContainingUrl} from 'utils/url';
|
import {isStringContainingUrl} from 'utils/url';
|
||||||
|
|
||||||
|
import './default_announcement_bar.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: string;
|
id?: string;
|
||||||
showCloseButton: boolean;
|
showCloseButton: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#announcement-bar__tooltip {
|
||||||
|
width: 50%;
|
||||||
|
max-width: 100%;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {MouseEvent, ReactNode, RefObject} from 'react';
|
import type {MouseEvent, ReactNode, RefObject} from 'react';
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import type {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap';
|
||||||
import {Overlay} from 'react-bootstrap';
|
import {Overlay} from 'react-bootstrap';
|
||||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
@@ -18,7 +20,6 @@ import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
|||||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||||
import Markdown from 'components/markdown';
|
import Markdown from 'components/markdown';
|
||||||
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
|
||||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||||
import Timestamp from 'components/timestamp';
|
import Timestamp from 'components/timestamp';
|
||||||
import Popover from 'components/widgets/popover';
|
import Popover from 'components/widgets/popover';
|
||||||
@@ -44,6 +45,10 @@ import HeaderIconWrapper from './components/header_icon_wrapper';
|
|||||||
const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true};
|
const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true};
|
||||||
const popoverMarkdownOptions = {singleline: false, mentionHighlight: false, atMentions: true};
|
const popoverMarkdownOptions = {singleline: false, mentionHighlight: false, atMentions: true};
|
||||||
|
|
||||||
|
export type OverlayTrigger = BaseOverlayTrigger & {
|
||||||
|
hide: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
currentUser: UserProfile;
|
currentUser: UserProfile;
|
||||||
@@ -94,7 +99,7 @@ class ChannelHeader extends React.PureComponent<Props, State> {
|
|||||||
toggleFavoriteRef: RefObject<HTMLButtonElement>;
|
toggleFavoriteRef: RefObject<HTMLButtonElement>;
|
||||||
headerDescriptionRef: RefObject<HTMLSpanElement>;
|
headerDescriptionRef: RefObject<HTMLSpanElement>;
|
||||||
headerPopoverTextMeasurerRef: RefObject<HTMLDivElement>;
|
headerPopoverTextMeasurerRef: RefObject<HTMLDivElement>;
|
||||||
headerOverlayRef: RefObject<BaseOverlayTrigger>;
|
headerOverlayRef: RefObject<OverlayTrigger>;
|
||||||
getHeaderMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
getHeaderMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
||||||
getPopoverMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
getPopoverMarkdownOptions: (channelNamesMap: Record<string, any>) => Record<string, any>;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
import ReplyIcon from 'components/widgets/icons/reply_icon';
|
import ReplyIcon from 'components/widgets/icons/reply_icon';
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
|
||||||
|
|
||||||
import type {Locations} from 'utils/constants';
|
import type {Locations} from 'utils/constants';
|
||||||
|
|
||||||
@@ -43,8 +43,6 @@ const CommentIcon = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
id='comment-icon-tooltip'
|
|
||||||
placement='top'
|
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
id: 'post_info.comment_icon.tooltip.reply',
|
id: 'post_info.comment_icon.tooltip.reply',
|
||||||
defaultMessage: 'Reply',
|
defaultMessage: 'Reply',
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {mount} from 'enzyme';
|
|
||||||
import React from 'react';
|
|
||||||
import {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap'; // eslint-disable-line no-restricted-imports
|
|
||||||
import {FormattedMessage, IntlProvider} from 'react-intl';
|
|
||||||
import {Provider as ReduxProvider} from 'react-redux';
|
|
||||||
import type {Store} from 'redux';
|
|
||||||
|
|
||||||
import testConfigureStore from 'packages/mattermost-redux/test/test_store';
|
|
||||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
|
||||||
|
|
||||||
import OverlayTrigger from './overlay_trigger';
|
|
||||||
|
|
||||||
describe('OverlayTrigger', () => {
|
|
||||||
const testId = 'test.value';
|
|
||||||
|
|
||||||
let store: Store;
|
|
||||||
|
|
||||||
const intlProviderProps = {
|
|
||||||
defaultLocale: 'en',
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
[testId]: 'Actual value',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const baseProps = {
|
|
||||||
overlay: (
|
|
||||||
<FormattedMessage
|
|
||||||
id={testId}
|
|
||||||
defaultMessage='Default value'
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Intercept console error messages since we intentionally cause some as part of these tests
|
|
||||||
let originalConsoleError: () => void;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = testConfigureStore();
|
|
||||||
originalConsoleError = console.error;
|
|
||||||
console.error = jest.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
console.error = originalConsoleError;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('base OverlayTrigger should fail to pass intl to overlay', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<ReduxProvider store={store}>
|
|
||||||
<IntlProvider {...intlProviderProps}>
|
|
||||||
<BaseOverlayTrigger {...baseProps}>
|
|
||||||
<span/>
|
|
||||||
</BaseOverlayTrigger>
|
|
||||||
</IntlProvider>
|
|
||||||
</ReduxProvider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// console.error will have been called by FormattedMessage because its intl context is missing
|
|
||||||
expect(() => {
|
|
||||||
mount(wrapper.find(BaseOverlayTrigger).prop('overlay'));
|
|
||||||
}).toThrow('[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry.');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('custom OverlayTrigger should pass intl to overlay', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<ReduxProvider store={store}>
|
|
||||||
<IntlProvider {...intlProviderProps}>
|
|
||||||
<OverlayTrigger {...baseProps}>
|
|
||||||
<span/>
|
|
||||||
</OverlayTrigger>
|
|
||||||
</IntlProvider>
|
|
||||||
</ReduxProvider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const overlay = mount(wrapper.find(BaseOverlayTrigger).prop('overlay'));
|
|
||||||
|
|
||||||
expect(overlay.text()).toBe('Actual value');
|
|
||||||
expect(console.error).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ref should properly be forwarded', () => {
|
|
||||||
const ref = React.createRef<BaseOverlayTrigger>();
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
ref,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
|
||||||
<ReduxProvider store={store}>
|
|
||||||
<IntlProvider {...intlProviderProps}>
|
|
||||||
<OverlayTrigger {...props}>
|
|
||||||
<span/>
|
|
||||||
</OverlayTrigger>
|
|
||||||
</IntlProvider>
|
|
||||||
</ReduxProvider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(ref.current).toBe(wrapper.find(BaseOverlayTrigger).instance());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('style and className should correctly be passed to overlay', () => {
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
overlay: (
|
|
||||||
<span
|
|
||||||
className='test-overlay-className'
|
|
||||||
style={{backgroundColor: 'red'}}
|
|
||||||
>
|
|
||||||
{'test-overlay'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
defaultOverlayShown: true, // Make sure the overlay is visible
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mount(
|
|
||||||
<ReduxProvider store={store}>
|
|
||||||
<IntlProvider {...intlProviderProps}>
|
|
||||||
<OverlayTrigger {...props}>
|
|
||||||
<span/>
|
|
||||||
</OverlayTrigger>
|
|
||||||
</IntlProvider>
|
|
||||||
</ReduxProvider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dive into the react-bootstrap internals to find our overlay
|
|
||||||
const overlay = mount((wrapper.find(BaseOverlayTrigger).instance() as any)._overlay).find('span'); // eslint-disable-line no-underscore-dangle
|
|
||||||
|
|
||||||
// Confirm that we've found the right span
|
|
||||||
expect(overlay.exists()).toBe(true);
|
|
||||||
expect(overlay.text()).toBe('test-overlay');
|
|
||||||
|
|
||||||
// Confirm that our props are included
|
|
||||||
expect(overlay.prop('className')).toContain('test-overlay-className');
|
|
||||||
expect(overlay.prop('style')).toMatchObject({backgroundColor: 'red'});
|
|
||||||
|
|
||||||
// And confirm that react-bootstrap's props are included
|
|
||||||
expect(overlay.prop('className')).toContain('fade in');
|
|
||||||
expect(overlay.prop('placement')).toBe('right');
|
|
||||||
expect(overlay.prop('positionTop')).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('disabled and style should both be supported', () => {
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
overlay: (
|
|
||||||
<span
|
|
||||||
style={{backgroundColor: 'red'}}
|
|
||||||
>
|
|
||||||
{'test-overlay'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
defaultOverlayShown: true, // Make sure the overlay is visible
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mount(
|
|
||||||
<ReduxProvider store={store}>
|
|
||||||
<IntlProvider {...intlProviderProps}>
|
|
||||||
<OverlayTrigger {...props}>
|
|
||||||
<span/>
|
|
||||||
</OverlayTrigger>
|
|
||||||
</IntlProvider>
|
|
||||||
</ReduxProvider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dive into the react-bootstrap internals to find our overlay
|
|
||||||
const overlay = mount((wrapper.find(BaseOverlayTrigger).instance() as any)._overlay).find('span'); // eslint-disable-line no-underscore-dangle
|
|
||||||
|
|
||||||
// Confirm that we've found the right span
|
|
||||||
expect(overlay.exists()).toBe(true);
|
|
||||||
expect(overlay.text()).toBe('test-overlay');
|
|
||||||
|
|
||||||
// Confirm that our props are included
|
|
||||||
expect(overlay.prop('style')).toMatchObject({backgroundColor: 'red', visibility: 'hidden'});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`components/post_view/PostFlagIcon should match snapshot 1`] = `
|
exports[`components/post_view/PostFlagIcon should match snapshot 1`] = `
|
||||||
<WithTooltip
|
<Memo(WithTooltip)
|
||||||
id="flagTooltip"
|
|
||||||
key="flagtooltipkey"
|
key="flagtooltipkey"
|
||||||
placement="top"
|
|
||||||
title={
|
title={
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
defaultMessage="Save Message"
|
defaultMessage="Save Message"
|
||||||
@@ -22,14 +20,12 @@ exports[`components/post_view/PostFlagIcon should match snapshot 1`] = `
|
|||||||
className="icon icon--small"
|
className="icon icon--small"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</WithTooltip>
|
</Memo(WithTooltip)>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/post_view/PostFlagIcon should match snapshot 2`] = `
|
exports[`components/post_view/PostFlagIcon should match snapshot 2`] = `
|
||||||
<WithTooltip
|
<Memo(WithTooltip)
|
||||||
id="flagTooltip"
|
|
||||||
key="flagtooltipkeyflagged"
|
key="flagtooltipkeyflagged"
|
||||||
placement="top"
|
|
||||||
title={
|
title={
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
defaultMessage="Remove from Saved"
|
defaultMessage="Remove from Saved"
|
||||||
@@ -47,5 +43,5 @@ exports[`components/post_view/PostFlagIcon should match snapshot 2`] = `
|
|||||||
className="icon icon--small icon--small-filled post-menu__item--selected"
|
className="icon icon--small icon--small-filled post-menu__item--selected"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</WithTooltip>
|
</Memo(WithTooltip)>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
|||||||
|
|
||||||
import FlagIcon from 'components/widgets/icons/flag_icon';
|
import FlagIcon from 'components/widgets/icons/flag_icon';
|
||||||
import FlagIconFilled from 'components/widgets/icons/flag_icon_filled';
|
import FlagIconFilled from 'components/widgets/icons/flag_icon_filled';
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
|
||||||
|
|
||||||
import {Locations, A11yCustomEventTypes} from 'utils/constants';
|
import {Locations, A11yCustomEventTypes} from 'utils/constants';
|
||||||
|
|
||||||
@@ -82,9 +82,7 @@ const PostFlagIcon = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
id='flagTooltip'
|
|
||||||
key={`flagtooltipkey${isFlagged ? 'flagged' : ''}`}
|
key={`flagtooltipkey${isFlagged ? 'flagged' : ''}`}
|
||||||
placement='top'
|
|
||||||
title={
|
title={
|
||||||
isFlagged ? (
|
isFlagged ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
|
|||||||
target={[MockFunction]}
|
target={[MockFunction]}
|
||||||
topOffset={-7}
|
topOffset={-7}
|
||||||
/>
|
/>
|
||||||
<WithTooltip
|
<Memo(WithTooltip)
|
||||||
id="reaction-icon-tooltip"
|
|
||||||
placement="top"
|
|
||||||
title={
|
title={
|
||||||
Object {
|
Object {
|
||||||
"defaultMessage": "Add Reaction",
|
"defaultMessage": "Add Reaction",
|
||||||
@@ -38,6 +36,6 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
|
|||||||
className="icon icon--small"
|
className="icon icon--small"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</WithTooltip>
|
</Memo(WithTooltip)>
|
||||||
</Memo(ChannelPermissionGate)>
|
</Memo(ChannelPermissionGate)>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
|
|||||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
||||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||||
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
|
||||||
|
|
||||||
import {Locations} from 'utils/constants';
|
import {Locations} from 'utils/constants';
|
||||||
import {localizeMessage} from 'utils/utils';
|
import {localizeMessage} from 'utils/utils';
|
||||||
@@ -91,9 +91,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
|
|||||||
spaceRequiredBelow={spaceRequiredBelow}
|
spaceRequiredBelow={spaceRequiredBelow}
|
||||||
/>
|
/>
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
id='reaction-icon-tooltip'
|
|
||||||
title={messages.addReaction}
|
title={messages.addReaction}
|
||||||
placement='top'
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
data-testid='post-reaction-emoji-icon'
|
data-testid='post-reaction-emoji-icon'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Permissions from 'mattermost-redux/constants/permissions';
|
|||||||
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
|
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
|
||||||
|
|
||||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
|
||||||
|
|
||||||
import {Locations} from 'utils/constants';
|
import {Locations} from 'utils/constants';
|
||||||
|
|
||||||
@@ -92,20 +92,16 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
|
|||||||
permissions={[Permissions.ADD_REACTION]}
|
permissions={[Permissions.ADD_REACTION]}
|
||||||
>
|
>
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
id='post_info.emoji.tooltip'
|
|
||||||
title={this.emojiName(emoji, this.props.locale)}
|
title={this.emojiName(emoji, this.props.locale)}
|
||||||
emoji={getEmojiName(emoji)}
|
emoji={getEmojiName(emoji)}
|
||||||
emojiStyle='large'
|
isEmojiLarge={true}
|
||||||
placement='top'
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<>
|
<EmojiItem
|
||||||
<EmojiItem
|
emoji={emoji}
|
||||||
emoji={emoji}
|
onItemClick={this.handleToggleEmoji}
|
||||||
onItemClick={this.handleToggleEmoji}
|
order={n}
|
||||||
order={n}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
</ChannelPermissionGate>
|
</ChannelPermissionGate>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ exports[`components/post_view/Reaction should apply read-only class if user does
|
|||||||
canRemoveReactions={true}
|
canRemoveReactions={true}
|
||||||
currentUserReacted={false}
|
currentUserReacted={false}
|
||||||
emojiName="smile"
|
emojiName="smile"
|
||||||
id="post_id-smile-reaction"
|
|
||||||
onShow={[Function]}
|
onShow={[Function]}
|
||||||
reactions={
|
reactions={
|
||||||
Array [
|
Array [
|
||||||
@@ -73,7 +72,6 @@ exports[`components/post_view/Reaction should apply read-only class if user does
|
|||||||
canRemoveReactions={false}
|
canRemoveReactions={false}
|
||||||
currentUserReacted={true}
|
currentUserReacted={true}
|
||||||
emojiName="smile"
|
emojiName="smile"
|
||||||
id="post_id-smile-reaction"
|
|
||||||
onShow={[Function]}
|
onShow={[Function]}
|
||||||
reactions={
|
reactions={
|
||||||
Array [
|
Array [
|
||||||
@@ -140,7 +138,6 @@ exports[`components/post_view/Reaction should match snapshot 1`] = `
|
|||||||
canRemoveReactions={true}
|
canRemoveReactions={true}
|
||||||
currentUserReacted={false}
|
currentUserReacted={false}
|
||||||
emojiName="smile"
|
emojiName="smile"
|
||||||
id="post_id-smile-reaction"
|
|
||||||
onShow={[Function]}
|
onShow={[Function]}
|
||||||
reactions={
|
reactions={
|
||||||
Array [
|
Array [
|
||||||
@@ -207,7 +204,6 @@ exports[`components/post_view/Reaction should match snapshot when a current user
|
|||||||
canRemoveReactions={true}
|
canRemoveReactions={true}
|
||||||
currentUserReacted={true}
|
currentUserReacted={true}
|
||||||
emojiName="smile"
|
emojiName="smile"
|
||||||
id="post_id-smile-reaction"
|
|
||||||
onShow={[Function]}
|
onShow={[Function]}
|
||||||
reactions={
|
reactions={
|
||||||
Array [
|
Array [
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ export default class Reaction extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactionTooltip
|
<ReactionTooltip
|
||||||
id={`${this.props.post.id}-${this.props.emojiName}-reaction`}
|
|
||||||
canAddReactions={canAddReactions}
|
canAddReactions={canAddReactions}
|
||||||
canRemoveReactions={canRemoveReactions}
|
canRemoveReactions={canRemoveReactions}
|
||||||
currentUserReacted={currentUserReacted}
|
currentUserReacted={currentUserReacted}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
|
|||||||
|
|
||||||
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
|
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
|
||||||
|
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
canAddReactions: boolean;
|
canAddReactions: boolean;
|
||||||
@@ -14,7 +14,6 @@ type Props = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
currentUserReacted: boolean;
|
currentUserReacted: boolean;
|
||||||
emojiName: string;
|
emojiName: string;
|
||||||
id: string;
|
|
||||||
onShow: () => void;
|
onShow: () => void;
|
||||||
reactions: ReactionType[];
|
reactions: ReactionType[];
|
||||||
users: string[];
|
users: string[];
|
||||||
@@ -27,7 +26,6 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
|
|||||||
children,
|
children,
|
||||||
currentUserReacted,
|
currentUserReacted,
|
||||||
emojiName,
|
emojiName,
|
||||||
id,
|
|
||||||
onShow,
|
onShow,
|
||||||
reactions,
|
reactions,
|
||||||
users,
|
users,
|
||||||
@@ -101,7 +99,7 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltip = intl.formatMessage(
|
const tooltipTitle = intl.formatMessage(
|
||||||
{
|
{
|
||||||
id: 'reaction.reacted',
|
id: 'reaction.reacted',
|
||||||
defaultMessage: '{users} {reactionVerb} with {emoji}',
|
defaultMessage: '{users} {reactionVerb} with {emoji}',
|
||||||
@@ -113,14 +111,14 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let clickTooltip;
|
let tooltipHint;
|
||||||
if (currentUserReacted && canRemoveReactions) {
|
if (currentUserReacted && canRemoveReactions) {
|
||||||
clickTooltip = intl.formatMessage({
|
tooltipHint = intl.formatMessage({
|
||||||
id: 'reaction.clickToRemove',
|
id: 'reaction.clickToRemove',
|
||||||
defaultMessage: '(click to remove)',
|
defaultMessage: '(click to remove)',
|
||||||
});
|
});
|
||||||
} else if (!currentUserReacted && canAddReactions) {
|
} else if (!currentUserReacted && canAddReactions) {
|
||||||
clickTooltip = intl.formatMessage({
|
tooltipHint = intl.formatMessage({
|
||||||
id: 'reaction.clickToAdd',
|
id: 'reaction.clickToAdd',
|
||||||
defaultMessage: '(click to add)',
|
defaultMessage: '(click to add)',
|
||||||
});
|
});
|
||||||
@@ -128,13 +126,11 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
id={id}
|
title={tooltipTitle}
|
||||||
|
hint={tooltipHint}
|
||||||
emoji={emojiName}
|
emoji={emojiName}
|
||||||
emojiStyle='large'
|
isEmojiLarge={true}
|
||||||
placement='top'
|
onOpen={onShow}
|
||||||
title={tooltip}
|
|
||||||
hint={clickTooltip}
|
|
||||||
onShow={onShow}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function ProfilePopoverController<TriggerComponentType = HTMLSpanElement>
|
|||||||
</TriggerComponent>
|
</TriggerComponent>
|
||||||
|
|
||||||
{isMounted && (
|
{isMounted && (
|
||||||
<FloatingPortal id='user-profile-popover-portal'>
|
<FloatingPortal id='root-portal'>
|
||||||
<FloatingOverlay
|
<FloatingOverlay
|
||||||
id='user-profile-popover-floating-overlay'
|
id='user-profile-popover-floating-overlay'
|
||||||
className='user-profile-popover-floating-overlay'
|
className='user-profile-popover-floating-overlay'
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {Link} from 'react-router-dom';
|
|||||||
import {mark, trackEvent} from 'actions/telemetry_actions.jsx';
|
import {mark, trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
|
|
||||||
import TeamIcon from 'components/widgets/team_icon/team_icon';
|
import TeamIcon from 'components/widgets/team_icon/team_icon';
|
||||||
import WithTooltip from 'components/with_tooltip';
|
|
||||||
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
|
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
|
||||||
|
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
|
||||||
|
|
||||||
import {Mark} from 'utils/performance_telemetry';
|
import {Mark} from 'utils/performance_telemetry';
|
||||||
|
|
||||||
@@ -153,7 +153,6 @@ export default function TeamButton({
|
|||||||
<WithTeamTooltip
|
<WithTeamTooltip
|
||||||
order={order}
|
order={order}
|
||||||
tip={tip}
|
tip={tip}
|
||||||
url={url}
|
|
||||||
>
|
>
|
||||||
<div className={'team-btn ' + btnClass}>
|
<div className={'team-btn ' + btnClass}>
|
||||||
{!otherProps.isInProduct && badge}
|
{!otherProps.isInProduct && badge}
|
||||||
@@ -211,9 +210,8 @@ export default function TeamButton({
|
|||||||
function WithTeamTooltip({
|
function WithTeamTooltip({
|
||||||
order,
|
order,
|
||||||
tip,
|
tip,
|
||||||
url,
|
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<Pick<Props, 'order' | 'tip' | 'url'>>) {
|
}: React.PropsWithChildren<Pick<Props, 'order' | 'tip'>>) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const shortcut = useMemo(() => {
|
const shortcut = useMemo(() => {
|
||||||
@@ -229,10 +227,8 @@ function WithTeamTooltip({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTooltip
|
<WithTooltip
|
||||||
id={`tooltip-${url}`}
|
|
||||||
title={tip || intl.formatMessage(messages.nameUndefined)}
|
title={tip || intl.formatMessage(messages.nameUndefined)}
|
||||||
shortcut={shortcut}
|
shortcut={shortcut}
|
||||||
placement='right'
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function UserGroupPopoverController(props: Props) {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{isMounted && (
|
{isMounted && (
|
||||||
<FloatingPortal id='user-group-popover-portal'>
|
<FloatingPortal id='root-portal'>
|
||||||
<FloatingOverlay
|
<FloatingOverlay
|
||||||
id='user-group-popover-floating-overlay'
|
id='user-group-popover-floating-overlay'
|
||||||
className='user-group-popover-floating-overlay'
|
className='user-group-popover-floating-overlay'
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import type {ComponentProps} from 'react';
|
|||||||
import {type MessageDescriptor} from 'react-intl';
|
import {type MessageDescriptor} from 'react-intl';
|
||||||
|
|
||||||
import RenderEmoji from 'components/emoji/render_emoji';
|
import RenderEmoji from 'components/emoji/render_emoji';
|
||||||
import Tooltip from 'components/tooltip';
|
|
||||||
|
|
||||||
import {formatAsComponent} from 'utils/i18n';
|
import {formatAsComponent} from 'utils/i18n';
|
||||||
|
|
||||||
import {TooltipShortcutSequence, type ShortcutDefinition} from './shortcut';
|
import {TooltipShortcutSequence, type ShortcutDefinition} from './shortcut';
|
||||||
|
import BSTooltip from './tooltip';
|
||||||
|
|
||||||
type EmojiStyle = 'inline' | 'large' | undefined;
|
type EmojiStyle = 'inline' | 'large' | undefined;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export type CommonTooltipProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createTooltip(commonTooltipProps: CommonTooltipProps) {
|
export function createTooltip(commonTooltipProps: CommonTooltipProps) {
|
||||||
return (props: Omit<ComponentProps<typeof Tooltip>, 'children' | 'id'>) => {
|
return (props: Omit<ComponentProps<typeof BSTooltip>, 'children' | 'id'>) => {
|
||||||
const contents = [];
|
const contents = [];
|
||||||
|
|
||||||
if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle === 'large') {
|
if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle === 'large') {
|
||||||
@@ -90,12 +90,12 @@ export function createTooltip(commonTooltipProps: CommonTooltipProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<BSTooltip
|
||||||
{...props}
|
{...props}
|
||||||
id={commonTooltipProps.id}
|
id={commonTooltipProps.id}
|
||||||
>
|
>
|
||||||
{contents}
|
{contents}
|
||||||
</Tooltip>
|
</BSTooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,11 @@
|
|||||||
import React, {useMemo} from 'react';
|
import React, {useMemo} from 'react';
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps} from 'react';
|
||||||
|
|
||||||
import OverlayTrigger from 'components/overlay_trigger';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
|
|
||||||
import type {CommonTooltipProps} from './create_tooltip';
|
import type {CommonTooltipProps} from './create_tooltip';
|
||||||
import {createTooltip} from './create_tooltip';
|
import {createTooltip} from './create_tooltip';
|
||||||
|
import OverlayTrigger from './overlay_trigger';
|
||||||
|
|
||||||
type OverlayTriggerProps = ComponentProps<typeof OverlayTrigger>;
|
type OverlayTriggerProps = ComponentProps<typeof OverlayTrigger>;
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ type WithTooltipProps = {
|
|||||||
delayHide?: number;
|
delayHide?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
} & CommonTooltipProps;
|
} & CommonTooltipProps;
|
||||||
|
|
||||||
const WithTooltip = ({
|
const WithTooltip = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Props = OverlayTriggerProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use (and expand when extrictly needed) WithTooltip instead
|
* @deprecated Use (and expand when strictly needed) WithTooltip instead
|
||||||
*/
|
*/
|
||||||
const OverlayTrigger = React.forwardRef((props: Props, ref?: React.Ref<OriginalOverlayTrigger>) => {
|
const OverlayTrigger = React.forwardRef((props: Props, ref?: React.Ref<OriginalOverlayTrigger>) => {
|
||||||
const {overlay, disabled, ...otherProps} = props;
|
const {overlay, disabled, ...otherProps} = props;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`TooltipContent have correct structure with just title 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="tooltipContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitleContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitle"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`TooltipContent have correct structure with title and emoji 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="tooltipContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitleContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentEmoji"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label=":smile:"
|
||||||
|
class="emoticon"
|
||||||
|
data-emoticon="smile"
|
||||||
|
style="background-image: url(/static/emoji/1f604.png); background-size: contain; height: 16px; width: 16px; max-height: 16px; max-width: 16px; min-height: 16px; min-width: 16px; overflow: hidden;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitle"
|
||||||
|
>
|
||||||
|
Title with Emoji
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`TooltipContent have correct structure with title and large emoji 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="tooltipContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitleContainer isEmojiLarge"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentEmoji"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label=":smile:"
|
||||||
|
class="emoticon"
|
||||||
|
data-emoticon="smile"
|
||||||
|
style="background-image: url(/static/emoji/1f604.png); background-size: contain; height: 48px; width: 48px; max-height: 48px; max-width: 48px; min-height: 48px; min-width: 48px; overflow: hidden;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitle"
|
||||||
|
>
|
||||||
|
Title with Large Emoji
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`TooltipContent have correct structure with title and shortcut 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="tooltipContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitleContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitle"
|
||||||
|
>
|
||||||
|
Title with Shortcut
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="tooltipContentShortcut"
|
||||||
|
>
|
||||||
|
<mark
|
||||||
|
class="shortcut-key shortcut-key--tooltip"
|
||||||
|
>
|
||||||
|
Ctrl
|
||||||
|
</mark>
|
||||||
|
<mark
|
||||||
|
class="shortcut-key shortcut-key--tooltip"
|
||||||
|
>
|
||||||
|
K
|
||||||
|
</mark>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`TooltipContent have correct structure with title, emoji and hint 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="tooltipContent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitleContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltipContentEmoji"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label=":smile:"
|
||||||
|
class="emoticon"
|
||||||
|
data-emoticon="smile"
|
||||||
|
style="background-image: url(/static/emoji/1f604.png); background-size: contain; height: 16px; width: 16px; max-height: 16px; max-width: 16px; min-height: 16px; min-width: 16px; overflow: hidden;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="tooltipContentTitle"
|
||||||
|
>
|
||||||
|
Title with Hint
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="tooltipContentHint"
|
||||||
|
>
|
||||||
|
This is a hint
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {renderWithContext, screen, waitFor, act} from 'tests/react_testing_utils';
|
||||||
|
|
||||||
|
import WithTooltip from './index';
|
||||||
|
|
||||||
|
jest.mock('utils/user_agent', () => ({
|
||||||
|
isMac: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('WithTooltip', () => {
|
||||||
|
test('renders children correctly', async () => {
|
||||||
|
renderWithContext(
|
||||||
|
<WithTooltip title='TooltipOfButton'>
|
||||||
|
<button>{'I am a button surrounded by a tooltip'}</button>
|
||||||
|
</WithTooltip>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('I am a button surrounded by a tooltip')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows tooltip on hover', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<WithTooltip title='Tooltip will appear on hover'>
|
||||||
|
<div>{'Hover Me'}</div>
|
||||||
|
</WithTooltip>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.hover(screen.getByText('Hover Me'));
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Tooltip will appear on hover')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows tooltip on focus', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<WithTooltip title='Tooltip will appear on hover'>
|
||||||
|
<button>{'Hover Me'}</button>
|
||||||
|
</WithTooltip>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const trigger = screen.getByText('Hover Me');
|
||||||
|
|
||||||
|
// Clicking the button will simulate a focus event
|
||||||
|
userEvent.click(trigger);
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(trigger).toHaveFocus();
|
||||||
|
expect(screen.getByText('Tooltip will appear on hover')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls onOpen when tooltip appears', async () => {
|
||||||
|
const onOpen = jest.fn();
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<WithTooltip
|
||||||
|
title='Tooltip will appear on hover'
|
||||||
|
onOpen={onOpen}
|
||||||
|
>
|
||||||
|
<div>{'Hover Me'}</div>
|
||||||
|
</WithTooltip>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
expect(onOpen).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
userEvent.hover(screen.getByText('Hover Me'));
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Tooltip will appear on hover')).toBeInTheDocument();
|
||||||
|
expect(onOpen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {Placement} from '@floating-ui/react';
|
||||||
|
import {
|
||||||
|
useFloating,
|
||||||
|
autoUpdate,
|
||||||
|
offset,
|
||||||
|
useHover,
|
||||||
|
useFocus,
|
||||||
|
useDismiss,
|
||||||
|
useRole,
|
||||||
|
useInteractions,
|
||||||
|
arrow,
|
||||||
|
FloatingPortal,
|
||||||
|
useTransitionStyles,
|
||||||
|
FloatingArrow,
|
||||||
|
flip,
|
||||||
|
} from '@floating-ui/react';
|
||||||
|
import React, {useRef, useState, memo, useMemo, cloneElement, isValidElement} from 'react';
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import type {MessageDescriptor} from 'react-intl';
|
||||||
|
import {defineMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import {Constants} from 'utils/constants';
|
||||||
|
|
||||||
|
import TooltipContent from './tooltip_content';
|
||||||
|
import type {ShortcutDefinition} from './tooltip_shortcut';
|
||||||
|
|
||||||
|
import './tooltip.scss';
|
||||||
|
|
||||||
|
const ARROW_WIDTH = 10; // in px
|
||||||
|
const ARROW_HEIGHT = 6; // in px
|
||||||
|
const ARROW_OFFSET = 8; // in px
|
||||||
|
|
||||||
|
const TOOLTIP_REST_TIME_BEFORE_OPEN = 400; // in ms
|
||||||
|
const TOOLTIP_APPEAR_DURATION = 250; // in ms
|
||||||
|
const TOOLTIP_DISAPPEAR_DURATION = 200; // in ms
|
||||||
|
|
||||||
|
export const ShortcutKeys = {
|
||||||
|
alt: defineMessage({
|
||||||
|
id: 'shortcuts.generic.alt',
|
||||||
|
defaultMessage: 'Alt',
|
||||||
|
}),
|
||||||
|
cmd: '⌘',
|
||||||
|
ctrl: defineMessage({
|
||||||
|
id: 'shortcuts.generic.ctrl',
|
||||||
|
defaultMessage: 'Ctrl',
|
||||||
|
}),
|
||||||
|
option: '⌥',
|
||||||
|
shift: defineMessage({
|
||||||
|
id: 'shortcuts.generic.shift',
|
||||||
|
defaultMessage: 'Shift',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string | ReactNode | MessageDescriptor;
|
||||||
|
emoji?: string;
|
||||||
|
isEmojiLarge?: boolean;
|
||||||
|
hint?: string;
|
||||||
|
shortcut?: ShortcutDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the tooltip should be vertical or horizontal, by default it is vertical
|
||||||
|
* This doesn't always guarantee the tooltip will be vertical, it just determines the initial placement and fallback placements
|
||||||
|
*/
|
||||||
|
isVertical?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Do not use this except for special cases
|
||||||
|
* Callback when the tooltip appears
|
||||||
|
*/
|
||||||
|
onOpen?: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WithTooltip({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
emoji,
|
||||||
|
isEmojiLarge = false,
|
||||||
|
hint,
|
||||||
|
shortcut,
|
||||||
|
isVertical = true,
|
||||||
|
onOpen,
|
||||||
|
}: Props) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const arrowRef = useRef(null);
|
||||||
|
|
||||||
|
function handleChange(open: boolean) {
|
||||||
|
setOpen(open);
|
||||||
|
|
||||||
|
if (onOpen && open) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const placements = useMemo<{initial: Placement; fallback: Placement[]}>(() => {
|
||||||
|
let initial: Placement;
|
||||||
|
let fallback: Placement[];
|
||||||
|
if (isVertical) {
|
||||||
|
initial = 'top';
|
||||||
|
fallback = ['bottom', 'right', 'left'];
|
||||||
|
} else {
|
||||||
|
initial = 'right';
|
||||||
|
fallback = ['left', 'top', 'bottom'];
|
||||||
|
}
|
||||||
|
return {initial, fallback};
|
||||||
|
}, [isVertical]);
|
||||||
|
|
||||||
|
const {refs: {setReference, setFloating}, floatingStyles, context} = useFloating({
|
||||||
|
open,
|
||||||
|
onOpenChange: handleChange,
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
placement: placements.initial,
|
||||||
|
middleware: [
|
||||||
|
offset(ARROW_OFFSET),
|
||||||
|
flip({
|
||||||
|
fallbackPlacements: placements.fallback,
|
||||||
|
}),
|
||||||
|
arrow({
|
||||||
|
element: arrowRef,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const hover = useHover(context, {
|
||||||
|
restMs: TOOLTIP_REST_TIME_BEFORE_OPEN,
|
||||||
|
delay: {
|
||||||
|
open: Constants.OVERLAY_TIME_DELAY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const focus = useFocus(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const role = useRole(context, {role: 'tooltip'});
|
||||||
|
|
||||||
|
const {getReferenceProps, getFloatingProps} = useInteractions([hover, focus, dismiss, role]);
|
||||||
|
const {isMounted, styles: transitionStyles} = useTransitionStyles(context, {
|
||||||
|
duration: {
|
||||||
|
open: TOOLTIP_APPEAR_DURATION,
|
||||||
|
close: TOOLTIP_DISAPPEAR_DURATION,
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValidElement(children)) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Children must be a valid React element for WithTooltip');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trigger = cloneElement(children, {
|
||||||
|
...getReferenceProps({
|
||||||
|
ref: setReference,
|
||||||
|
...children.props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{trigger}
|
||||||
|
{isMounted && (
|
||||||
|
<FloatingPortal
|
||||||
|
id='root-portal' // This is the global portal container id
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='tooltipContainer'
|
||||||
|
ref={setFloating}
|
||||||
|
style={floatingStyles}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='tooltipContentContainer'
|
||||||
|
style={transitionStyles}
|
||||||
|
>
|
||||||
|
<TooltipContent
|
||||||
|
title={title}
|
||||||
|
emoji={emoji}
|
||||||
|
isEmojiLarge={isEmojiLarge}
|
||||||
|
hint={hint}
|
||||||
|
shortcut={shortcut}
|
||||||
|
/>
|
||||||
|
<FloatingArrow
|
||||||
|
ref={arrowRef}
|
||||||
|
context={context}
|
||||||
|
width={ARROW_WIDTH}
|
||||||
|
height={ARROW_HEIGHT}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(WithTooltip);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
.tooltipContainer {
|
||||||
|
z-index: 1070;
|
||||||
|
|
||||||
|
> .tooltipContentContainer {
|
||||||
|
z-index: 1070;
|
||||||
|
max-width: 220px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 0, 0, 1);
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
|
||||||
|
line-height: 18px;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
> .tooltipContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
|
||||||
|
> .tooltipContentTitleContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&.isEmojiLarge {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
> .tooltipContentEmoji {
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tooltipContentEmoji {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tooltipContentTitle {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tooltipContentShortcut {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tooltipContentHint {
|
||||||
|
color: rgba(255, 255, 255, 0.64);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {renderWithContext} from 'tests/react_testing_utils';
|
||||||
|
|
||||||
|
import TooltipContent from './tooltip_content';
|
||||||
|
|
||||||
|
describe('TooltipContent', () => {
|
||||||
|
test('have correct structure with just title', () => {
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<TooltipContent title='Title'/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('have correct structure with title and emoji', () => {
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<TooltipContent
|
||||||
|
title='Title with Emoji'
|
||||||
|
emoji='smile'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('have correct structure with title and large emoji', () => {
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<TooltipContent
|
||||||
|
title='Title with Large Emoji'
|
||||||
|
emoji='smile'
|
||||||
|
isEmojiLarge={true}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('have correct structure with title, emoji and hint', () => {
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<TooltipContent
|
||||||
|
title='Title with Hint'
|
||||||
|
hint='This is a hint'
|
||||||
|
emoji='smile'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('have correct structure with title and shortcut', () => {
|
||||||
|
const shortcut = {
|
||||||
|
default: ['Ctrl', 'K'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<TooltipContent
|
||||||
|
title='Title with Shortcut'
|
||||||
|
shortcut={shortcut}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import type {MessageDescriptor} from 'react-intl';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import RenderEmoji from 'components/emoji/render_emoji';
|
||||||
|
|
||||||
|
import {isMessageDescriptor} from 'utils/i18n';
|
||||||
|
|
||||||
|
import TooltipShortcut from './tooltip_shortcut';
|
||||||
|
import {type ShortcutDefinition} from './tooltip_shortcut';
|
||||||
|
|
||||||
|
const TOOLTIP_EMOTICON_SIZE = 16;
|
||||||
|
const TOOLTIP_EMOTICON_LARGE_SIZE = 48;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string | ReactNode | MessageDescriptor;
|
||||||
|
emoji?: string;
|
||||||
|
isEmojiLarge?: boolean;
|
||||||
|
hint?: string;
|
||||||
|
shortcut?: ShortcutDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent(props: Props) {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
let title = props.title;
|
||||||
|
if (isMessageDescriptor(title)) {
|
||||||
|
title = formatMessage(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='tooltipContent'>
|
||||||
|
<span
|
||||||
|
className={classNames('tooltipContentTitleContainer', {
|
||||||
|
isEmojiLarge: props.isEmojiLarge,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{props.emoji && (
|
||||||
|
<span className='tooltipContentEmoji'>
|
||||||
|
<RenderEmoji
|
||||||
|
emojiName={props.emoji}
|
||||||
|
size={props.isEmojiLarge ? TOOLTIP_EMOTICON_LARGE_SIZE : TOOLTIP_EMOTICON_SIZE}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className='tooltipContentTitle'>{title}</span>
|
||||||
|
</span>
|
||||||
|
{props.hint && (
|
||||||
|
<span className='tooltipContentHint'>{props.hint}</span>
|
||||||
|
)}
|
||||||
|
{props.shortcut && (
|
||||||
|
<span className='tooltipContentShortcut'>
|
||||||
|
<TooltipShortcut shortcut={props.shortcut}/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TooltipContent);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {defineMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||||
|
import * as userAgentUtils from 'utils/user_agent';
|
||||||
|
|
||||||
|
import TooltipShortcut from './tooltip_shortcut';
|
||||||
|
|
||||||
|
jest.mock('utils/user_agent', () => ({
|
||||||
|
isMac: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('TooltipShortcut', () => {
|
||||||
|
const isMacMock = jest.mocked(userAgentUtils.isMac);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show non mac shortcut when on non mac', () => {
|
||||||
|
isMacMock.mockReturnValue(false);
|
||||||
|
const shortcut = {
|
||||||
|
default: ['Ctrl', 'K'],
|
||||||
|
mac: ['⌘', 'K'],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<TooltipShortcut shortcut={shortcut}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Ctrl')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('K')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByText('⌘')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show mac shortcut when on mac', () => {
|
||||||
|
isMacMock.mockReturnValue(true);
|
||||||
|
|
||||||
|
const shortcut = {
|
||||||
|
default: ['Ctrl', 'K'],
|
||||||
|
mac: ['⌘', 'K'],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<TooltipShortcut shortcut={shortcut}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('⌘')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('K')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByText('Ctrl')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show shortcut with message descriptor', () => {
|
||||||
|
const shortcut = {
|
||||||
|
default: [
|
||||||
|
defineMessage({
|
||||||
|
id: 'shortcuts.generic.enter',
|
||||||
|
defaultMessage: 'Enter',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<TooltipShortcut shortcut={shortcut}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Enter')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import type {MessageDescriptor} from 'react-intl';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import {ShortcutKey, ShortcutKeyVariant} from 'components/shortcut_key';
|
||||||
|
|
||||||
|
import {isMessageDescriptor} from 'utils/i18n';
|
||||||
|
import {isMac} from 'utils/user_agent';
|
||||||
|
|
||||||
|
export type ShortcutKeyDescriptor = string | MessageDescriptor;
|
||||||
|
|
||||||
|
export type ShortcutDefinition = {
|
||||||
|
default: ShortcutKeyDescriptor[];
|
||||||
|
mac?: ShortcutKeyDescriptor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
shortcut: ShortcutDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipShortcut(props: Props) {
|
||||||
|
let shortcut = props.shortcut.default;
|
||||||
|
if (props.shortcut.mac && isMac()) {
|
||||||
|
shortcut = props.shortcut.mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{shortcut.map((shortcutKey) => {
|
||||||
|
let key;
|
||||||
|
let content;
|
||||||
|
if (isMessageDescriptor(shortcutKey)) {
|
||||||
|
key = shortcutKey.id;
|
||||||
|
content = <FormattedMessage {...shortcutKey}/>;
|
||||||
|
} else {
|
||||||
|
key = shortcutKey;
|
||||||
|
content = shortcutKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShortcutKey
|
||||||
|
key={key}
|
||||||
|
variant={ShortcutKeyVariant.Tooltip}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ShortcutKey>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TooltipShortcut);
|
||||||
@@ -10,12 +10,6 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&#announcement-bar__tooltip {
|
|
||||||
width: 50%;
|
|
||||||
max-width: 100%;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-inner {
|
.tooltip-inner {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@@ -75,33 +69,3 @@
|
|||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-ui-tooltip {
|
|
||||||
max-width: 220px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #000;
|
|
||||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 18px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
text-align: center;
|
|
||||||
transition: opacity 0.15s linear;
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
&.floating-ui-tooltip--visible {
|
|
||||||
opacity: 0.9;
|
|
||||||
transition: opacity 0.15s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-ui-tooltip-arrow {
|
|
||||||
position: absolute;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: #000;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|||||||
2
webapp/package-lock.json
сгенерированный
2
webapp/package-lock.json
сгенерированный
@@ -57,7 +57,7 @@
|
|||||||
"name": "mattermost-webapp",
|
"name": "mattermost-webapp",
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "0.26.6",
|
"@floating-ui/react": "0.26.28",
|
||||||
"@giphy/js-fetch-api": "5.1.0",
|
"@giphy/js-fetch-api": "5.1.0",
|
||||||
"@giphy/react-components": "8.1.0",
|
"@giphy/react-components": "8.1.0",
|
||||||
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
|
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user