--- /dev/null
+/*
+ * 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<HTMLUListElement> {
+ children?: React.ReactNode;
+ className?: string;
+}
+
+export function NavBarTabs({ children, className, ...other }: Props) {
+ return (
+ <ul className={classNames('sw-flex sw-items-end sw-gap-6', className)} {...other}>
+ {children}
+ </ul>
+ );
+}
+
+interface NavBarTabLinkProps extends Omit<NavLinkProps, 'children'> {
+ active?: boolean;
+ children?: React.ReactNode;
+ text: string;
+ withChevron?: boolean;
+}
+
+export function NavBarTabLink(props: NavBarTabLinkProps) {
+ const { active, children, text, withChevron = false, ...linkProps } = props;
+ return (
+ <NavBarTabLinkWrapper>
+ <NavLink
+ className={({ isActive }) =>
+ classNames('sw-flex sw-items-center', { active: isDefined(active) ? active : isActive })
+ }
+ {...linkProps}
+ >
+ <span className="sw-inline-block sw-text-center" data-text={text}>
+ {text}
+ </span>
+ {children}
+ {withChevron && <ChevronDownIcon className="sw-ml-1" />}
+ </NavLink>
+ </NavBarTabLinkWrapper>
+ );
+}
+
+export function DisabledTabLink(props: { label: string; overlay: React.ReactNode }) {
+ return (
+ <NavBarTabLinkWrapper>
+ <Tooltip overlay={props.overlay}>
+ <a aria-disabled="true" className="disabled-link" role="link">
+ {props.label}
+ </a>
+ </Tooltip>
+ </NavBarTabLinkWrapper>
+ );
+}
+
+// Styling for <NavLink> 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')};
+ }
+`;
+++ /dev/null
-/*
- * 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(
- <MainMenuItem>
- <a>Hi</a>
- </MainMenuItem>
- );
-
- expect(screen.getByText('Hi')).toHaveStyle({
- color: 'rgb(62, 67, 87)',
- 'border-bottom': '4px solid transparent',
- });
-});
-
-it('should render active link', () => {
- render(
- <MainMenuItem>
- <a className="active">Hi</a>
- </MainMenuItem>
- );
-
- 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(
- <MainMenuItem>
- <a className="hover">Hi</a>
- </MainMenuItem>
- );
-
- expect(screen.getByText('Hi')).toHaveStyle({
- color: 'rgb(42, 47, 64)',
- 'border-bottom': '4px solid rgba(123,135,217,1)',
- });
-});
--- /dev/null
+/*
+ * 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(<MainMenu>Children</MainMenu>);
+
+ expect(screen.getByText('Children')).toBeInTheDocument();
+});
--- /dev/null
+/*
+ * 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(
+ <MainMenuItem>
+ <a>Hi</a>
+ </MainMenuItem>
+ );
+
+ expect(screen.getByText('Hi')).toHaveStyle({
+ color: 'rgb(62, 67, 87)',
+ 'border-bottom': '4px solid transparent',
+ });
+});
+
+it('should render active link', () => {
+ render(
+ <MainMenuItem>
+ <a className="active">Hi</a>
+ </MainMenuItem>
+ );
+
+ 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(
+ <MainMenuItem>
+ <a className="hover">Hi</a>
+ </MainMenuItem>
+ );
+
+ expect(screen.getByText('Hi')).toHaveStyle({
+ color: 'rgb(42, 47, 64)',
+ 'border-bottom': '4px solid rgba(123,135,217,1)',
+ });
+});
--- /dev/null
+/*
+ * 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(
+ <NavBarTabs>
+ <NavBarTabLink text="test" to="/summary/new_code" />
+ </NavBarTabs>
+ );
+ }
+});
+
+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<FCProps<typeof NavBarTabLink>> = {}) {
+ return renderWithRouter(<NavBarTabLink text="test" to="/summary/new_code" {...props} />);
+ }
+});
+
+describe('DisabledTabLink', () => {
+ it('should render correctly', () => {
+ renderWithRouter(<DisabledTabLink label="label" overlay={<span>Overlay</span>} />);
+ expect(screen.getByRole('link')).toHaveClass('disabled-link');
+ });
+});
--- /dev/null
+/*
+ * 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 (
+ <CustomIcon {...iconProps}>
+ <path
+ clipRule="evenodd"
+ d="M12.7236 5.83199c.1953.19527.1953.51185 0 .70711l-4.18499 4.185c-.19526.1953-.51184.1953-.7071 0l-4.18503-4.185c-.19527-.19526-.19527-.51184 0-.70711.19526-.19526.51184-.19526.7071 0l3.83148 3.83148 3.83144-3.83148c.1953-.19526.5119-.19526.7071 0Z"
+ fill={themeColor(fill)({ theme })}
+ fillRule="evenodd"
+ />
+ </CustomIcon>
+ );
+}
* 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';
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';
subnavigationBorder: COLORS.grey[100],
subnavigationSeparator: COLORS.grey[50],
subnavigationSubheading: COLORS.blueGrey[25],
+ subnavigationDisabled: COLORS.blueGrey[300],
// footer
footer: COLORS.white,
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],
},
* 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';
pathname,
additionalQueryParams = {},
}: {
- label: React.ReactNode;
+ label: string;
pathname: string;
additionalQueryParams?: Dict<string>;
}) => {
if (isApplicationChildInaccessble) {
return this.renderLinkWhenInaccessibleChild(label);
}
- return (
- <li>
- {hasAnalysis ? (
- <NavLink
- to={{
- pathname,
- search: new URLSearchParams({ ...query, ...additionalQueryParams }).toString(),
- }}
- >
- {label}
- </NavLink>
- ) : (
- <Tooltip overlay={translate('layout.must_be_configured')}>
- <a aria-disabled="true" className="disabled-link">
- {label}
- </a>
- </Tooltip>
- )}
- </li>
+ return hasAnalysis ? (
+ <NavBarTabLink
+ to={{
+ pathname,
+ search: new URLSearchParams({ ...query, ...additionalQueryParams }).toString(),
+ }}
+ text={label}
+ />
+ ) : (
+ <DisabledTabLink overlay={translate('layout.must_be_configured')} label={label} />
);
};
if (this.isPortfolio()) {
return this.isGovernanceEnabled() ? (
- <li>
- <NavLink to={getPortfolioUrl(id)}>{translate('overview.page')}</NavLink>
- </li>
+ <NavBarTabLink to={getPortfolioUrl(id)} text={translate('overview.page')} />
) : null;
}
return this.renderLinkWhenInaccessibleChild(translate('overview.page'));
}
return (
- <li>
- <NavLink to={getProjectQueryUrl(id, branchLike)}>{translate('overview.page')}</NavLink>
- </li>
+ <NavBarTabLink to={getProjectQueryUrl(id, branchLike)} text={translate('overview.page')} />
);
};
render() {
return (
<div className="display-flex-center display-flex-space-between">
- <NavBarTabs>
+ <NavBarTabs className="it__navbar-tabs">
{this.renderDashboardLink()}
{this.renderBreakdownLink()}
{this.renderIssuesLink()}
{this.renderActivityLink()}
{this.renderExtensions()}
</NavBarTabs>
- <NavBarTabs>
+ <SQNavBarTabs>
{this.renderAdministration()}
{this.renderProjectInformationButton()}
- </NavBarTabs>
+ </SQNavBarTabs>
</div>
);
}
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(),
},
});
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();
});
export default function NavBarTabs({ children, className, ...other }: Props) {
return (
- <ul {...other} className={classNames('navbar-tabs', className)}>
+ <ul {...other} className={classNames('it__navbar-tabs navbar-tabs', className)}>
{children}
</ul>
);