]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19485 Fix accessibility issue with Table component
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Wed, 28 Jun 2023 09:39:02 +0000 (11:39 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 29 Jun 2023 20:05:14 +0000 (20:05 +0000)
server/sonar-web/design-system/src/components/Table.tsx
server/sonar-web/src/main/js/apps/code/components/Component.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
server/sonar-web/src/main/js/apps/code/components/Components.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx

index 8b8e75a240c7a85a029a537d7627bc2759e1f394..b97b2237fc60f6202685bfcb877406807108d690 100644 (file)
  */
 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';
 
-interface TableBaseProps extends ComponentProps<'table'> {
+export interface TableProps extends ComponentProps<'table'> {
+  columnCount: number;
+  columnWidths?: Array<number | string>;
   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;
+  const {
+    className,
+    columnCount,
+    columnWidths = [],
+    header,
+    children,
+    noHeaderTopBorder,
+    noSidePadding,
+    ...rest
+  } = props;
 
   return (
     <StyledTable
@@ -53,6 +53,11 @@ export function Table(props: TableProps) {
       )}
       {...rest}
     >
+      <colgroup>
+        {times(columnCount, (i) => (
+          <col key={i} width={columnWidths[i] ?? 'auto'} />
+        ))}
+      </colgroup>
       {header && (
         <thead>
           <CellTypeContext.Provider value="th">{header}</CellTypeContext.Provider>
@@ -151,40 +156,56 @@ export const TableRowInteractive = styled(TableRowInteractiveBase)`
   }
 `;
 
-export const ContentCell = styled(CellComponent)`
-  ${tw`sw-text-left sw-justify-start`}
-`;
-export const NumericalCell = styled(CellComponent)`
-  ${tw`sw-text-right sw-justify-end`}
-`;
-export const RatingCell = styled(CellComponent)`
-  ${tw`sw-text-right sw-justify-end`}
-`;
-export const CheckboxCell = styled(CellComponent)`
-  ${tw`sw-text-center`}
-  ${tw`sw-flex`}
-  ${tw`sw-items-center sw-justify-center`}
-`;
+const CellTypeContext = createContext<'th' | 'td'>('td');
+type CellComponentProps = ComponentProps<'th' | 'td'>;
+
+export function CellComponent(props: CellComponentProps) {
+  const containerType = useContext(CellTypeContext);
+  return <CellComponentStyled as={containerType} {...props} />;
+}
+
+export function ContentCell({ children, ...props }: CellComponentProps) {
+  return (
+    <CellComponent {...props}>
+      <div className="sw-text-left sw-justify-start sw-flex sw-items-center">{children}</div>
+    </CellComponent>
+  );
+}
 
-const StyledTable = styled.table<GenericTableProps | CustomTableProps>`
-  display: grid;
-  grid-template-columns: ${(props) => props.gridTemplate ?? `repeat(${props.columnCount}, 1fr)`};
+export function NumericalCell({ children, ...props }: CellComponentProps) {
+  return (
+    <CellComponent {...props}>
+      <div className="sw-text-right sw-justify-end sw-flex sw-items-center">{children}</div>
+    </CellComponent>
+  );
+}
+
+export function RatingCell({ children, ...props }: CellComponentProps) {
+  return (
+    <CellComponent {...props}>
+      <div className="sw-text-right sw-justify-end sw-flex sw-items-center">{children}</div>
+    </CellComponent>
+  );
+}
+
+export function CheckboxCell({ children, ...props }: CellComponentProps) {
+  return (
+    <CellComponent {...props}>
+      <div className="sw-text-center sw-justify-center sw-flex sw-items-center">{children}</div>
+    </CellComponent>
+  );
+}
+
+const StyledTable = styled.table`
   width: 100%;
   border-collapse: collapse;
-
-  thead,
-  tbody,
-  tr {
-    display: contents;
-  }
 `;
 
 const CellComponentStyled = styled.td`
   color: ${themeColor('pageContent')};
-  ${tw`sw-flex sw-items-center`}
   ${tw`sw-body-sm`}
   ${tw`sw-py-4 sw-px-2`}
-  ${tw`sw-align-top`}
+  ${tw`sw-align-middle`}
 
   thead > tr > & {
     color: ${themeColor('pageTitle')};
@@ -192,10 +213,3 @@ const CellComponentStyled = styled.td`
     ${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} />;
-}
index f18c72bc6fc66fc8de5076a1f6b5343da02d0bb5..580a2a58ae4170a2e56d81d8494e03446782d31a 100644 (file)
@@ -95,7 +95,7 @@ export default function Component(props: Props) {
       ))}
 
       {showAnalysisDate && (
-        <NumericalCell>
+        <NumericalCell className="sw-whitespace-nowrap">
           {!isBaseComponent &&
             (component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—')}
         </NumericalCell>
index e6ec611dc88ce80849076ba07dbdbb34e5f527ef..472f0b4ab80ef83d4bc801b13df39b3c2fa570dd 100644 (file)
@@ -59,7 +59,7 @@ export default function ComponentMeasure(props: Props) {
       const ariaLabel = translateWithParameters('overview.quality_gate_x', formatted);
 
       return (
-        <ContentCell>
+        <ContentCell className="sw-whitespace-nowrap">
           <QualityGateIndicator
             status={(value as Status) ?? 'NONE'}
             className="sw-mr-2"
@@ -72,7 +72,7 @@ export default function ComponentMeasure(props: Props) {
     }
     case MetricType.Rating:
       return (
-        <RatingCell>
+        <RatingCell className="sw-whitespace-nowrap">
           <MetricsRatingBadge
             label={value ?? '—'}
             rating={formatMeasure(value, MetricType.Rating) as MetricsEnum}
@@ -81,7 +81,7 @@ export default function ComponentMeasure(props: Props) {
       );
     default:
       return (
-        <NumericalCell>
+        <NumericalCell className="sw-whitespace-nowrap">
           <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={value} />
         </NumericalCell>
       );
index 588d0c8c198b690aa02f4cd22b1f4e8b4e5e2448..222e0c1df58c2ab8a0d20535f6b178e005c3507c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ContentCell, Table, TableRow } from 'design-system';
-import { sortBy } from 'lodash';
+import { sortBy, times } from 'lodash';
 import * as React from 'react';
 import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation';
 import { getComponentMeasureUniqueKey } from '../../../helpers/component';
+import { isDefined } from '../../../helpers/types';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
 import { ComponentMeasure, Metric } from '../../../types/types';
@@ -60,12 +61,16 @@ function Components(props: ComponentsProps) {
       ComponentQualifier.SubPortfolio,
     ].includes(baseComponent.qualifier as ComponentQualifier);
 
+  const columnCount = metrics.length + Number(canBePinned) + Number(showAnalysisDate) + 1;
   return (
     <div className="big-spacer-bottom table-wrapper">
       <Table
-        gridTemplate={`${canBePinned ? 'min-content' : ''} auto repeat(${
-          metrics.length + (showAnalysisDate ? 1 : 0)
-        }, max-content)`}
+        columnCount={columnCount}
+        columnWidths={[
+          canBePinned ? '1%' : undefined,
+          'auto',
+          ...times(columnCount - 1, () => '1%'),
+        ].filter(isDefined)}
         header={
           baseComponent && (
             <TableRow>
@@ -94,7 +99,7 @@ function Components(props: ComponentsProps) {
               showAnalysisDate={showAnalysisDate}
             />
             <TableRow>
-              <ContentCell className="sw-col-span-full" />
+              <ContentCell colSpan={columnCount} />
             </TableRow>
           </>
         )}
index d37f4a9c544cec19e1b13c0bc2789315ca5cfa0d..e436e321fa18846a76b50d1d5190bb8e2b16c97f 100644 (file)
@@ -75,7 +75,12 @@ export default function ComponentsHeader(props: ComponentsHeaderProps) {
     <>
       {canBePinned && <ContentCell aria-label={translate('code.pin')} />}
       <ContentCell aria-label={translate('code.name')} />
-      {baseComponent && columns.map((column) => <Cell key={column}>{column}</Cell>)}
+      {baseComponent &&
+        columns.map((column) => (
+          <Cell className="sw-whitespace-nowrap" key={column}>
+            {column}
+          </Cell>
+        ))}
     </>
   );
 }
index 9de89b8982ffb270242e20d62ca704f63b3f97e1..a55d5f14effc29db1a12cfae0b9e69e88cf89c7d 100644 (file)
@@ -84,7 +84,7 @@ export default function ComponentCell(props: ComponentCellProps) {
   }
 
   return (
-    <ContentCell className="sw-py-3 sw-truncate sw-flex">
+    <ContentCell className="sw-py-3 sw-truncate">
       <HoverLink
         aria-hidden
         tabIndex={-1}
index 797671d0793ec6467bef94349c9217a49f4d12ac..26a5e7a18e3247bef012e66c3311a5a82d9e28a4 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ContentCell, NumericalCell, Table, TableRow, TableRowInteractive } from 'design-system';
+import { times } from 'lodash';
 import * as React from 'react';
 import { getLocalizedMetricName } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
@@ -48,7 +49,8 @@ export default function ComponentsList({ components, metric, metrics, ...props }
   const otherMetrics = (complementary[metric.key] || []).map((key) => metrics[key]);
   return (
     <Table
-      gridTemplate={`1fr repeat(${otherMetrics.length + 1}, min-content)`}
+      columnCount={otherMetrics.length + 2}
+      columnWidths={['auto', ...times(otherMetrics.length + 1, () => '1%')]}
       header={
         otherMetrics.length > 0 && (
           <TableRow>