aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/design-system/src
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-01-26 13:54:12 +0100
committersonartech <sonartech@sonarsource.com>2024-01-29 20:03:16 +0000
commitec89bff4f8bbe324bf9a4f48cd137e192c283d30 (patch)
treeebf119a11db03768415f9884d9e8cb2476a08ae1 /server/sonar-web/design-system/src
parent9234a58514677a11b4ed680af855e22cd5d538ce (diff)
downloadsonarqube-ec89bff4f8bbe324bf9a4f48cd137e192c283d30.tar.gz
sonarqube-ec89bff4f8bbe324bf9a4f48cd137e192c283d30.zip
SONAR-21467 Implement new Tabs component
Diffstat (limited to 'server/sonar-web/design-system/src')
-rw-r--r--server/sonar-web/design-system/src/components/Badge.tsx31
-rw-r--r--server/sonar-web/design-system/src/components/Tabs.tsx135
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Tabs-test.tsx49
-rw-r--r--server/sonar-web/design-system/src/components/index.ts1
-rw-r--r--server/sonar-web/design-system/src/theme/light.ts8
5 files changed, 213 insertions, 11 deletions
diff --git a/server/sonar-web/design-system/src/components/Badge.tsx b/server/sonar-web/design-system/src/components/Badge.tsx
index 4d8ea88fed3..b1cc762001e 100644
--- a/server/sonar-web/design-system/src/components/Badge.tsx
+++ b/server/sonar-web/design-system/src/components/Badge.tsx
@@ -19,16 +19,17 @@
*/
import styled from '@emotion/styled';
import tw from 'twin.macro';
-import { themeColor, themeContrast } from '../helpers/theme';
+import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
import { ThemeColors } from '../types/theme';
-type BadgeVariant = 'default' | 'new' | 'deleted' | 'counter';
+type BadgeVariant = 'default' | 'new' | 'deleted' | 'counter' | 'counterFailed';
const variantList: Record<BadgeVariant, ThemeColors> = {
default: 'badgeDefault',
new: 'badgeNew',
deleted: 'badgeDeleted',
counter: 'badgeCounter',
+ counterFailed: 'badgeCounterFailed',
};
interface BadgeProps {
@@ -45,13 +46,13 @@ export function Badge({ className, children, title, variant = 'default' }: Badge
role: 'status',
title,
};
- if (variant === 'counter') {
- return <StyledCounter {...commonProps}>{children}</StyledCounter>;
- }
+
+ const Component = ['counter', 'counterFailed'].includes(variant) ? StyledCounter : StyledBadge;
+
return (
- <StyledBadge variantInfo={variantList[variant]} {...commonProps}>
+ <Component variantInfo={variantList[variant]} {...commonProps}>
{children}
- </StyledBadge>
+ </Component>
);
}
@@ -80,17 +81,25 @@ const StyledBadge = styled.span<{
}
`;
-const StyledCounter = styled.span`
+const StyledCounter = styled.span<{
+ variantInfo: ThemeColors;
+}>`
+ ${tw`sw-min-w-5 sw-min-h-5`};
${tw`sw-text-[0.75rem]`};
${tw`sw-font-regular`};
- ${tw`sw-px-2`};
+ ${tw`sw-box-border sw-px-[5px]`};
${tw`sw-inline-flex`};
${tw`sw-leading-[1.125rem]`};
${tw`sw-items-center sw-justify-center`};
${tw`sw-rounded-pill`};
- color: ${themeContrast('badgeCounter')};
- background-color: ${themeColor('badgeCounter')};
+ color: ${({ variantInfo }) => themeContrast(variantInfo)};
+ background-color: ${({ variantInfo }) => themeColor(variantInfo)};
+ border: ${({ variantInfo }) =>
+ themeBorder(
+ 'default',
+ variantInfo === 'badgeCounterFailed' ? 'badgeCounterFailedBorder' : 'transparent',
+ )};
&:empty {
${tw`sw-hidden`}
diff --git a/server/sonar-web/design-system/src/components/Tabs.tsx b/server/sonar-web/design-system/src/components/Tabs.tsx
new file mode 100644
index 00000000000..dd0381f3284
--- /dev/null
+++ b/server/sonar-web/design-system/src/components/Tabs.tsx
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { PropsWithChildren } from 'react';
+import tw from 'twin.macro';
+import { OPACITY_20_PERCENT, themeBorder, themeColor } from '../helpers';
+import { getTabId, getTabPanelId } from '../helpers/tabs';
+import { Badge } from './Badge';
+import { BareButton } from './buttons';
+
+type TabValueType = string | number | boolean;
+
+export interface TabOption<T extends TabValueType> {
+ counter?: number;
+ disabled?: boolean;
+ label: string | React.ReactNode;
+ value: T;
+}
+
+export interface TabsProps<T extends TabValueType> {
+ className?: string;
+ disabled?: boolean;
+ label?: string;
+ onChange: (value: T) => void;
+ options: ReadonlyArray<TabOption<T>>;
+ value?: T;
+}
+
+export function Tabs<T extends TabValueType>(props: PropsWithChildren<TabsProps<T>>) {
+ const { disabled = false, label, options, value, className, children } = props;
+
+ return (
+ <TabsContainer className={className}>
+ <TabList aria-label={label} role="tablist">
+ {options.map((option) => (
+ <TabButton
+ aria-controls={getTabPanelId(String(option.value))}
+ aria-current={option.value === value}
+ aria-selected={option.value === value}
+ data-value={option.value}
+ disabled={disabled || option.disabled}
+ id={getTabId(String(option.value))}
+ key={option.value.toString()}
+ onClick={() => {
+ if (option.value !== value) {
+ props.onChange(option.value);
+ }
+ }}
+ role="tab"
+ selected={option.value === value}
+ >
+ {option.label}
+ {option.counter ? (
+ <Badge className="sw-ml-2" variant="counterFailed">
+ {option.counter}
+ </Badge>
+ ) : null}
+ </TabButton>
+ ))}
+ </TabList>
+ <RightSection>{children}</RightSection>
+ </TabsContainer>
+ );
+}
+
+const TabsContainer = styled.div`
+ ${tw`sw-w-full`};
+ ${tw`sw-flex sw-justify-between`};
+ border-bottom: ${themeBorder('default')};
+`;
+
+const TabList = styled.div`
+ ${tw`sw-inline-flex`};
+`;
+
+const TabButton = styled(BareButton)<{ selected: boolean }>`
+ ${tw`sw-relative`};
+ ${tw`sw-px-3 sw-py-1 sw-mb-[-1px]`};
+ ${tw`sw-flex sw-items-center`};
+ ${tw`sw-body-sm`};
+ ${tw`sw-font-semibold`};
+ ${tw`sw-rounded-t-1`};
+
+ height: 34px;
+ background: ${(props) => (props.selected ? themeColor('backgroundSecondary') : 'none')};
+ color: ${(props) => (props.selected ? themeColor('tabSelected') : themeColor('tab'))};
+ border: ${(props) =>
+ props.selected ? themeBorder('default') : themeBorder('default', 'transparent')};
+ border-bottom: ${(props) =>
+ themeBorder('default', props.selected ? 'backgroundSecondary' : undefined)};
+
+ &:hover {
+ background: ${themeColor('tabHover')};
+ }
+
+ &:focus,
+ &:active {
+ outline: ${themeBorder('xsActive', 'tabSelected', OPACITY_20_PERCENT)};
+ z-index: 1;
+ }
+
+ // Active line
+ &::after {
+ content: '';
+ ${tw`sw-absolute`};
+ ${tw`sw-rounded-t-1`};
+ top: 0;
+ left: 0;
+ width: calc(100%);
+ height: 2px;
+ background: ${(props) => (props.selected ? themeColor('tabSelected') : 'none')};
+ }
+`;
+
+const RightSection = styled.div`
+ max-height: 43px;
+ ${tw`sw-flex sw-items-center`};
+`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/Tabs-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Tabs-test.tsx
new file mode 100644
index 00000000000..5b6eb4cc2b2
--- /dev/null
+++ b/server/sonar-web/design-system/src/components/__tests__/Tabs-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 userEvent from '@testing-library/user-event';
+import { render } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { TabOption, Tabs } from '../Tabs';
+
+it('should render all options', async () => {
+ const user = userEvent.setup();
+ const onChange = jest.fn();
+ const options: Array<TabOption<number>> = [
+ { value: 1, label: 'first' },
+ { value: 2, label: 'disabled', disabled: true },
+ { value: 3, label: 'has counter', counter: 7 },
+ ];
+ renderToggleButtons({ onChange, options, value: 1 });
+
+ expect(screen.getAllByRole('tab')).toHaveLength(3);
+
+ await user.click(screen.getByText('first'));
+
+ expect(onChange).not.toHaveBeenCalled();
+
+ await user.click(screen.getByText('has counter'));
+
+ expect(onChange).toHaveBeenCalledWith(3);
+});
+
+function renderToggleButtons(props: Partial<FCProps<typeof Tabs>> = {}) {
+ return render(<Tabs onChange={jest.fn()} options={[]} {...props} />);
+}
diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts
index 92661b36e23..c23b99a2d68 100644
--- a/server/sonar-web/design-system/src/components/index.ts
+++ b/server/sonar-web/design-system/src/components/index.ts
@@ -73,6 +73,7 @@ export { Spinner } from './Spinner';
export * from './SpotlightTour';
export * from './Switch';
export * from './Table';
+export * from './Tabs';
export * from './Tags';
export * from './Text';
export * from './TextAccordion';
diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts
index 35ec15d93bb..1eb37c7c559 100644
--- a/server/sonar-web/design-system/src/theme/light.ts
+++ b/server/sonar-web/design-system/src/theme/light.ts
@@ -305,6 +305,8 @@ export const lightTheme = {
badgeDefault: COLORS.blueGrey[100],
badgeDeleted: COLORS.red[100],
badgeCounter: COLORS.blueGrey[100],
+ badgeCounterFailed: COLORS.red[50],
+ badgeCounterFailedBorder: COLORS.red[200],
// pills
pillDanger: COLORS.red[50],
@@ -325,6 +327,11 @@ export const lightTheme = {
// tab
tabBorder: primary.light,
+ // tabs
+ tab: COLORS.blueGrey[400],
+ tabSelected: primary.default,
+ tabHover: COLORS.blueGrey[25],
+
//table
tableRowHover: COLORS.indigo[25],
tableRowSelected: COLORS.indigo[300],
@@ -709,6 +716,7 @@ export const lightTheme = {
badgeDefault: COLORS.blueGrey[700],
badgeDeleted: COLORS.red[900],
badgeCounter: secondary.darker,
+ badgeCounterFailed: danger.dark,
// pills
pillDanger: COLORS.red[800],