From ec89bff4f8bbe324bf9a4f48cd137e192c283d30 Mon Sep 17 00:00:00 2001 From: stanislavh Date: Fri, 26 Jan 2024 13:54:12 +0100 Subject: SONAR-21467 Implement new Tabs component --- .../design-system/src/components/Badge.tsx | 31 +++-- .../design-system/src/components/Tabs.tsx | 135 +++++++++++++++++++++ .../src/components/__tests__/Tabs-test.tsx | 49 ++++++++ .../design-system/src/components/index.ts | 1 + server/sonar-web/design-system/src/theme/light.ts | 8 ++ 5 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 server/sonar-web/design-system/src/components/Tabs.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Tabs-test.tsx (limited to 'server/sonar-web/design-system/src') 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 = { 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 {children}; - } + + const Component = ['counter', 'counterFailed'].includes(variant) ? StyledCounter : StyledBadge; + return ( - + {children} - + ); } @@ -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 { + counter?: number; + disabled?: boolean; + label: string | React.ReactNode; + value: T; +} + +export interface TabsProps { + className?: string; + disabled?: boolean; + label?: string; + onChange: (value: T) => void; + options: ReadonlyArray>; + value?: T; +} + +export function Tabs(props: PropsWithChildren>) { + const { disabled = false, label, options, value, className, children } = props; + + return ( + + + {options.map((option) => ( + { + if (option.value !== value) { + props.onChange(option.value); + } + }} + role="tab" + selected={option.value === value} + > + {option.label} + {option.counter ? ( + + {option.counter} + + ) : null} + + ))} + + {children} + + ); +} + +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> = [ + { 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> = {}) { + return render(); +} 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], -- cgit v1.2.3