From: Ismail Cherri Date: Mon, 15 Apr 2024 15:57:32 +0000 (+0200) Subject: SONAR-22049 Allow table to use grid template and column widths X-Git-Tag: 10.6.0.92116~165 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ac0a2bdb2cb1ad49a2effc5ec48e087308ac767c;p=sonarqube.git SONAR-22049 Allow table to use grid template and column widths --- diff --git a/server/sonar-web/design-system/src/components/Table.tsx b/server/sonar-web/design-system/src/components/Table.tsx deleted file mode 100644 index 44c09e399fe..00000000000 --- a/server/sonar-web/design-system/src/components/Table.tsx +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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 classNames from 'classnames'; -import { times } from 'lodash'; -import { ComponentProps, createContext, ReactNode, useContext } from 'react'; -import tw from 'twin.macro'; -import { themeBorder, themeColor } from '../helpers'; -import { FCProps } from '../types/misc'; - -export interface TableProps extends ComponentProps<'table'> { - caption?: ReactNode; - columnCount: number; - columnWidths?: Array; - header?: ReactNode; - noHeaderTopBorder?: boolean; - noSidePadding?: boolean; - withRoundedBorder?: boolean; -} - -export function Table(props: TableProps) { - const { - className, - columnCount, - columnWidths = [], - header, - caption, - children, - noHeaderTopBorder, - noSidePadding, - withRoundedBorder, - ...rest - } = props; - - return ( - - - {times(columnCount, (i) => ( - - ))} - - - {caption && ( - -
- {caption} -
- - )} - - {header && ( - - {header} - - )} - - {children} -
- ); -} - -export const TableSeparator = styled.tr` - ${tw`sw-h-4`} - border-top: ${themeBorder('default')}; -`; - -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; - } -`; - -const CellTypeContext = createContext<'th' | 'td'>('td'); -type CellComponentProps = ComponentProps<'th' | 'td'>; - -export function CellComponent(props: CellComponentProps) { - const containerType = useContext(CellTypeContext); - return ; -} - -export function ContentCell({ - children, - cellClassName, - className, - ...props -}: CellComponentProps & { cellClassName?: string }) { - return ( - -
- {children} -
-
- ); -} - -export function NumericalCell({ children, ...props }: CellComponentProps) { - return ( - -
{children}
-
- ); -} - -export function RatingCell({ children, ...props }: CellComponentProps) { - return ( - -
{children}
-
- ); -} - -export function ActionCell({ children, ...props }: CellComponentProps) { - return ( - -
{children}
-
- ); -} - -export function CheckboxCell({ children, ...props }: CellComponentProps) { - return ( - -
{children}
-
- ); -} - -const StyledTable = styled.table` - width: 100%; - border-collapse: collapse; - - &.with-rounded-border { - border-collapse: separate; - border: ${themeBorder('default', 'breakdownBorder')}; - ${tw`sw-rounded-1`}; - - th:first-of-type { - ${tw`sw-rounded-tl-1`}; - } - th:last-of-type { - ${tw`sw-rounded-tr-1`}; - } - - tr:last-child > td { - border-bottom: none; - } - } -`; - -const CellComponentStyled = styled.td` - color: ${themeColor('pageContent')}; - ${tw`sw-body-sm`} - ${tw`sw-py-4 sw-px-2`} - ${tw`sw-align-middle`} - - thead > tr > & { - color: ${themeColor('pageTitle')}; - - ${tw`sw-body-sm-highlight`} - } -`; 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 deleted file mode 100644 index c8e3bf8e418..00000000000 --- a/server/sonar-web/design-system/src/components/__tests__/Table-test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 { 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 ae2932330ec..35e06bdc597 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -71,7 +71,6 @@ export * from './SonarQubeLogo'; export { Spinner } from './Spinner'; export * from './SpotlightTour'; export * from './Switch'; -export * from './Table'; export * from './Tabs'; export * from './Tags'; export * from './Text'; diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/Table.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/Table.tsx new file mode 100644 index 00000000000..6de60ff4b2e --- /dev/null +++ b/server/sonar-web/design-system/src/sonar-aligned/components/Table.tsx @@ -0,0 +1,298 @@ +/* + * 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 classNames from 'classnames'; +import { isNumber, times } from 'lodash'; +import { ComponentProps, ReactNode, createContext, useContext } from 'react'; +import tw from 'twin.macro'; +import { FCProps } from '~types/misc'; +import { themeBorder, themeColor } from '../../helpers/theme'; + +interface TableBaseProps extends ComponentProps<'table'> { + caption?: ReactNode; + header?: ReactNode; + noHeaderTopBorder?: boolean; + noSidePadding?: boolean; + withRoundedBorder?: boolean; +} + +interface ColumnWidthsProps extends TableBaseProps { + columnCount: number; + columnWidths?: Array; + gridTemplate?: never; +} + +interface GridTemplateProps extends TableBaseProps { + columnCount?: never; + columnWidths?: never; + gridTemplate: string; +} + +export type TableProps = ColumnWidthsProps | GridTemplateProps; + +export function Table(props: Readonly) { + const { columnCount, gridTemplate } = props; + const { + className, + columnWidths, + header, + caption, + children, + noHeaderTopBorder, + noSidePadding, + withRoundedBorder, + ...rest + } = props; + + return ( + + {isNumber(columnCount) && ( + + {times(columnCount, (i) => ( + + ))} + + )} + + {caption && ( + +
+ {caption} +
+ + )} + + {header && ( + + {header} + + )} + + {children} +
+ ); +} + +export const TableSeparator = styled.tr` + ${tw`sw-h-4`} + border-top: ${themeBorder('default')}; +`; + +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 +}: Readonly) { + 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; + } +`; + +const CellTypeContext = createContext<'th' | 'td'>('td'); +type CellComponentProps = ComponentProps<'th' | 'td'>; + +export function CellComponent(props: CellComponentProps) { + const containerType = useContext(CellTypeContext); + return ; +} + +export function ContentCell({ + children, + cellClassName, + className, + ...props +}: CellComponentProps & { cellClassName?: string }) { + return ( + +
+ {children} +
+
+ ); +} + +export function NumericalCell({ children, ...props }: CellComponentProps) { + return ( + +
{children}
+
+ ); +} + +export function RatingCell({ children, ...props }: CellComponentProps) { + return ( + +
{children}
+
+ ); +} + +export function ActionCell({ children, ...props }: CellComponentProps) { + return ( + +
{children}
+
+ ); +} + +export function CheckboxCell({ children, ...props }: CellComponentProps) { + return ( + +
{children}
+
+ ); +} + +const StyledTable = styled.table<{ gridTemplate?: string }>` + width: 100%; + border-collapse: collapse; + + &.with-grid-template { + display: grid; + grid-template-columns: ${(props) => props.gridTemplate}; + thead, + tbody, + tr { + display: contents; + } + } + + &.with-rounded-border { + border-collapse: separate; + border: ${themeBorder('default', 'breakdownBorder')}; + ${tw`sw-rounded-1`}; + + th:first-of-type { + ${tw`sw-rounded-tl-1`}; + } + th:last-of-type { + ${tw`sw-rounded-tr-1`}; + } + + tr:last-child > td { + border-bottom: none; + } + } +`; + +const CellComponentStyled = styled.td` + color: ${themeColor('pageContent')}; + ${tw`sw-items-center`} + ${tw`sw-body-sm`} + ${tw`sw-py-4 sw-px-2`} + ${tw`sw-align-middle`} + + thead > tr > & { + color: ${themeColor('pageTitle')}; + + ${tw`sw-body-sm-highlight`} + } +`; diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/__tests__/Table-test.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/__tests__/Table-test.tsx new file mode 100644 index 00000000000..affcb195bf3 --- /dev/null +++ b/server/sonar-web/design-system/src/sonar-aligned/components/__tests__/Table-test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { render } from '../../../helpers/testUtils'; +import { + ActionCell, + CheckboxCell, + ContentCell, + NumericalCell, + RatingCell, + Table, + TableProps, + TableRow, + TableRowInteractive, +} from '../Table'; + +it.each([ + [ + 'using column count and widths', + { + columnCount: 5, + columnWidths: ['1%', 'auto', '1%', '1%', '1%'], + 'aria-colcount': 5, + }, + ], + [ + 'using column count only', + { + columnCount: 5, + 'aria-colcount': 5, + }, + ], + [ + 'using column grid template only', + { + gridTemplate: '1fr auto 1fr 1fr 1fr', + 'aria-colcount': 5, + }, + ], +])('check that the html structure and style is correct %s', (_, props) => { + renderTable({ + ...props, + header: ( + + ContentCellHeader + NumericalCellHeader + CheckboxCellHeader + RatingCellHeader + ActionCellHeader + + ), + children: ( + <> + + ContentCell 1 + NumericalCell 1 + CheckboxCell 1 + RatingCell 1 + ActionCell 1 + + + ContentCell 2 + NumericalCell 2 + CheckboxCell 2 + RatingCell 2 + ActionCell 2 + + + ContentCell 3 + + + NumericalCell 4 + CheckboxCell 4 + RatingCell 4 + ActionCell 4 + + + ), + }); + + // Table should have accessible attribute + expect(screen.getByRole('table')).toHaveAttribute('aria-colcount', '5'); + + // Rows should have accessible attributes + expect( + screen.getByRole('row', { + name: 'ContentCellHeader NumericalCellHeader CheckboxCellHeader RatingCellHeader ActionCellHeader', + }), + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: 'ContentCell 1 NumericalCell 1 CheckboxCell 1 RatingCell 1 ActionCell 1', + }), + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: 'ContentCell 1 NumericalCell 1 CheckboxCell 1 RatingCell 1 ActionCell 1', + }), + ).not.toHaveAttribute('aria-selected'); + expect( + screen.getByRole('row', { + selected: true, + name: 'ContentCell 2 NumericalCell 2 CheckboxCell 2 RatingCell 2 ActionCell 2', + }), + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: 'NumericalCell 4 CheckboxCell 4 RatingCell 4 ActionCell 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', + ); + expect(screen.getByRole('cell', { name: 'RatingCell 4' })).toHaveAttribute('aria-colindex', '4'); + expect(screen.getByRole('cell', { name: 'ActionCell 4' })).toHaveAttribute('aria-colindex', '5'); +}); + +function renderTable(props: TableProps) { + return render({props.children}
); +} diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/index.ts b/server/sonar-web/design-system/src/sonar-aligned/components/index.ts index d9765390baa..459ba9d1655 100644 --- a/server/sonar-web/design-system/src/sonar-aligned/components/index.ts +++ b/server/sonar-web/design-system/src/sonar-aligned/components/index.ts @@ -20,5 +20,6 @@ export * from './Card'; export * from './MetricsRatingBadge'; +export * from './Table'; export * from './buttons'; export * from './typography';