diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js
index 330f51014d..63d445bd9c 100644
--- a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js
+++ b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js
@@ -91,7 +91,7 @@ describe('channels > channel header', {testIsolation: true}, () => {
});
// * Verify tooltip text
- cy.get('#pluginTooltip').contains('Playbooks');
+ cy.get('#playbooksChannelHeaderButton').contains('Playbooks');
});
it('webapp should make the Playbook channel header button active when opened', () => {
diff --git a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap
index 37511f78c6..484fad673a 100644
--- a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap
+++ b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap
@@ -72,7 +72,6 @@ exports[`components/ChannelHeader should match snapshot with last active display
className="channel-header__icons"
>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
}
onClick={[Function]}
- tooltipKey="channelMembers"
+ tooltip="Members"
/>
}
onClick={[Function]}
- tooltipKey="pinnedPosts"
+ tooltip="Pinned messages"
/>
}
onClick={[Function]}
- tooltipKey="channelFiles"
+ tooltip="Channel files"
/>
{
memberListButton = (
);
}
@@ -607,20 +606,18 @@ class ChannelHeader extends React.PureComponent
{
{memberListButton}
{this.props.isFileAttachmentsEnabled &&
}
diff --git a/webapp/channels/src/components/channel_header/channel_info_button.tsx b/webapp/channels/src/components/channel_header/channel_info_button.tsx
index 7de1039b3d..d7b3a70f33 100644
--- a/webapp/channels/src/components/channel_header/channel_info_button.tsx
+++ b/webapp/channels/src/components/channel_header/channel_info_button.tsx
@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
+import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import styled from 'styled-components';
@@ -30,6 +31,7 @@ const Icon = styled.i`
const ChannelInfoButton = ({channel}: Props) => {
const dispatch = useDispatch();
+ const intl = useIntl();
const rhsState: RhsState = useSelector(getRhsState);
const isRhsOpen: boolean = useSelector(getIsRhsOpen);
@@ -48,7 +50,12 @@ const ChannelInfoButton = ({channel}: Props) => {
}
}, [buttonActive, channel.id, isChannelInfo, dispatch]);
- const tooltipKey = buttonActive ? 'closeChannelInfo' : 'openChannelInfo';
+ let tooltip;
+ if (buttonActive) {
+ tooltip = intl.formatMessage({id: 'channel_header.closeChannelInfo', defaultMessage: 'Close info'});
+ } else {
+ tooltip = intl.formatMessage({id: 'channel_header.openChannelInfo', defaultMessage: 'View Info'});
+ }
let buttonClass = 'channel-header__icon';
if (buttonActive) {
@@ -60,9 +67,8 @@ const ChannelInfoButton = ({channel}: Props) => {
buttonClass={buttonClass}
buttonId='channel-info-btn'
onClick={toggleRHS}
- ariaLabel={true}
iconComponent={}
- tooltipKey={tooltipKey}
+ tooltip={tooltip}
/>
);
};
diff --git a/webapp/channels/src/components/channel_header/components/__snapshots__/header_icon_wrapper.test.tsx.snap b/webapp/channels/src/components/channel_header/components/__snapshots__/header_icon_wrapper.test.tsx.snap
deleted file mode 100644
index 19a582620e..0000000000
--- a/webapp/channels/src/components/channel_header/components/__snapshots__/header_icon_wrapper.test.tsx.snap
+++ /dev/null
@@ -1,260 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on ChannelFilesIcon 1`] = `
-
-
-
-
- }
- placement="bottom"
- trigger={
- Array [
- "hover",
- "focus",
- ]
- }
- >
-
-
-
-`;
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on FlagIcon 1`] = `
-
-
-
-
- }
- placement="bottom"
- trigger={
- Array [
- "hover",
- "focus",
- ]
- }
- >
-
-
-
-`;
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on MentionsIcon 1`] = `
-
-
-
-
-
- }
- placement="bottom"
- trigger={
- Array [
- "hover",
- "focus",
- ]
- }
- >
-
-
-
-`;
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on PinIcon 1`] = `
-
-
-
-
- }
- placement="bottom"
- trigger={
- Array [
- "hover",
- "focus",
- ]
- }
- >
-
-
-
-`;
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on PluginIcon with tooltipText 1`] = `
-
-
-
- plugin_tooltip_text
-
-
- }
- placement="bottom"
- trigger={
- Array [
- "hover",
- "focus",
- ]
- }
- >
-
-
-
-`;
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on PluginIcon without tooltipText 1`] = `
-
-
-
-
-
-`;
-
-exports[`components/channel_header/components/HeaderIconWrapper should match snapshot, on SearchIcon 1`] = `
-
-
-
-
- }
- placement="bottom"
- trigger={
- Array [
- "hover",
- "focus",
- ]
- }
- >
-
-
-
-`;
diff --git a/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx b/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx
index e98f299226..86dc064b99 100644
--- a/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx
+++ b/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx
@@ -1,17 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {shallow} from 'enzyme';
import React from 'react';
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
-import FlagIcon from 'components/widgets/icons/flag_icon';
import MentionsIcon from 'components/widgets/icons/mentions_icon';
-import PinIcon from 'components/widgets/icons/pin_icon';
-import SearchIcon from 'components/widgets/icons/search_icon';
+
+import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
describe('components/channel_header/components/HeaderIconWrapper', () => {
- function emptyFunction() {} //eslint-disable-line no-empty-function
const mentionsIcon = (
{
iconComponent: mentionsIcon,
buttonClass: 'button_class',
buttonId: 'button_id',
- onClick: emptyFunction,
- tooltipKey: 'recentMentions',
+ onClick: jest.fn(),
+ tooltip: 'Recent mentions',
};
- test('should match snapshot, on MentionsIcon', () => {
- const wrapper = shallow(
- ,
+ test('should be accessible', async () => {
+ renderWithContext(
+ ,
);
- expect(wrapper).toMatchSnapshot();
+ expect(screen.getByLabelText('Recent mentions')).toBeVisible();
+ expect(screen.queryByText('Recent mentions')).not.toBeInTheDocument();
+
+ userEvent.hover(screen.getByLabelText('Recent mentions'));
+
+ await waitFor(() => {
+ expect(screen.queryByText('Recent mentions')).toBeInTheDocument();
+ });
});
- test('should match snapshot, on FlagIcon', () => {
- const flagIcon = (
-
+ test('should show the shortcut in its tooltip', async () => {
+ renderWithContext(
+ ,
);
- const props = {...baseProps, iconComponent: flagIcon, tooltipKey: 'flaggedPosts'};
- const wrapper = shallow(
- ,
- );
+ expect(screen.getByLabelText('Recent mentions')).toBeVisible();
+ expect(screen.queryByText('Recent mentions')).not.toBeInTheDocument();
+ expect(screen.queryByText('a')).not.toBeInTheDocument();
+ expect(screen.queryByText('b')).not.toBeInTheDocument();
+ expect(screen.queryByText('c')).not.toBeInTheDocument();
- expect(wrapper).toMatchSnapshot();
- });
+ userEvent.hover(screen.getByLabelText('Recent mentions'));
- test('should match snapshot, on PinIcon', () => {
- const pinIcon = (
-
- );
+ await waitFor(() => {
+ expect(screen.queryByText('Recent mentions')).toBeInTheDocument();
- const props = {...baseProps, iconComponent: pinIcon, tooltipKey: 'pinnedPosts', buttonClass: 'pinned_posts_class'};
- const wrapper = shallow(
- ,
- );
-
- expect(wrapper).toMatchSnapshot();
- });
-
- test('should match snapshot, on ChannelFilesIcon', () => {
- const channelFilesIcon = ;
-
- const props = {...baseProps, iconComponent: channelFilesIcon, tooltipKey: 'channelFiles', buttonClass: 'channel_files_class'};
- const wrapper = shallow(
- ,
- );
-
- expect(wrapper).toMatchSnapshot();
- });
-
- test('should match snapshot, on SearchIcon', () => {
- const searchIcon = (
-
- );
-
- const props = {...baseProps, iconComponent: searchIcon, tooltipKey: 'search', buttonClass: 'search_class'};
- const wrapper = shallow(
- ,
- );
-
- expect(wrapper).toMatchSnapshot();
- });
-
- test('should match snapshot, on PluginIcon with tooltipText', () => {
- const pluginIcon = (
-
- );
-
- const props = {...baseProps, iconComponent: pluginIcon, tooltipKey: 'plugin', tooltipText: 'plugin_tooltip_text'};
- const wrapper = shallow(
- ,
- );
-
- expect(wrapper).toMatchSnapshot();
- });
-
- test('should match snapshot, on PluginIcon without tooltipText', () => {
- const pluginIcon = (
-
- );
-
- const props = {...baseProps, iconComponent: pluginIcon, tooltipKey: 'plugin'};
- const wrapper = shallow(
- ,
- );
-
- expect(wrapper).toMatchSnapshot();
+ expect(screen.queryByText('a')).toBeVisible();
+ expect(screen.queryByText('b')).toBeVisible();
+ expect(screen.queryByText('c')).toBeVisible();
+ });
});
});
diff --git a/webapp/channels/src/components/channel_header/components/header_icon_wrapper.tsx b/webapp/channels/src/components/channel_header/components/header_icon_wrapper.tsx
index d2618eda52..4bd6ef2ae8 100644
--- a/webapp/channels/src/components/channel_header/components/header_icon_wrapper.tsx
+++ b/webapp/channels/src/components/channel_header/components/header_icon_wrapper.tsx
@@ -2,191 +2,65 @@
// See LICENSE.txt for license information.
import React from 'react';
-import {FormattedMessage} from 'react-intl';
import NewChannelWithBoardTourTip from 'components/app_bar/new_channel_with_board_tour_tip';
-import KeyboardShortcutSequence, {
- KEYBOARD_SHORTCUTS,
-} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
-import type {
- KeyboardShortcutDescriptor} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
-import OverlayTrigger from 'components/overlay_trigger';
-import Tooltip from 'components/tooltip';
+import WithTooltip from 'components/with_tooltip';
+import type {ShortcutDefinition} from 'components/with_tooltip/shortcut';
-import {Constants, suitePluginIds} from 'utils/constants';
-import {t} from 'utils/i18n';
-import {localizeMessage} from 'utils/utils';
+import {suitePluginIds} from 'utils/constants';
type Props = {
- ariaLabel?: boolean;
+
+ /**
+ * ariaLabelOverride lets you override the aria-label which would otherwise use the tooltip text. This typically
+ * shouldn't be needed.
+ */
+ ariaLabelOverride?: string;
+
buttonClass?: string;
buttonId: string;
iconComponent: React.ReactNode;
onClick: (event: React.MouseEvent) => void;
- tooltipKey: string;
- tooltipText?: React.ReactNode;
+ tooltip: string;
+ tooltipShortcut?: ShortcutDefinition;
isRhsOpen?: boolean;
pluginId?: string;
}
-type TooltipInfo = {
- class: string;
- id: string;
- messageID: string;
- message: string;
- keyboardShortcut?: KeyboardShortcutDescriptor;
-}
-
const HeaderIconWrapper = (props: Props) => {
const {
- ariaLabel,
+ ariaLabelOverride,
buttonClass,
buttonId,
iconComponent,
onClick,
- tooltipKey,
- tooltipText,
+ tooltip: tooltipText,
+ tooltipShortcut,
isRhsOpen,
pluginId,
} = props;
- const toolTips: Record = {
- flaggedPosts: {
- class: 'text-nowrap',
- id: 'flaggedTooltip',
- messageID: t('channel_header.flagged'),
- message: 'Saved messages',
- },
- pinnedPosts: {
- class: 'pinned-posts',
- id: 'pinnedPostTooltip',
- messageID: t('channel_header.pinnedPosts'),
- message: 'Pinned messages',
- },
- recentMentions: {
- class: '',
- id: 'recentMentionsTooltip',
- messageID: t('channel_header.recentMentions'),
- message: 'Recent mentions',
- keyboardShortcut: KEYBOARD_SHORTCUTS.navMentions,
- },
- search: {
- class: '',
- id: 'searchTooltip',
- messageID: t('channel_header.search'),
- message: 'Search',
- },
- channelFiles: {
- class: 'channel-files',
- id: 'channelFilesTooltip',
- messageID: t('channel_header.channelFiles'),
- message: 'Channel files',
- },
- openChannelInfo: {
- class: 'channel-info',
- id: 'channelInfoTooltip',
- messageID: t('channel_header.openChannelInfo'),
- message: 'View Info',
- },
- closeChannelInfo: {
- class: 'channel-info',
- id: 'channelInfoTooltip',
- messageID: t('channel_header.closeChannelInfo'),
- message: 'Close info',
- },
- channelMembers: {
- class: 'channel-info',
- id: 'channelMembersTooltip',
- messageID: t('channel_header.channelMembers'),
- message: 'Members',
- },
- };
-
- function getTooltip(key: string) {
- if (toolTips[key] == null) {
- return null;
- }
-
- return (
-
-
- {toolTips[key].keyboardShortcut &&
-
- }
-
- );
- }
-
- let tooltip;
- if (tooltipKey === 'plugin' && tooltipText) {
- tooltip = (
-
- {tooltipText}
-
- );
- } else {
- tooltip = getTooltip(tooltipKey);
- }
-
- let ariaLabelText;
- if (ariaLabel) {
- ariaLabelText = `${localizeMessage(toolTips[tooltipKey].messageID, toolTips[tooltipKey].message)}`;
- }
-
const boardsEnabled = pluginId === suitePluginIds.focalboard;
- if (tooltip) {
- return (
-
- > : tooltip}
- >
-
-
- {boardsEnabled &&
-
- }
-
- );
- }
+ const ariaLabelText = ariaLabelOverride ?? tooltipText;
return (
<>
-
+
-
+
{boardsEnabled &&
= (props: Props): JSX.Element => {
aria-hidden='true'
/>
}
- ariaLabel={true}
buttonClass={classNames(
'channel-header__icon',
{'channel-header__icon--active': props.isMentionSearch},
)}
buttonId={props.isSideBarRight ? 'sbrChannelHeaderMentionButton' : 'channelHeaderMentionButton'}
onClick={searchMentions}
- tooltipKey={'recentMentions'}
+ tooltip={intl.formatMessage({id: 'channel_header.recentMentions', defaultMessage: 'Recent mentions'})}
+ tooltipShortcut={mentionsShortcut}
isRhsOpen={props.isRhsOpen}
/>
);
@@ -399,14 +405,13 @@ const Search: React.FC = (props: Props): JSX.Element => {
iconComponent={
}
- ariaLabel={true}
buttonClass={classNames(
'channel-header__icon ',
{'channel-header__icon--active': props.isFlaggedPosts},
)}
buttonId={props.isSideBarRight ? 'sbrChannelHeaderFlagButton' : 'channelHeaderFlagButton'}
onClick={getFlagged}
- tooltipKey={'flaggedPosts'}
+ tooltip={intl.formatMessage({id: 'channel_header.flagged', defaultMessage: 'Saved messages'})}
isRhsOpen={props.isRhsOpen}
/>
);
@@ -501,10 +506,9 @@ const Search: React.FC = (props: Props): JSX.Element => {
aria-hidden='true'
/>
}
- ariaLabel={true}
buttonId={'channelHeaderSearchButton'}
onClick={searchButtonClick}
- tooltipKey={'search'}
+ tooltip={intl.formatMessage({id: 'channel_header.search', defaultMessage: 'Search'})}
/>
);
}
diff --git a/webapp/channels/src/plugins/channel_header_plug/__snapshots__/channel_header_plug.test.tsx.snap b/webapp/channels/src/plugins/channel_header_plug/__snapshots__/channel_header_plug.test.tsx.snap
deleted file mode 100644
index c185d19bc8..0000000000
--- a/webapp/channels/src/plugins/channel_header_plug/__snapshots__/channel_header_plug.test.tsx.snap
+++ /dev/null
@@ -1,175 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`plugins/ChannelHeaderPlug should match snapshot with one extended component 1`] = `
-
-
-
-
-
-`;
-
-exports[`plugins/ChannelHeaderPlug should match snapshot with six extended components 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`plugins/ChannelHeaderPlug should not render anything when the App Bar is visible 1`] = ``;
-
-exports[`plugins/ChannelHeaderPlug should not render anything with no extended component 1`] = ``;
diff --git a/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx b/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx
index 1c96874c9b..cb2978235f 100644
--- a/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx
+++ b/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx
@@ -3,127 +3,115 @@
import React from 'react';
-import type {Channel, ChannelMembership} from '@mattermost/types/channels';
-
-import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
-
-import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug';
-import {renderWithContext} from 'tests/react_testing_utils';
+import {renderWithContext, screen} from 'tests/react_testing_utils';
+import {TestHelper} from 'utils/test_helper';
import type {PluginComponent} from 'types/store/plugins';
+import ChannelHeaderPlug, {maxComponentsBeforeDropdown} from './channel_header_plug';
+
describe('plugins/ChannelHeaderPlug', () => {
- const testPlug: PluginComponent = {
- id: 'someid',
- pluginId: 'pluginid',
- icon: ,
- action: jest.fn,
- dropdownText: 'some dropdown text',
- tooltipText: 'some tooltip text',
- } as PluginComponent;
+ const baseProps = {
+ components: [],
+ channel: TestHelper.getChannelMock({id: 'channel1'}),
+ channelMember: TestHelper.getChannelMembershipMock({channel_id: 'channel1', user_id: 'user1'}),
+ sidebarOpen: false,
+ actions: {
+ handleBindingClick: jest.fn(),
+ postEphemeralCallResponseForChannel: jest.fn(),
+ openAppsModal: jest.fn(),
+ },
+ appBindings: [],
+ appsEnabled: false,
+ shouldShowAppBar: false,
+ };
+
+ function makeTestPlug(n = 1): PluginComponent {
+ return {
+ id: 'someid' + n,
+ pluginId: 'pluginid' + n,
+ icon: ,
+ action: jest.fn,
+ dropdownText: 'some dropdown text ' + n,
+ tooltipText: 'some tooltip text ' + n,
+ };
+ }
test('should not render anything with no extended component', () => {
- const {asFragment} = renderWithContext(
+ const {container} = renderWithContext(
,
);
- expect(asFragment()).toMatchSnapshot();
+
+ expect(container).toBeEmptyDOMElement();
});
- test('should match snapshot with one extended component', () => {
- const {asFragment} = renderWithContext(
+ test('should render a single plug', () => {
+ renderWithContext(
,
);
- expect(asFragment()).toMatchSnapshot();
+
+ expect(screen.getByLabelText('some tooltip text 1')).toBeInTheDocument();
});
- test('should match snapshot with six extended components', () => {
- const {asFragment} = renderWithContext(
+ test(`should render ${maxComponentsBeforeDropdown} plugs in the header`, () => {
+ const components = [];
+ for (let i = 0; i < maxComponentsBeforeDropdown; i++) {
+ components.push(makeTestPlug(i));
+ }
+
+ renderWithContext(
,
);
- expect(asFragment()).toMatchSnapshot();
+
+ for (let i = 0; i < components.length; i++) {
+ expect(screen.getByLabelText('some tooltip text ' + i)).toBeInTheDocument();
+ }
+ });
+
+ test(`should render more than ${maxComponentsBeforeDropdown} plugs in a dropdown`, () => {
+ const components = [];
+ for (let i = 0; i < maxComponentsBeforeDropdown + 1; i++) {
+ components.push(makeTestPlug(i));
+ }
+
+ renderWithContext(
+ ,
+ );
+
+ for (let i = 0; i < components.length; i++) {
+ expect(screen.queryByLabelText('some tooltip text ' + i)).not.toBeInTheDocument();
+ }
+
+ // Ideally, this would identify the dropdown button better, but this uses a custom dropdown which is
+ // not at all accessible
+ expect(screen.getByRole('button', {name: components.length.toString()})).toBeVisible();
});
test('should not render anything when the App Bar is visible', () => {
- const {asFragment} = renderWithContext(
+ const {container} = renderWithContext(
,
);
- expect(asFragment()).toMatchSnapshot();
+
+ expect(container).toBeEmptyDOMElement();
});
});
diff --git a/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.tsx b/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.tsx
index 492b79d265..a21889c614 100644
--- a/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.tsx
+++ b/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.tsx
@@ -13,7 +13,6 @@ import type {AppBinding} from '@mattermost/types/apps';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
-import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
import OverlayTrigger from 'components/overlay_trigger';
@@ -33,6 +32,8 @@ type CustomMenuProps = {
bsRole: string;
}
+export const maxComponentsBeforeDropdown = 15;
+
class CustomMenu extends React.PureComponent {
handleRootClose = () => {
this.props.onClose();
@@ -104,7 +105,6 @@ type ChannelHeaderPlugProps = {
appsEnabled: boolean;
channel: Channel;
channelMember?: ChannelMembership;
- theme: Theme;
sidebarOpen: boolean;
shouldShowAppBar: boolean;
actions: {
@@ -165,15 +165,44 @@ class ChannelHeaderPlug extends React.PureComponent {
+ // These values are supposed to be strings based on PluginComponent, but some plugins pass non-strings,
+ // so do some hacky stuff to try to convert it back to a string. DO NOT USE THIS ELSEWHERE!
+ function tooltipToAriaLabelHack(intl: IntlShape, stringOrElement: string | React.ReactElement) {
+ if (typeof stringOrElement === 'string') {
+ // This is the case that we hope for
+ return stringOrElement;
+ }
+
+ if (stringOrElement.type === FormattedMessage) {
+ // This is a FormattedMessage, so extract the props to translate the text manually
+ return intl.formatMessage(
+ {
+ id: stringOrElement.props.id,
+ defaultMessage: stringOrElement.props.defaultMessage,
+ },
+ stringOrElement.props.value,
+ );
+ }
+
+ return '';
+ }
+
+ let ariaLabel;
+ if (plug.tooltipText) {
+ ariaLabel = tooltipToAriaLabelHack(this.props.intl, plug.tooltipText);
+ } else if (plug.dropdownText) {
+ ariaLabel = tooltipToAriaLabelHack(this.props.intl, plug.dropdownText);
+ }
+
return (
this.fireAction(plug.action!)}
- buttonId={plug.id}
- tooltipKey={'plugin'}
- tooltipText={plug.tooltipText ? plug.tooltipText : plug.dropdownText}
+ buttonId={plug.id + 'ChannelHeaderButton'}
+ tooltip={plug.tooltipText ?? plug.dropdownText ?? ''}
+ ariaLabelOverride={ariaLabel}
pluginId={plug.pluginId}
/>
);
@@ -245,8 +274,7 @@ class ChannelHeaderPlug extends React.PureComponent this.onBindingClick(binding)}
buttonId={`${binding.app_id}_${binding.location}`}
- tooltipKey={'plugin'}
- tooltipText={binding.label}
+ tooltip={binding.label}
/>
);
};
@@ -346,7 +374,7 @@ class ChannelHeaderPlug extends React.PureComponent plug.icon && plug.action).map(this.createComponentButton);
if (this.props.appsEnabled) {
componentButtons = componentButtons.concat(appBindings.map(this.createAppBindingButton));
diff --git a/webapp/channels/src/sass/components/_tooltip.scss b/webapp/channels/src/sass/components/_tooltip.scss
index 7a7ef8227e..dc3808fc8c 100644
--- a/webapp/channels/src/sass/components/_tooltip.scss
+++ b/webapp/channels/src/sass/components/_tooltip.scss
@@ -40,11 +40,6 @@
}
}
- &.text-nowrap {
- .tooltip-inner {
- white-space: nowrap;
- }
- }
.tooltip-help {
color: rgba(255, 255, 255, 0.75);
diff --git a/webapp/channels/src/tests/setup_jest.ts b/webapp/channels/src/tests/setup_jest.ts
index c3d6c29780..b7eb5754a4 100644
--- a/webapp/channels/src/tests/setup_jest.ts
+++ b/webapp/channels/src/tests/setup_jest.ts
@@ -60,6 +60,7 @@ function isDependencyWarning(params: string[]) {
paramsHasComponent('Portal') ||
paramsHasComponent('Overlay') ||
paramsHasComponent('Position') ||
+ paramsHasComponent('Dropdown') ||
// React-Select
paramsHasComponent('Select')