]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19388 Create new styled table in component library
author7PH <benjamin.raymond@sonarsource.com>
Thu, 25 May 2023 08:51:21 +0000 (10:51 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 25 May 2023 20:02:51 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/Table.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/Table-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/index.ts

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 (file)
index 0000000..ac3a806
--- /dev/null
@@ -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 (
+    <StyledTable
+      className={classNames(
+        { 'no-header-top-border': noHeaderTopBorder, 'no-side-padding': noSidePadding },
+        className
+      )}
+      {...rest}
+    >
+      {header && (
+        <thead>
+          <CellTypeContext.Provider value="th">{header}</CellTypeContext.Provider>
+        </thead>
+      )}
+      <tbody>{children}</tbody>
+    </StyledTable>
+  );
+}
+
+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<typeof TableRow> {
+  selected?: boolean;
+}
+
+function TableRowInteractiveBase({
+  className,
+  children,
+  selected,
+  ...props
+}: TableRowInteractiveProps) {
+  return (
+    <TableRow aria-selected={selected} className={classNames(className, { selected })} {...props}>
+      {children}
+    </TableRow>
+  );
+}
+
+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<GenericTableProps | CustomTableProps>`
+  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 <CellComponentStyled as={containerType} {...props} />;
+}
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 (file)
index 0000000..1f16c3a
--- /dev/null
@@ -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: (
+      <TableRow>
+        <ContentCell>ContentCellHeader</ContentCell>
+        <NumericalCell>NumericalCellHeader</NumericalCell>
+        <CheckboxCell>CheckboxCellHeader</CheckboxCell>
+      </TableRow>
+    ),
+    children: (
+      <>
+        <TableRowInteractive>
+          <ContentCell>ContentCell 1</ContentCell>
+          <NumericalCell>NumericalCell 1</NumericalCell>
+          <CheckboxCell>CheckboxCell 1</CheckboxCell>
+        </TableRowInteractive>
+        <TableRowInteractive selected={true}>
+          <ContentCell>ContentCell 2</ContentCell>
+          <NumericalCell>NumericalCell 2</NumericalCell>
+          <CheckboxCell>CheckboxCell 2</CheckboxCell>
+        </TableRowInteractive>
+        <TableRow>
+          <ContentCell aria-colspan={3}>ContentCell 3</ContentCell>
+        </TableRow>
+        <TableRowInteractive>
+          <NumericalCell aria-colindex={2}>NumericalCell 4</NumericalCell>
+          <CheckboxCell aria-colindex={3}>CheckboxCell 4</CheckboxCell>
+        </TableRowInteractive>
+      </>
+    ),
+  });
+
+  // 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(<Table {...props}>{props.children}</Table>);
+}
index 0cf94260576f5ced66598a8507fbf24d34b24cc5..5b5ed58705a43575a985b10b28fdfe67a8dad269 100644 (file)
@@ -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';