diff --git a/src/components/DataTable/DataTable.module.css b/src/components/DataTable/DataTable.module.css index 3efbab7bb..2de3f098a 100644 --- a/src/components/DataTable/DataTable.module.css +++ b/src/components/DataTable/DataTable.module.css @@ -127,6 +127,28 @@ } } +.data-table__status-cell { + text-align: center; + + .data-table--size-md & { + padding: calc(var(--eds-size-2) / 16 * 1rem) + calc(var(--eds-size-3) / 16 * 1rem); + } + .data-table--size-sm & { + padding: calc(var(--eds-size-half) / 16 * 1rem) + calc(var(--eds-size-1) / 16 * 1rem); + } +} + +.data-table__status-header-cell { + /* display: none; */ + /* visibility: hidden; */ + height: 1px; + width: 1px; + overflow: hidden; + text-indent: 9999; +} + .data-table__cell { display: flex; gap: calc(var(--eds-size-1) / 16 * 1rem); @@ -288,3 +310,31 @@ background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis); } } + +.data-table .data-table__status-cell { + &.data-table--status-critical { + color: var(--eds-theme-color-icon-utility-critical) + } + + &.data-table--status-favorable { + color: var(--eds-theme-color-icon-utility-favorable); + } + + &.data-table--status-warning { + color: var(--eds-theme-color-icon-utility-warning); + } +} + +.data-table .data-table__row { + &.data-table--status-critical { + background-color: var(--eds-theme-color-background-utility-critical-low-emphasis); + } + + &.data-table--status-favorable { + background-color: var(--eds-theme-color-background-utility-favorable-low-emphasis); + } + + &.data-table--status-warning { + background-color: var(--eds-theme-color-background-utility-warning-low-emphasis); + } +} \ No newline at end of file diff --git a/src/components/DataTable/DataTable.stories.tsx b/src/components/DataTable/DataTable.stories.tsx index 314a2cbe5..895cbc4c5 100644 --- a/src/components/DataTable/DataTable.stories.tsx +++ b/src/components/DataTable/DataTable.stories.tsx @@ -16,6 +16,7 @@ import { // We import all of the utilities from tanstack here, and this can contain other custom utilities import { Button, Menu, Checkbox, DataTableUtils } from '../..'; +import type { Status } from '../../util/variant-types'; import { chromaticViewports } from '../../util/viewports'; export default { @@ -59,6 +60,8 @@ type Person = { age: number; visits: number; progress: number; + // This column is used for tables that are eligible for status + status?: Extract; }; // Specifying the example (static) data for the table to use with tanstack primitives @@ -76,6 +79,7 @@ const defaultData: Person[] = [ age: 40, visits: 40, progress: 80, + status: 'warning', }, { firstName: 'Tanner', @@ -90,6 +94,7 @@ const defaultData: Person[] = [ age: 45, visits: 20, progress: 10, + status: 'critical', }, { firstName: 'Tandy', @@ -111,6 +116,7 @@ const defaultData: Person[] = [ age: 45, visits: 20, progress: 10, + status: 'favorable', }, { firstName: 'Tandy', @@ -700,6 +706,96 @@ export const Grouping: StoryObj = { }, }; +/** + * You can specify detailed statuses for each row in a table, matching a few common options. + * Extend the data type to include `status` which maps to the internal type + * + * Use the Utility type StatusDataTable (TODO-AH) + * + * TODO: + * - should indent table caption and subcaption to align to status column? + */ +export const StatusRows: StoryObj = { + args: { + caption: 'Test table', + subcaption: 'Additional Subcaption', + isStatusEligible: true, + tableStyle: 'border', + rowStyle: 'lined', + }, + render: (args) => { + const columns = [ + // TODO-AH: export something to make this more convenient + columnHelper.accessor(DataTable.__StatusColumnId__, { + header: () => , + cell: (info) => , + // TODO-AH: figure out cell size to make 28x32 + size: 32, + }), + columnHelper.accessor('firstName', { + header: () => ( + + First Name + + ), + cell: (info) => ( + {info.getValue()} + ), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => Last Name, + cell: (info) => ( + {info.getValue()} + ), + }), + columnHelper.accessor('age', { + header: () => ( + Age + ), + cell: (info) => ( + + {info.renderValue()} + + ), + }), + columnHelper.accessor('visits', { + header: () => ( + + Visits + + ), + cell: (info) => ( + + {info.renderValue()} + + ), + }), + columnHelper.accessor('progress', { + header: () => ( + + Profile Progress + + ), + cell: (info) => ( + + {info.renderValue()} + + ), + }), + ]; + + // eslint-disable-next-line react-hooks/rules-of-hooks + const table = DataTableUtils.useReactTable({ + data: defaultData, + columns, + getCoreRowModel: DataTableUtils.getCoreRowModel(), + }); + + return ; + }, +}; + // TODO: Story for sticky column pinning (https://tanstack.com/table/latest/docs/framework/react/examples/column-pinning-sticky) export const DefaultWithCustomTable: StoryObj = { diff --git a/src/components/DataTable/DataTable.tsx b/src/components/DataTable/DataTable.tsx index 77a8e5ad5..a93bb799e 100644 --- a/src/components/DataTable/DataTable.tsx +++ b/src/components/DataTable/DataTable.tsx @@ -2,6 +2,7 @@ import { flexRender, type Table } from '@tanstack/react-table'; import clsx from 'clsx'; import React, { useEffect } from 'react'; +import getIconNameFromStatus from '../../util/getIconNameFromStatus'; import type { EDSBase, Size, Status, Align } from '../../util/variant-types'; import Button, { type ButtonProps } from '../Button'; @@ -46,7 +47,7 @@ export type DataTableProps = EDSBase & { */ caption?: string; /** - * Controls whether the rows allow for a status color/icon treatment. + * Controls whether the table allows rows for a status color/icon treatment. */ isStatusEligible?: boolean; /** @@ -77,11 +78,11 @@ export type DataTableProps = EDSBase & { export type DataTableTableProps = EDSBase & Pick; -// TODO: Implement as followup export type DataTableRowProps = Pick & { + 'aria-label'?: string; isInteractive?: boolean; isSelected?: boolean; - status?: Extract; + status?: Extract; }; export type DataTableHeaderCellProps = EDSBase & { @@ -129,6 +130,11 @@ export type DataTableDataCellProps = DataTableHeaderCellProps & { children: React.ReactNode; }; +export type DataTableStatusCellProps = { + 'aria-label'?: string; + status?: Extract; +}; + /** * `import {DataTable} from "@chanzuckerberg/eds";` * @@ -142,6 +148,7 @@ export function DataTable({ className, caption, isInteractive = false, + isStatusEligible, onSearchChange, rowStyle = 'striped', size = 'md', @@ -149,7 +156,7 @@ export function DataTable({ table, tableClassName, tableStyle = 'basic', - ...other + ...rest }: DataTableProps) { const componentClassName = clsx(styles['data-table'], className); @@ -159,7 +166,7 @@ export function DataTable({ * header, search field, and actions, and preserve accessibility. */ return ( -
+
{(caption || subcaption || onSearchChange || actions) && (
{(caption || subcaption) && ( @@ -211,23 +218,28 @@ export function DataTable({ {table?.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} + {headerGroup.headers.map((header) => { + // Special Case: avoid a11y error for status column by using instead of + const CellComponent = + header.id === DataTable.__StatusColumnId__ ? 'th' : 'th'; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} ))} @@ -261,6 +273,9 @@ export function DataTable({ {row.getVisibleCells().map((cell) => ( { + return ( +
+ Status Column +
+ ); +}; + +export const DataTableStatusCell = ({ + 'aria-label': ariaLabel, + status, + ...rest +}: DataTableStatusCellProps) => { + const statusCellClassName = clsx( + styles['data-table__status-cell'], + status && styles[`data-table--status-${status}`], + ); + + return ( +
+ {status && ( + + )} +
+ ); +}; + export const DataTableTable = ({ children, tableClassName, @@ -471,10 +521,12 @@ export const DataTableHeader = ({ }; export const DataTableRow = ({ + 'aria-label': ariaLabel, children, className, isInteractive, isSelected, + status, ...rest }: DataTableRowProps) => { const componnentClassName = clsx( @@ -482,9 +534,20 @@ export const DataTableRow = ({ styles['data-table__row'], isInteractive && styles['data-table__row--is-interactive'], isSelected && styles['data-table__row--is-selected'], + status && styles[`data-table--status-${status}`], ); + + const rowA11yDesc = + ariaLabel || + (status && + { + favorable: 'This table row has a favorable status', + critical: 'This table row has a critical status', + warning: 'This table row has a warning status', + }[status]); + return ( - + {children} ); @@ -535,3 +598,8 @@ DataTable.Row = DataTableRow; DataTable.GroupRow = DataTableGroupRow; DataTable.HeaderCell = DataTableHeaderCell; DataTable.DataCell = DataTableDataCell; + +// Special Cell Sub-types and data +DataTable.StatusCell = DataTableStatusCell; +DataTable.StatusHeaderCell = DataTableStatusHeaderCell; +DataTable.__StatusColumnId__ = 'status' as const; diff --git a/src/components/DataTable/__snapshots__/DataTable.test.ts.snap b/src/components/DataTable/__snapshots__/DataTable.test.ts.snap index 0501270b9..5696c560d 100644 --- a/src/components/DataTable/__snapshots__/DataTable.test.ts.snap +++ b/src/components/DataTable/__snapshots__/DataTable.test.ts.snap @@ -4012,6 +4012,1098 @@ exports[` Selectable story renders snapshot 1`] = `
`; +exports[` StatusRows story renders snapshot 1`] = ` +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Test table: Additional Subcaption +
+
+ Status Column +
+
+
+
+ First Name +
+
+
+
+
+ Last Name +
+
+
+
+
+ Age +
+
+
+
+
+ Visits +
+
+
+
+
+ Profile Progress +
+
+
+
+
+
+
+ Joe +
+
+
+
+
+ Dirte +
+
+
+
+
+ 45 +
+
+
+
+
+ 20 +
+
+
+
+
+ 10 +
+
+
+
+ +
+
+
+
+ Tandy +
+
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 +
+
+
+
+
+
+
+ Tanner +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 +
+
+
+
+ +
+
+
+
+ Joe +
+
+
+
+
+ Dirte +
+
+
+
+
+ 45 +
+
+
+
+
+ 20 +
+
+
+
+
+ 10 +
+
+
+
+
+
+
+ Tandy +
+
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 +
+
+
+
+
+
+
+ Tanner +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 +
+
+
+
+ +
+
+
+
+ Joe +
+
+
+
+
+ Dirte +
+
+
+
+
+ 45 +
+
+
+
+
+ 20 +
+
+
+
+
+ 10 +
+
+
+
+
+
+
+ Tandy +
+
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 +
+
+
+
+
+
+
+ Tanner +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 +
+
+
+
+
+
+
+ Joe +
+
+
+
+
+ Dirte +
+
+
+
+
+ 45 +
+
+
+
+
+ 20 +
+
+
+
+
+ 10 +
+
+
+
+
+
+
+ Tandy +
+
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 +
+
+
+
+
+
+
+ Tanner +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 +
+
+
+
+`; + exports[` TableSizeSm story renders snapshot 1`] = `