From: Kevin Silva Date: Tue, 21 Mar 2023 14:11:45 +0000 (+0100) Subject: SONAR-18776 Change the sub navigation menu X-Git-Tag: 10.0.0.68432~62 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=54686faf91547cd918e84c8e081a722ce974c89d;p=sonarqube.git SONAR-18776 Change the sub navigation menu --- diff --git a/server/sonar-web/design-system/src/components/NavBarTabs.tsx b/server/sonar-web/design-system/src/components/NavBarTabs.tsx new file mode 100644 index 00000000000..669f2d3642c --- /dev/null +++ b/server/sonar-web/design-system/src/components/NavBarTabs.tsx @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import tw, { theme } from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import { isDefined } from '../helpers/types'; +import ChevronDownIcon from './icons/ChevronDownIcon'; +import NavLink, { NavLinkProps } from './NavLink'; +import Tooltip from './Tooltip'; + +interface Props extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +export function NavBarTabs({ children, className, ...other }: Props) { + return ( + + ); +} + +interface NavBarTabLinkProps extends Omit { + active?: boolean; + children?: React.ReactNode; + text: string; + withChevron?: boolean; +} + +export function NavBarTabLink(props: NavBarTabLinkProps) { + const { active, children, text, withChevron = false, ...linkProps } = props; + return ( + + + classNames('sw-flex sw-items-center', { active: isDefined(active) ? active : isActive }) + } + {...linkProps} + > + + {text} + + {children} + {withChevron && } + + + ); +} + +export function DisabledTabLink(props: { label: string; overlay: React.ReactNode }) { + return ( + + + + {props.label} + + + + ); +} + +// Styling for due to its special className function, it conflicts when styled with Emotion. +const NavBarTabLinkWrapper = styled.li` + & > a { + ${tw`sw-pb-3`}; + ${tw`sw-block`}; + ${tw`sw-box-border`}; + ${tw`sw-transition-none`}; + ${tw`sw-body-md`}; + color: ${themeContrast('buttonSecondary')}; + text-decoration: none; + border-bottom: ${themeBorder('xsActive', 'transparent')}; + padding-bottom: calc(${theme('spacing.3')} + 1px); // 12px spacing + 3px border + 1px = 16px + } + & > a.active, + & > a:active, + & > a:hover, + & > a:focus { + border-bottom-color: ${themeColor('tabBorder')}; + } + & > a.active > span[data-text], + & > a:active > span { + ${tw`sw-body-md-highlight`}; + } + // This is a hack to have a link take the space of the bold font, so when active other ones are not moving + & > a > span[data-text]::before { + ${tw`sw-block`}; + ${tw`sw-body-md-highlight`}; + ${tw`sw-h-0`}; + ${tw`sw-overflow-hidden`}; + ${tw`sw-invisible`}; + content: attr(data-text); + } + &:has(a.disabled-link) > a, + &:has(a.disabled-link) > a:hover, + &:has(a.disabled-link) > a.hover, + &:has(a.disabled-link)[aria-expanded='true'] { + ${tw`sw-cursor-default`}; + border-bottom: ${themeBorder('xsActive', 'transparent', 1)}; + color: ${themeContrast('subnavigationDisabled')}; + } +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx b/server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx deleted file mode 100644 index 562613e9ea8..00000000000 --- a/server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -/* eslint-disable import/no-extraneous-dependencies */ - -import { screen } from '@testing-library/react'; -import { render } from '../../helpers/testUtils'; -import { MainMenuItem } from '../MainMenuItem'; - -it('should render default', () => { - render( - - Hi - - ); - - expect(screen.getByText('Hi')).toHaveStyle({ - color: 'rgb(62, 67, 87)', - 'border-bottom': '4px solid transparent', - }); -}); - -it('should render active link', () => { - render( - - Hi - - ); - - expect(screen.getByText('Hi')).toHaveStyle({ - color: 'rgb(62, 67, 87)', - 'border-bottom': '4px solid rgba(123,135,217,1)', - }); -}); - -it('should render hovered link', () => { - render( - - Hi - - ); - - expect(screen.getByText('Hi')).toHaveStyle({ - color: 'rgb(42, 47, 64)', - 'border-bottom': '4px solid rgba(123,135,217,1)', - }); -}); diff --git a/server/sonar-web/design-system/src/components/__tests__/Menu-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Menu-test.tsx new file mode 100644 index 00000000000..b53bce89b4b --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Menu-test.tsx @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { MainMenu } from '../MainMenu'; + +it('should render MainMenu', () => { + render(Children); + + expect(screen.getByText('Children')).toBeInTheDocument(); +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/MenuItem-test.tsx b/server/sonar-web/design-system/src/components/__tests__/MenuItem-test.tsx new file mode 100644 index 00000000000..562613e9ea8 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/MenuItem-test.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { MainMenuItem } from '../MainMenuItem'; + +it('should render default', () => { + render( + + Hi + + ); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(62, 67, 87)', + 'border-bottom': '4px solid transparent', + }); +}); + +it('should render active link', () => { + render( + + Hi + + ); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(62, 67, 87)', + 'border-bottom': '4px solid rgba(123,135,217,1)', + }); +}); + +it('should render hovered link', () => { + render( + + Hi + + ); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(42, 47, 64)', + 'border-bottom': '4px solid rgba(123,135,217,1)', + }); +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/NavBarTabs-test.tsx b/server/sonar-web/design-system/src/components/__tests__/NavBarTabs-test.tsx new file mode 100644 index 00000000000..0c2a64c124d --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/NavBarTabs-test.tsx @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { screen } from '@testing-library/react'; +import { renderWithRouter } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { DisabledTabLink, NavBarTabLink, NavBarTabs } from '../NavBarTabs'; + +describe('NewNavBarTabs', () => { + it('should render correctly', () => { + setup(); + + expect(screen.getByRole('list')).toBeInTheDocument(); + expect(screen.getByRole('listitem')).toBeInTheDocument(); + expect(screen.getByRole('link')).toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveTextContent('test'); + }); + + function setup() { + return renderWithRouter( + + + + ); + } +}); + +describe('NewNavBarTabLink', () => { + it('should not be active when on different url', () => { + setupWithProps(); + + expect(screen.getByRole('link')).not.toHaveClass('active'); + }); + + it('should be active when on same url', () => { + setupWithProps({ to: '/' }); + + expect(screen.getByRole('link')).toHaveClass('active'); + }); + + it('should be active when active prop is set regardless of the url', () => { + setupWithProps({ active: true, withChevron: true }); + + expect(screen.getByRole('link')).toHaveClass('active'); + }); + + it('should not be active when active prop is false regardless of the url', () => { + setupWithProps({ active: false, to: '/' }); + + expect(screen.getByRole('link')).not.toHaveClass('active'); + }); + + function setupWithProps(props: Partial> = {}) { + return renderWithRouter(); + } +}); + +describe('DisabledTabLink', () => { + it('should render correctly', () => { + renderWithRouter(Overlay} />); + expect(screen.getByRole('link')).toHaveClass('disabled-link'); + }); +}); diff --git a/server/sonar-web/design-system/src/components/icons/ChevronDownIcon.tsx b/server/sonar-web/design-system/src/components/icons/ChevronDownIcon.tsx new file mode 100644 index 00000000000..863c092f78e --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/ChevronDownIcon.tsx @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export default function ChevronDownIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index 3b681fbe1b8..a8ba3597407 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +export { default as ChevronDownIcon } from './ChevronDownIcon'; export { default as ClockIcon } from './ClockIcon'; export { FlagErrorIcon } from './FlagErrorIcon'; export { FlagInfoIcon } from './FlagInfoIcon'; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 67b83f5d7af..4cb65f8730c 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -32,7 +32,8 @@ export * from './InteractiveIcon'; export { default as Link } from './Link'; export * from './MainAppBar'; export * from './MainMenu'; -export { MainMenuItem } from './MainMenuItem'; +export * from './MainMenuItem'; +export * from './NavBarTabs'; export * from './popups'; export * from './SonarQubeLogo'; export * from './Text'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 7a86822c0b4..ea031787166 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -365,6 +365,7 @@ const lightTheme = { subnavigationBorder: COLORS.grey[100], subnavigationSeparator: COLORS.grey[50], subnavigationSubheading: COLORS.blueGrey[25], + subnavigationDisabled: COLORS.blueGrey[300], // footer footer: COLORS.white, @@ -675,6 +676,7 @@ const lightTheme = { borders: { default: ['1px', 'solid', ...COLORS.grey[50]], active: ['4px', 'solid', ...primary.light], + xsActive: ['3px', 'solid', ...primary.light], focus: ['4px', 'solid', ...secondary.default, OPACITY_20_PERCENT], }, diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx index 9e3da2c2c60..c23e4665989 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx @@ -18,14 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { DisabledTabLink, NavBarTabLink, NavBarTabs, Tooltip } from 'design-system'; import * as React from 'react'; import { NavLink } from 'react-router-dom'; import { ButtonLink } from '../../../../components/controls/buttons'; import Dropdown from '../../../../components/controls/Dropdown'; -import Tooltip from '../../../../components/controls/Tooltip'; import BulletListIcon from '../../../../components/icons/BulletListIcon'; import DropdownIcon from '../../../../components/icons/DropdownIcon'; -import NavBarTabs from '../../../../components/ui/NavBarTabs'; +import SQNavBarTabs from '../../../../components/ui/NavBarTabs'; import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like'; import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n'; import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; @@ -148,7 +148,7 @@ export class Menu extends React.PureComponent { pathname, additionalQueryParams = {}, }: { - label: React.ReactNode; + label: string; pathname: string; additionalQueryParams?: Dict; }) => { @@ -158,25 +158,16 @@ export class Menu extends React.PureComponent { if (isApplicationChildInaccessble) { return this.renderLinkWhenInaccessibleChild(label); } - return ( -
  • - {hasAnalysis ? ( - - {label} - - ) : ( - - - {label} - - - )} -
  • + return hasAnalysis ? ( + + ) : ( + ); }; @@ -185,9 +176,7 @@ export class Menu extends React.PureComponent { if (this.isPortfolio()) { return this.isGovernanceEnabled() ? ( -
  • - {translate('overview.page')} -
  • + ) : null; } @@ -196,9 +185,7 @@ export class Menu extends React.PureComponent { return this.renderLinkWhenInaccessibleChild(translate('overview.page')); } return ( -
  • - {translate('overview.page')} -
  • + ); }; @@ -638,7 +625,7 @@ export class Menu extends React.PureComponent { render() { return (
    - + {this.renderDashboardLink()} {this.renderBreakdownLink()} {this.renderIssuesLink()} @@ -649,10 +636,10 @@ export class Menu extends React.PureComponent { {this.renderActivityLink()} {this.renderExtensions()} - + {this.renderAdministration()} {this.renderProjectInformationButton()} - +
    ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx index c3c0fb61b63..ad00e62a704 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx @@ -65,6 +65,29 @@ it('should render correctly', () => { expect(screen.getByRole('link', { name: 'ComponentBar' })).toBeInTheDocument(); }); +it('should render correctly when on a Portofolio', () => { + const component = { + ...BASE_COMPONENT, + configuration: { + showSettings: true, + extensions: [ + { key: 'foo', name: 'Foo' }, + { key: 'bar', name: 'Bar' }, + ], + }, + qualifier: ComponentQualifier.Portfolio, + extensions: [ + { key: 'governance/foo', name: 'governance foo' }, + { key: 'governance/bar', name: 'governance bar' }, + ], + }; + renderMenu({ component }); + expect(screen.getByRole('link', { name: 'overview.page' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'issues.page' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'layout.measures' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'portfolio_breakdown.page' })).toBeInTheDocument(); +}); + it('should render correctly when on a branch', () => { renderMenu({ branchLike: mockBranch(), @@ -115,8 +138,8 @@ it('should disable links if no analysis has been done', () => { }, }); expect(screen.getByRole('link', { name: 'overview.page' })).toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'issues.page' })).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'layout.measures' })).not.toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'issues.page' })).toHaveClass('disabled-link'); + expect(screen.queryByRole('link', { name: 'layout.measures' })).toHaveClass('disabled-link'); expect(screen.getByRole('button', { name: 'project.info.title' })).toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/components/ui/NavBarTabs.tsx b/server/sonar-web/src/main/js/components/ui/NavBarTabs.tsx index 80d92c8995e..325b7989ad4 100644 --- a/server/sonar-web/src/main/js/components/ui/NavBarTabs.tsx +++ b/server/sonar-web/src/main/js/components/ui/NavBarTabs.tsx @@ -28,7 +28,7 @@ interface Props extends React.HTMLAttributes { export default function NavBarTabs({ children, className, ...other }: Props) { return ( -
      +
        {children}
      );