diff --git a/.storybook/preview.js b/.storybook/preview.js index d2b420fb9c3..05242052cd1 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -29,6 +29,9 @@ export const parameters = { root: '#html-addon-root', removeEmptyComments: true, }, + controls: { + hideNoControlsWarning: true, + }, options: { storySort: (a, b) => { const defaultOrder = [ diff --git a/docs/content/drafts/DataTable.mdx b/docs/content/drafts/DataTable.mdx new file mode 100644 index 00000000000..348b8252d59 --- /dev/null +++ b/docs/content/drafts/DataTable.mdx @@ -0,0 +1,92 @@ +--- +title: DataTable +componentId: data_table +status: Draft +--- + +import data from '../../../src/DataTable/DataTable.docs.json' + +## Examples + +```jsx + { + return + }, + }, + { + header: 'Dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + ]} +/> +``` + +## Props + + + +## Status + + diff --git a/generated/components.json b/generated/components.json index add33745532..4f74d6c345d 100644 --- a/generated/components.json +++ b/generated/components.json @@ -2586,6 +2586,153 @@ ], "subcomponents": [] }, + "data_table": { + "id": "data_table", + "name": "DataTable", + "status": "draft", + "a11yReviewed": false, + "stories": [], + "props": [ + { + "name": "aria-describedby", + "type": "string", + "description": "Provide an id to an element which uniquely describes this table" + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "Provide an id to an element which uniquely labels this table" + }, + { + "name": "data", + "type": "Array", + "description": "Provide a collection of the rows which will be rendered inside of the table" + }, + { + "name": "columns", + "type": "Array>", + "description": "Provide the columns for the table and the fields in `data` to which they correspond" + }, + { + "name": "cellPadding", + "type": "'condensed' | 'normal' | 'spacious'", + "description": "Specify the amount of space that should be available around the contents of a cell" + } + ], + "subcomponents": [ + { + "name": "Table", + "props": [ + { + "name": "aria-describedby", + "type": "string", + "description": "Provide an id to an element which uniquely describes this table" + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "Provide an id to an element which uniquely labels this table" + }, + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "cellPadding", + "type": "'condensed' | 'normal' | 'spacious'", + "description": "Specify the amount of space that should be available around the contents of a cell" + } + ] + }, + { + "name": "TableHead", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableBody", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableRow", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableHeader", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableCell", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "scope", + "type": "string", + "description": "Provide the scope for a table cell, useful for defining a row header using `scope=\"row\"`" + } + ] + }, + { + "name": "TableContainer", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableTitle", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "id", + "type": "string", + "required": true + } + ] + }, + { + "name": "TableSubtitle", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "id", + "type": "string", + "required": true + } + ] + } + ] + }, "drafts_dialog": { "id": "drafts_dialog", "name": "Dialog", diff --git a/src/DataTable/DataTable.docs.json b/src/DataTable/DataTable.docs.json new file mode 100644 index 00000000000..8a140da5c42 --- /dev/null +++ b/src/DataTable/DataTable.docs.json @@ -0,0 +1,147 @@ +{ + "id": "data_table", + "name": "DataTable", + "status": "draft", + "a11yReviewed": false, + "stories": [], + "props": [ + { + "name": "aria-describedby", + "type": "string", + "description": "Provide an id to an element which uniquely describes this table" + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "Provide an id to an element which uniquely labels this table" + }, + { + "name": "data", + "type": "Array", + "description": "Provide a collection of the rows which will be rendered inside of the table" + }, + { + "name": "columns", + "type": "Array>", + "description": "Provide the columns for the table and the fields in `data` to which they correspond" + }, + { + "name": "cellPadding", + "type": "'condensed' | 'normal' | 'spacious'", + "description": "Specify the amount of space that should be available around the contents of a cell" + } + ], + "subcomponents": [ + { + "name": "Table", + "props": [ + { + "name": "aria-describedby", + "type": "string", + "description": "Provide an id to an element which uniquely describes this table" + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "Provide an id to an element which uniquely labels this table" + }, + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "cellPadding", + "type": "'condensed' | 'normal' | 'spacious'", + "description": "Specify the amount of space that should be available around the contents of a cell" + } + ] + }, + { + "name": "TableHead", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableBody", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableRow", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableHeader", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableCell", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "scope", + "type": "string", + "description": "Provide the scope for a table cell, useful for defining a row header using `scope=\"row\"`" + } + ] + }, + { + "name": "TableContainer", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + } + ] + }, + { + "name": "TableTitle", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "id", + "type": "string", + "required": true + } + ] + }, + { + "name": "TableSubtitle", + "props": [ + { + "name": "children", + "type": "React.ReactNode" + }, + { + "name": "id", + "type": "string", + "required": true + } + ] + } + ] +} diff --git a/src/DataTable/DataTable.features.stories.tsx b/src/DataTable/DataTable.features.stories.tsx new file mode 100644 index 00000000000..2bc06c63334 --- /dev/null +++ b/src/DataTable/DataTable.features.stories.tsx @@ -0,0 +1,319 @@ +import {Meta} from '@storybook/react' +import React from 'react' +import { + DataTable, + Table, + TableHead, + TableBody, + TableRow, + TableHeader, + TableCell, + TableContainer, + TableTitle, + TableSubtitle, +} from '../DataTable' +import Label from '../Label' +import LabelGroup from '../LabelGroup' +import RelativeTime from '../RelativeTime' + +export default { + title: 'Drafts/Components/DataTable/Features', + component: DataTable, + subcomponents: { + Table, + TableHead, + TableBody, + TableRow, + TableHeader, + TableCell, + TableContainer, + TableTitle, + TableSubtitle, + }, +} as Meta + +const now = Date.now() +const Second = 1000 +const Minute = 60 * Second +const Hour = 60 * Minute +const Day = 24 * Hour +const Week = 7 * Day +const Month = 4 * Week + +interface Repo { + id: number + name: string + type: 'public' | 'internal' + updatedAt: number + securityFeatures: { + dependabot: Array + codeScanning: Array + } +} + +const data: Array = [ + { + id: 1, + name: 'codeql-dca-worker', + type: 'internal', + updatedAt: now, + securityFeatures: { + dependabot: ['alerts', 'security updates'], + codeScanning: ['report secrets'], + }, + }, + { + id: 2, + name: 'aegir', + type: 'public', + updatedAt: now - 5 * Minute, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: ['report secrets'], + }, + }, + { + id: 3, + name: 'strapi', + type: 'public', + updatedAt: now - 1 * Hour, + securityFeatures: { + dependabot: [], + codeScanning: [], + }, + }, + { + id: 4, + name: 'codeql-ci-nightlies', + type: 'public', + updatedAt: now - 6 * Hour, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: [], + }, + }, + { + id: 5, + name: 'dependabot-updates', + type: 'public', + updatedAt: now - 1 * Day, + securityFeatures: { + dependabot: [], + codeScanning: [], + }, + }, + { + id: 6, + name: 'tsx-create-react-app', + type: 'public', + updatedAt: now - 1 * Week, + securityFeatures: { + dependabot: [], + codeScanning: [], + }, + }, + { + id: 7, + name: 'bootstrap', + type: 'public', + updatedAt: now - 1 * Month, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: [], + }, + }, + { + id: 8, + name: 'docker-templates', + type: 'public', + updatedAt: now - 3 * Month, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: [], + }, + }, +] + +function uppercase(input: string): string { + return input[0].toUpperCase() + input.slice(1) +} + +export const Default = () => ( + + + Repositories + + + A subtitle could appear here to give extra context to the data. + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + ]} + /> + +) + +export const WithTitle = () => ( + + + Repositories + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + ]} + /> + +) + +export const WithTitleAndSubtitle = () => ( + + + Repositories + + + A subtitle could appear here to give extra context to the data. + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + ]} + /> + +) diff --git a/src/DataTable/DataTable.stories.tsx b/src/DataTable/DataTable.stories.tsx new file mode 100644 index 00000000000..d5f757b7e4f --- /dev/null +++ b/src/DataTable/DataTable.stories.tsx @@ -0,0 +1,221 @@ +import {Meta, ComponentStory} from '@storybook/react' +import React from 'react' +import {DataTable} from '../DataTable' +import Label from '../Label' +import LabelGroup from '../LabelGroup' +import RelativeTime from '../RelativeTime' + +export default { + title: 'Drafts/Components/DataTable', + component: DataTable, +} as Meta + +const now = Date.now() +const Second = 1000 +const Minute = 60 * Second +const Hour = 60 * Minute +const Day = 24 * Hour +const Week = 7 * Day +const Month = 4 * Week + +interface Repo { + id: number + name: string + type: 'public' | 'internal' + updatedAt: number + securityFeatures: { + dependabot: Array + codeScanning: Array + } +} + +const data: Array = [ + { + id: 1, + name: 'codeql-dca-worker', + type: 'internal', + updatedAt: now, + securityFeatures: { + dependabot: ['alerts', 'security updates'], + codeScanning: ['report secrets'], + }, + }, + { + id: 2, + name: 'aegir', + type: 'public', + updatedAt: now - 5 * Minute, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: ['report secrets'], + }, + }, + { + id: 3, + name: 'strapi', + type: 'public', + updatedAt: now - 1 * Hour, + securityFeatures: { + dependabot: [], + codeScanning: [], + }, + }, + { + id: 4, + name: 'codeql-ci-nightlies', + type: 'public', + updatedAt: now - 6 * Hour, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: [], + }, + }, + { + id: 5, + name: 'dependabot-updates', + type: 'public', + updatedAt: now - 1 * Day, + securityFeatures: { + dependabot: [], + codeScanning: [], + }, + }, + { + id: 6, + name: 'tsx-create-react-app', + type: 'public', + updatedAt: now - 1 * Week, + securityFeatures: { + dependabot: [], + codeScanning: [], + }, + }, + { + id: 7, + name: 'bootstrap', + type: 'public', + updatedAt: now - 1 * Month, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: [], + }, + }, + { + id: 8, + name: 'docker-templates', + type: 'public', + updatedAt: now - 3 * Month, + securityFeatures: { + dependabot: ['alerts'], + codeScanning: [], + }, + }, +] + +function uppercase(input: string): string { + return input[0].toUpperCase() + input.slice(1) +} + +export const Playground: ComponentStory = args => { + return ( + + + Repositories + + + A subtitle could appear here to give extra context to the data. + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + ]} + /> + + ) +} + +Playground.args = { + cellPadding: 'normal', +} + +Playground.argTypes = { + 'aria-describedby': { + control: false, + table: { + disable: true, + }, + }, + 'aria-labelledby': { + control: false, + table: { + disable: true, + }, + }, + columns: { + control: false, + table: { + disable: true, + }, + }, + data: { + control: false, + table: { + disable: true, + }, + }, + cellPadding: { + control: { + type: 'radio', + }, + type: { + name: 'enum', + value: ['condensed', 'normal', 'spacious'], + }, + }, +} diff --git a/src/DataTable/__tests__/DataTable.test.tsx b/src/DataTable/__tests__/DataTable.test.tsx new file mode 100644 index 00000000000..a2631125e45 --- /dev/null +++ b/src/DataTable/__tests__/DataTable.test.tsx @@ -0,0 +1,265 @@ +import React from 'react' +import {DataTable, TableContainer, TableTitle, TableSubtitle} from '..' +import {render, screen} from '@testing-library/react' + +describe('DataTable', () => { + it('should render a semantic through `data` and `columns`', () => { + const columns = [ + { + header: 'Name', + field: 'name', + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + render() + + //
+ expect(screen.getByRole('table')).toBeInTheDocument() + + // + expect(screen.getAllByRole('row').length).toBe(4) + // {children} +} + +// ---------------------------------------------------------------------------- +// TableBody +// ---------------------------------------------------------------------------- + +interface TableBodyProps extends React.ComponentPropsWithoutRef<'tbody'> { + children?: React.ReactNode +} + +function TableBody({children}: TableBodyProps) { + return {children} +} + +// ---------------------------------------------------------------------------- +// TableHeader +// ---------------------------------------------------------------------------- + +interface TableHeaderProps extends React.ComponentPropsWithoutRef<'th'> { + children?: React.ReactNode +} + +function TableHeader({children}: TableHeaderProps) { + return +} + +// ---------------------------------------------------------------------------- +// TableRow +// ---------------------------------------------------------------------------- + +interface TableRowProps extends React.ComponentPropsWithoutRef<'tr'> { + children?: React.ReactNode +} + +function TableRow({children}: TableRowProps) { + return {children} +} + +// ---------------------------------------------------------------------------- +// TableCell +// ---------------------------------------------------------------------------- + +interface TableCellProps extends React.ComponentPropsWithoutRef<'td'> { + children?: React.ReactNode + + /** + * Provide the scope for a table cell, useful for defining a row header using + * `scope="row"` + */ + scope?: string | undefined +} + +function TableCell({children, scope}: TableCellProps) { + const BaseComponent = scope ? 'th' : 'td' + + return ( + + {children} + + ) +} + +// ---------------------------------------------------------------------------- +// TableContainer +// ---------------------------------------------------------------------------- +interface TableContainerProps { + children?: React.ReactNode | undefined +} + +function TableContainer({children}: TableContainerProps) { + return {children} +} + +interface TableTitleProps { + /** + * Provide an alternate element or component to use as the container for + * `TableSubtitle`. This is useful when specifying markup that is more + * semantic for your use-case, such as a heading tag. + */ + as?: keyof JSX.IntrinsicElements | React.ComponentType + + children?: React.ReactNode | undefined + + /** + * Provide a unique id for the table subtitle. This should be used along with + * `aria-labelledby` on `DataTable` + */ + id: string +} + +function TableTitle({as, children, id}: TableTitleProps) { + return ( + + {children} + + ) +} + +interface TableSubtitleProps { + /** + * Provide an alternate element or component to use as the container for + * `TableSubtitle`. This is useful when specifying markup that is more + * semantic for your use-case + */ + as?: keyof JSX.IntrinsicElements | React.ComponentType + + children?: React.ReactNode | undefined + + /** + * Provide a unique id for the table subtitle. This should be used along with + * `aria-describedby` on `DataTable` + */ + id: string +} + +function TableSubtitle({as, children, id}: TableSubtitleProps) { + return ( + + {children} + + ) +} + +export { + DataTable, + Table, + TableHead, + TableBody, + TableRow, + TableHeader, + TableCell, + TableContainer, + TableTitle, + TableSubtitle, +}
+ expect(screen.getAllByRole('columnheader')).toHaveLength(1) + expect(screen.getByRole('columnheader', {name: 'Name'})).toBeInTheDocument() + + //
+ expect(screen.getAllByRole('cell').length).toBe(3) + }) + + it('should support custom cell rendering with `renderCell`', () => { + const data = [ + { + id: 1, + name: { + value: 'one', + }, + }, + { + id: 2, + name: { + value: 'two', + }, + }, + { + id: 3, + name: { + value: 'three', + }, + }, + ] + render( + { + return row.name.value + }, + }, + ]} + />, + ) + + for (const row of data) { + expect(screen.getByRole('cell', {name: row.name.value})).toBeInTheDocument() + } + }) + + it('should support custom labeling through `aria-labelledby`', () => { + const columns = [ + { + header: 'Name', + field: 'name', + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + render( + <> +

custom-title

+ + , + ) + expect(screen.getByRole('table', {name: 'custom-title'})).toBeInTheDocument() + }) + + it('should support custom labeling through `aria-labelledby` and `TableTitle`', () => { + const columns = [ + { + header: 'Name', + field: 'name', + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + render( + + + custom-title + + + , + ) + expect(screen.getByRole('table', {name: 'custom-title'})).toBeInTheDocument() + }) + + it('should support custom descriptions through `aria-describedby`', () => { + const columns = [ + { + header: 'Name', + field: 'name', + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + render( + <> +

custom-description

+ + , + ) + expect(screen.getByRole('table', {description: 'custom-description'})).toBeInTheDocument() + }) + + it('should support custom descriptions through `aria-describedby` and `TableSubtitle`', () => { + const columns = [ + { + header: 'Name', + field: 'name', + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + render( + + + custom-description + + + , + ) + expect(screen.getByRole('table', {description: 'custom-description'})).toBeInTheDocument() + }) + + it('should support customizing the `cellPadding` of cells', () => { + const columns = [ + { + header: 'Name', + field: 'name', + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + const {rerender} = render() + + expect(screen.getByRole('table')).toHaveAttribute('data-cell-padding', 'normal') + + rerender() + expect(screen.getByRole('table')).toHaveAttribute('data-cell-padding', 'condensed') + + rerender() + expect(screen.getByRole('table')).toHaveAttribute('data-cell-padding', 'spacious') + }) + + it('should support specifying a rowHeader through `rowHeader` in `columns`', () => { + const columns = [ + { + header: 'Name', + field: 'name', + rowHeader: true, + }, + ] + const data = [ + { + id: 1, + name: 'one', + }, + { + id: 2, + name: 'two', + }, + { + id: 3, + name: 'three', + }, + ] + render() + for (const row of data) { + expect(screen.getByRole('rowheader', {name: row.name})).toBeInTheDocument() + } + }) +}) diff --git a/src/DataTable/index.tsx b/src/DataTable/index.tsx new file mode 100644 index 00000000000..85547bfcafa --- /dev/null +++ b/src/DataTable/index.tsx @@ -0,0 +1,435 @@ +import React from 'react' +import styled from 'styled-components' +import Box from '../Box' +import {get} from '../constants' + +// ---------------------------------------------------------------------------- +// DataTable +// ---------------------------------------------------------------------------- +interface Row { + /** + * Provide a value that uniquely identifies the row + */ + id: string | number + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any +} + +export interface DataTableProps { + /** + * Provide an id to an element which uniquely describes this table + */ + 'aria-describedby'?: string | undefined + + /** + * Provide an id to an element which uniquely labels this table + */ + 'aria-labelledby'?: string | undefined + + /** + * Specify the amount of space that should be available around the contents of + * a cell + */ + cellPadding?: 'condensed' | 'normal' | 'spacious' | undefined + + /** + * Provide a collection of the rows which will be rendered inside of the table + */ + data: Array + + /** + * Provide the columns for the table and the fields in `data` to which they + * correspond + */ + columns: Array> +} + +interface Column { + /** + * Provide the name of the column. This will be rendered as a table header + * within the table itself + */ + header: string + + /** + * Optionally provide a field to render for this column. This may be the key + * of the object or a string that accesses nested objects through `.`. For + * exmaple: `field: a.b.c` + * + * Alternatively, you may provide a `renderCell` for this column to render the + * field in a row + */ + field?: string | undefined + + /** + * Provide a custom component or render prop to render the data for this + * column in a row + */ + renderCell?: ((data: Data) => React.ReactNode) | undefined + + /** + * Specify if the value of this column for a row should be treated as a row + * header + */ + rowHeader?: boolean | undefined +} + +function DataTable({ + 'aria-labelledby': labelledby, + 'aria-describedby': describedby, + cellPadding, + columns, + data, +}: DataTableProps) { + return ( + + + + {columns.map(column => { + return {column.header} + })} + + + + {data.map(row => { + return ( + + {columns.map(column => { + const columnProps = { + scope: column.rowHeader ? 'row' : undefined, + } + + if (column.renderCell) { + return ( + + {column.renderCell(row)} + + ) + } + + if (column.field) { + const value = row[column.field] + + if (typeof value === 'string' || typeof value === 'number' || React.isValidElement(value)) { + return ( + + {value} + + ) + } + } + + return null + })} + + ) + })} + +
+ ) +} + +// ---------------------------------------------------------------------------- +// Table +// ---------------------------------------------------------------------------- + +const StyledTable = styled.table>` + /* Default table styles */ + --table-border-radius: 0.375rem; + --table-cell-padding: var(--cell-padding-block, 0.5rem) var(--cell-padding-inline, 0.75rem); + --table-font-size: 0.75rem; + + background-color: ${get('colors.canvas.default')}; + border-spacing: 0; + border-collapse: separate; + font-size: var(--table-font-size); + line-height: calc(20 / var(--table-font-size)); + width: 100%; + overflow-x: auto; + + /* Density modes: condensed, normal, spacious */ + &[data-cell-padding='condensed'] { + --cell-padding-block: 0.25rem; + --cell-padding-inline: 0.5rem; + } + + &[data-cell-padding='normal'] { + --cell-padding-block: 0.5rem; + --cell-padding-inline: 0.75rem; + } + + &[data-cell-padding='spacious'] { + --cell-padding-block: 0.75rem; + --cell-padding-inline: 1rem; + } + + /* Borders */ + .TableCell:first-child, + .TableHeader:first-child { + border-left: 1px solid ${get('colors.border.default')}; + } + + .TableCell:last-child, + .TableHeader:last-child { + border-right: 1px solid ${get('colors.border.default')}; + } + + .TableHeader, + .TableCell { + border-bottom: 1px solid ${get('colors.border.default')}; + } + + .TableHead .TableRow:first-of-type .TableHeader { + border-top: 1px solid ${get('colors.border.default')}; + } + + /* Border radius */ + .TableHead .TableRow:first-of-type .TableHeader:first-child { + border-top-left-radius: var(--table-border-radius); + } + + .TableHead .TableRow:first-of-type .TableHeader:last-child { + border-top-right-radius: var(--table-border-radius); + } + + .TableBody .TableRow:last-of-type .TableCell:first-child { + border-bottom-left-radius: var(--table-border-radius); + } + + .TableBody .TableRow:last-of-type .TableCell:last-child { + border-bottom-right-radius: var(--table-border-radius); + } + + /* TableHeader, TableCell */ + .TableCell, + .TableHeader { + padding: var(--table-cell-padding); + } + + /** + * Offset padding to make sure type aligns regardless of cell padding + * selection + */ + .TableRow > *:first-child { + padding-inline-start: 1rem; + } + + .TableRow > *:last-child { + padding-inline-end: 1rem; + } + + /* TableHeader */ + .TableHeader { + background-color: ${get('colors.canvas.subtle')}; + color: ${get('colors.fg.muted')}; + font-weight: 600; + text-align: start; + border-top: 1px solid ${get('colors.border.default')}; + } + + /* TableCell */ + .TableCell[scope='row'] { + color: ${get('colors.fg.default')}; + font-weight: 600; + text-align: start; + } + + /* Spacing if table details are present */ + .TableTitle + &, + .TableSubtitle + & { + margin-top: ${get('space.2')}; + } +` + +interface TableProps extends React.ComponentPropsWithoutRef<'table'> { + /** + * Provide an id to an element which uniquely describes this table + */ + 'aria-describedby'?: string | undefined + + /** + * Provide an id to an element which uniquely labels this table + */ + 'aria-labelledby'?: string | undefined + + children?: React.ReactNode + + /** + * Specify the amount of space that should be available around the contents of + * a cell + */ + cellPadding?: 'condensed' | 'normal' | 'spacious' | undefined +} + +const Table = React.forwardRef(function Table({cellPadding = 'normal', ...rest}, ref) { + return +}) + +// ---------------------------------------------------------------------------- +// TableHead +// ---------------------------------------------------------------------------- + +interface TableHeadProps extends React.ComponentPropsWithoutRef<'thead'> { + children?: React.ReactNode +} + +function TableHead({children}: TableHeadProps) { + return
{children}