mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
[MM-67883] Add "Open in new tab" button to Product Switcher menu items (#35560)
* [MM-67883] Add buttons for Product Switcher to pop into new tabs * PR feedback * Fix snaps --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
co-authored by
Mattermost Build
parent
a744a75805
commit
33437b7ef6
+148
-12
@@ -46,7 +46,7 @@ exports[`components/global/product_switcher should have an active button state w
|
||||
role="menu"
|
||||
>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
role="menuitem"
|
||||
>
|
||||
@@ -86,7 +86,7 @@ exports[`components/global/product_switcher should have an active button state w
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Boards"
|
||||
role="menuitem"
|
||||
@@ -121,9 +121,26 @@ exports[`components/global/product_switcher should have an active button state w
|
||||
>
|
||||
Boards
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Playbooks"
|
||||
role="menuitem"
|
||||
@@ -148,6 +165,23 @@ exports[`components/global/product_switcher should have an active button state w
|
||||
>
|
||||
Playbooks
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
data-testid="product-menu-list"
|
||||
@@ -288,7 +322,7 @@ exports[`components/global/product_switcher should match snapshot with product s
|
||||
role="menu"
|
||||
>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
role="menuitem"
|
||||
>
|
||||
@@ -328,7 +362,7 @@ exports[`components/global/product_switcher should match snapshot with product s
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Boards"
|
||||
role="menuitem"
|
||||
@@ -363,9 +397,26 @@ exports[`components/global/product_switcher should match snapshot with product s
|
||||
>
|
||||
Boards
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Playbooks"
|
||||
role="menuitem"
|
||||
@@ -390,6 +441,23 @@ exports[`components/global/product_switcher should match snapshot with product s
|
||||
>
|
||||
Playbooks
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
data-testid="product-menu-list"
|
||||
@@ -491,7 +559,7 @@ exports[`components/global/product_switcher should render once when there are no
|
||||
role="menu"
|
||||
>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
role="menuitem"
|
||||
>
|
||||
@@ -531,7 +599,7 @@ exports[`components/global/product_switcher should render once when there are no
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Boards"
|
||||
role="menuitem"
|
||||
@@ -566,9 +634,26 @@ exports[`components/global/product_switcher should render once when there are no
|
||||
>
|
||||
Boards
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Playbooks"
|
||||
role="menuitem"
|
||||
@@ -593,6 +678,23 @@ exports[`components/global/product_switcher should render once when there are no
|
||||
>
|
||||
Playbooks
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
data-testid="product-menu-list"
|
||||
@@ -655,7 +757,7 @@ exports[`components/global/product_switcher should render the correct amount of
|
||||
role="menu"
|
||||
>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
role="menuitem"
|
||||
>
|
||||
@@ -695,7 +797,7 @@ exports[`components/global/product_switcher should render the correct amount of
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Boards"
|
||||
role="menuitem"
|
||||
@@ -730,9 +832,26 @@ exports[`components/global/product_switcher should render the correct amount of
|
||||
>
|
||||
Boards
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<a
|
||||
class="MenuItem-fJA-dRx RqdsE"
|
||||
class="MenuItem-fJA-dRx eXCaei"
|
||||
href="/"
|
||||
id="product-menu-item-Playbooks"
|
||||
role="menuitem"
|
||||
@@ -757,6 +876,23 @@ exports[`components/global/product_switcher should render the correct amount of
|
||||
>
|
||||
Playbooks
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open in new tab"
|
||||
class="OpenInNewTabButton-fKoVVp cLFfqU"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
data-testid="product-menu-list"
|
||||
|
||||
+32
-6
@@ -6,7 +6,8 @@ import React from 'react';
|
||||
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
||||
|
||||
import ProductMenuItem from './product_menu_item';
|
||||
import type {ProductMenuItemProps} from './product_menu_item';
|
||||
|
||||
type ProductMenuItemProps = React.ComponentProps<typeof ProductMenuItem>;
|
||||
|
||||
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(<ProductMenuItem {...defaultProps}/>);
|
||||
|
||||
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(<ProductMenuItem {...props}/>);
|
||||
|
||||
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(<ProductMenuItem {...props}/>);
|
||||
|
||||
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 = <div data-testid='tour-tip'>{tourTipContent}</div>;
|
||||
|
||||
+57
-10
@@ -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 (
|
||||
<MenuItem
|
||||
to={destination}
|
||||
@@ -74,11 +110,22 @@ const ProductMenuItem = ({icon, destination, text, active, onClick, tourTip, id}
|
||||
<MenuItemTextContainer>
|
||||
{text}
|
||||
</MenuItemTextContainer>
|
||||
{active && (
|
||||
{active ? (
|
||||
<CheckIcon
|
||||
size={18}
|
||||
color={'var(--button-bg)'}
|
||||
/>
|
||||
) : (
|
||||
<WithTooltip
|
||||
title={formatMessage({id: 'product_menu_item.open_in_new_tab', defaultMessage: 'Open in new tab'})}
|
||||
>
|
||||
<OpenInNewTabButton
|
||||
onClick={handleOpenInNewTab}
|
||||
aria-label={formatMessage({id: 'product_menu_item.open_in_new_tab', defaultMessage: 'Open in new tab'})}
|
||||
>
|
||||
<OpenInNewIcon size={16}/>
|
||||
</OpenInNewTabButton>
|
||||
</WithTooltip>
|
||||
)}
|
||||
{tourTip || null}
|
||||
</MenuItem>
|
||||
|
||||
@@ -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?",
|
||||
|
||||
Reference in New Issue
Block a user