From 4fdb79c472968625da299a0c9276b0166c542392 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 25 May 2023 10:51:21 +0200 Subject: [PATCH] SONAR-19388 Create new styled table in component library --- .../design-system/src/components/Table.tsx | 201 ++++++++++++++++++ .../src/components/__tests__/Table-test.tsx | 108 ++++++++++ .../design-system/src/components/index.ts | 1 + 3 files changed, 310 insertions(+) create mode 100644 server/sonar-web/design-system/src/components/Table.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Table-test.tsx diff --git a/server/sonar-web/design-system/src/components/Table.tsx b/server/sonar-web/design-system/src/components/Table.tsx new file mode 100644 index 00000000000..ac3a8060b54 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Table.tsx @@ -0,0 +1,201 @@ +/* + * 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 { ComponentProps, createContext, ReactNode, useContext } from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor } from '../helpers'; +import { FCProps } from '../types/misc'; + +interface TableBaseProps extends ComponentProps<'table'> { + header?: ReactNode; + noHeaderTopBorder?: boolean; + noSidePadding?: boolean; +} + +interface GenericTableProps extends TableBaseProps { + columnCount: number; + gridTemplate?: never; +} + +interface CustomTableProps extends TableBaseProps { + columnCount?: never; + gridTemplate: string; +} + +export type TableProps = GenericTableProps | CustomTableProps; + +export function Table(props: TableProps) { + const { className, header, children, noHeaderTopBorder, noSidePadding, ...rest } = props; + + return ( + + {header && ( + + {header} + + )} + {children} + + ); +} + +export const TableRow = styled.tr` + td, + th { + border-top: ${themeBorder('default')}; + } + + .no-header-top-border & th { + ${tw`sw-border-t-0`} + } + + td:first-of-type, + th:first-of-type, + td:last-child, + th:last-child { + border-right: ${themeBorder('default', 'transparent')}; + border-left: ${themeBorder('default', 'transparent')}; + } + + .no-side-padding & { + td:first-of-type, + th:first-of-type { + ${tw`sw-pl-0`} + } + + td:last-child, + th:last-child { + ${tw`sw-pr-0`} + } + } + + &:last-child > td { + border-bottom: ${themeBorder('default')}; + } +`; + +interface TableRowInteractiveProps extends FCProps { + selected?: boolean; +} + +function TableRowInteractiveBase({ + className, + children, + selected, + ...props +}: TableRowInteractiveProps) { + return ( + + {children} + + ); +} + +export const TableRowInteractive = styled(TableRowInteractiveBase)` + &:hover > td, + &.selected > td, + &.selected > th, + th.selected, + td.selected { + background: ${themeColor('tableRowHover')}; + } + + &.selected > td:first-of-type, + &.selected > th:first-of-type, + th.selected:first-of-type, + td.selected:first-of-type { + border-left: ${themeBorder('default', 'tableRowSelected')}; + } + + &.selected > td, + &.selected > th, + th.selected, + td.selected { + border-top: ${themeBorder('default', 'tableRowSelected')}; + border-bottom: ${themeBorder('default', 'tableRowSelected')}; + } + + &.selected > td:last-child, + &.selected > th:last-child, + th.selected:last-child, + td.selected:last-child { + border-right: ${themeBorder('default', 'tableRowSelected')}; + } + + &.selected + &:not(.selected) > td { + border-top: none; + } +`; + +export const ContentCell = styled(CellComponent)` + ${tw`sw-text-left`} +`; +export const NumericalCell = styled(CellComponent)` + ${tw`sw-text-right`} +`; +export const RatingCell = styled(CellComponent)` + ${tw`sw-text-right`} +`; +export const CheckboxCell = styled(CellComponent)` + ${tw`sw-text-center`} + ${tw`sw-flex`} + ${tw`sw-items-center sw-justify-center`} +`; + +const StyledTable = styled.table` + display: grid; + grid-template-columns: ${(props) => props.gridTemplate ?? `repeat(${props.columnCount}, 1fr)`}; + width: 100%; + border-collapse: collapse; + + thead, + tbody, + tr { + display: contents; + } +`; + +const CellComponentStyled = styled.td` + color: ${themeColor('pageContent')}; + + ${tw`sw-body-sm`} + ${tw`sw-py-4 sw-px-2`} + ${tw`sw-align-top`} + + thead > tr > & { + color: ${themeColor('pageTitle')}; + + ${tw`sw-body-sm-highlight`} + } +`; + +const CellTypeContext = createContext<'th' | 'td'>('td'); + +export function CellComponent(props: ComponentProps<'th' | 'td'>) { + const containerType = useContext(CellTypeContext); + return ; +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Table-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Table-test.tsx new file mode 100644 index 00000000000..1f16c3a9a2a --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Table-test.tsx @@ -0,0 +1,108 @@ +/* + * 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 { render } from '../../helpers/testUtils'; +import { + CheckboxCell, + ContentCell, + NumericalCell, + Table, + TableProps, + TableRow, + TableRowInteractive, +} from '../Table'; + +it('check that the html structure and style is correct for a regular table', () => { + renderTable({ + columnCount: 3, + 'aria-colcount': 3, + header: ( + + ContentCellHeader + NumericalCellHeader + CheckboxCellHeader + + ), + children: ( + <> + + ContentCell 1 + NumericalCell 1 + CheckboxCell 1 + + + ContentCell 2 + NumericalCell 2 + CheckboxCell 2 + + + ContentCell 3 + + + NumericalCell 4 + CheckboxCell 4 + + + ), + }); + + // Table should have accessible attribute + expect(screen.getByRole('table')).toHaveAttribute('aria-colcount', '3'); + + // Rows should have accessible attributes + expect( + screen.getByRole('row', { name: 'ContentCellHeader NumericalCellHeader CheckboxCellHeader' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: 'ContentCell 1 NumericalCell 1 CheckboxCell 1', + }) + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: 'ContentCell 1 NumericalCell 1 CheckboxCell 1', + }) + ).not.toHaveAttribute('aria-selected'); + expect( + screen.getByRole('row', { + selected: true, + name: 'ContentCell 2 NumericalCell 2 CheckboxCell 2', + }) + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: 'NumericalCell 4 CheckboxCell 4', + }) + ).toBeInTheDocument(); + + // Cells should have accessible attributes + expect(screen.getByRole('cell', { name: 'NumericalCell 4' })).toHaveAttribute( + 'aria-colindex', + '2' + ); + expect(screen.getByRole('cell', { name: 'CheckboxCell 4' })).toHaveAttribute( + 'aria-colindex', + '3' + ); +}); + +function renderTable(props: TableProps) { + return render({props.children}
); +} diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 0cf94260576..5b5ed58705a 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -66,6 +66,7 @@ export * from './SelectionCard'; export * from './Separator'; export * from './SizeIndicator'; export * from './SonarQubeLogo'; +export * from './Table'; export * from './Tags'; export * from './TagsSelector'; export * from './Text'; -- 2.39.5