diff --git a/webapp/channels/src/components/global_header/left_controls/product_menu/__snapshots__/product_menu.test.tsx.snap b/webapp/channels/src/components/global_header/left_controls/product_menu/__snapshots__/product_menu.test.tsx.snap
index 60611824377..7c710d9cd85 100644
--- a/webapp/channels/src/components/global_header/left_controls/product_menu/__snapshots__/product_menu.test.tsx.snap
+++ b/webapp/channels/src/components/global_header/left_controls/product_menu/__snapshots__/product_menu.test.tsx.snap
@@ -46,7 +46,7 @@ exports[`components/global/product_switcher should have an active button state w
role="menu"
>
;
describe('components/ProductMenuItem', () => {
const defaultProps: ProductMenuItemProps = {
@@ -75,14 +76,21 @@ describe('components/ProductMenuItem', () => {
expect(svgElements.length).toBe(2);
});
- test('should not show check icon when active is false', () => {
+ test('should show open in new tab button when active is false', () => {
renderWithContext(
);
- const menuItem = screen.getByRole('menuitem');
+ expect(screen.getByLabelText('Open in new tab')).toBeInTheDocument();
+ });
- // When not active, there should only be one SVG element: the product icon
- const svgElements = menuItem.querySelectorAll('svg');
- expect(svgElements.length).toBe(1);
+ test('should not show open in new tab button when active is true', () => {
+ const props: ProductMenuItemProps = {
+ ...defaultProps,
+ active: true,
+ };
+
+ renderWithContext(
);
+
+ expect(screen.queryByLabelText('Open in new tab')).not.toBeInTheDocument();
});
test('should call onClick when clicked', async () => {
@@ -99,6 +107,24 @@ describe('components/ProductMenuItem', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
+ test('should open destination in new tab when open in new tab button is clicked', async () => {
+ const onClick = jest.fn();
+ const windowOpenSpy = jest.spyOn(window, 'open').mockImplementation();
+ const props: ProductMenuItemProps = {
+ ...defaultProps,
+ onClick,
+ };
+
+ renderWithContext(
);
+
+ await userEvent.click(screen.getByLabelText('Open in new tab'), {pointerEventsCheck: 0});
+
+ expect(windowOpenSpy).toHaveBeenCalledWith('/test-destination', '_blank', 'noopener,noreferrer');
+ expect(onClick).toHaveBeenCalledTimes(1);
+
+ windowOpenSpy.mockRestore();
+ });
+
test('should render tour tip when provided', () => {
const tourTipContent = 'Tour tip content';
const TourTip =
{tourTipContent}
;
diff --git a/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_item/product_menu_item.tsx b/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_item/product_menu_item.tsx
index c343a077d6e..c97cd34284c 100644
--- a/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_item/product_menu_item.tsx
+++ b/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_item/product_menu_item.tsx
@@ -1,13 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
+import React, {useCallback} from 'react';
+import {useIntl} from 'react-intl';
import {Link} from 'react-router-dom';
import styled from 'styled-components';
-import glyphMap, {CheckIcon} from '@mattermost/compass-icons/components';
+import glyphMap, {CheckIcon, OpenInNewIcon} from '@mattermost/compass-icons/components';
import type {IconGlyphTypes} from '@mattermost/compass-icons/IconGlyphs';
+import WithTooltip from 'components/with_tooltip';
+
export interface ProductMenuItemProps {
destination: string;
icon: IconGlyphTypes | React.ReactNode;
@@ -19,6 +22,34 @@ export interface ProductMenuItemProps {
id?: string;
}
+const MenuItemTextContainer = styled.div`
+ margin-left: 8px;
+ flex-grow: 1;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 20px;
+`;
+
+const OpenInNewTabButton = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ color: rgba(var(--center-channel-color-rgb), 0.56);
+ padding: 6px !important;
+ margin-right: -6px;
+ opacity: 0;
+ pointer-events: none;
+
+ &:hover {
+ background: rgba(var(--center-channel-color-rgb), 0.08);
+ color: rgba(var(--center-channel-color-rgb), 0.72);
+ }
+`;
+
const MenuItem = styled(Link)`
&& {
text-decoration: none;
@@ -43,19 +74,24 @@ const MenuItem = styled(Link)`
button {
padding: 0 6px;
}
-`;
-const MenuItemTextContainer = styled.div`
- margin-left: 8px;
- flex-grow: 1;
- font-weight: 600;
- font-size: 14px;
- line-height: 20px;
+ &:hover ${OpenInNewTabButton} {
+ opacity: 1;
+ pointer-events: auto;
+ }
`;
const ProductMenuItem = ({icon, destination, text, active, onClick, tourTip, id}: ProductMenuItemProps): JSX.Element => {
+ const {formatMessage} = useIntl();
const ProductIcon = typeof icon === 'string' ? glyphMap[icon as IconGlyphTypes] : null;
+ const handleOpenInNewTab = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ window.open(destination, '_blank', 'noopener,noreferrer');
+ onClick();
+ }, [destination, onClick]);
+
return (
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 3b67749402a..d5b8d2c6889 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -5684,6 +5684,7 @@
"pricing_modal.plan_label_trialDays": "{days} DAYS LEFT ON TRIAL",
"pricing_modal.wantToTry": "Want to try? ",
"pricing_modal.wantToUpgrade": "Want to upgrade? ",
+ "product_menu_item.open_in_new_tab": "Open in new tab",
"profile_popover.aria_label.with_username": "{userName}'s profile popover",
"profile_popover.aria_label.without_username": "profile popover",
"promote_to_user_modal.desc": "This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?",