Skip to content

Commit

Permalink
feat(react,styles): add grid layout for table component (#1748)
Browse files Browse the repository at this point in the history
  • Loading branch information
scurker authored Nov 22, 2024
1 parent 242ac5d commit 983c89f
Show file tree
Hide file tree
Showing 11 changed files with 586 additions and 115 deletions.
131 changes: 131 additions & 0 deletions docs/pages/components/Table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,115 @@ function SortableTableExample() {
</Table>
```

### Grid Layout

The Table component supports an optional css grid layout that can specify column alignment and width definitions per column.

```jsx example
<Table
layout="grid"
columns={[
{ width: 'max-content', align: 'start' },
{ width: 'max-content', align: 'start' },
{ width: '1fr', align: 'end' }
]}>
<TableHead>
<TableRow>
<TableHeader scope="col">First Name</TableHeader>
<TableHeader scope="col">Last Name</TableHeader>
<TableHeader scope="col">Email</TableHeader>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Frank</TableCell>
<TableCell>Zappa</TableCell>
<TableCell>frank@zappa.io</TableCell>
</TableRow>
<TableRow>
<TableCell>Duane</TableCell>
<TableCell>Allman</TableCell>
<TableCell>duane@almond.biz</TableCell>
</TableRow>
<TableRow>
<TableCell>Yamandu</TableCell>
<TableCell>Costa</TableCell>
<TableCell>yamandu_costa@gmail.br</TableCell>
</TableRow>
<TableRow>
<TableCell>Jimmy</TableCell>
<TableCell>Herring</TableCell>
<TableCell>jamesHerring@hotmail.gov</TableCell>
</TableRow>
</TableBody>
</Table>
```

For column alignments, all cells will be positioned according to the alignment specified for that column, defaulting to `start`:

<Table layout="grid" columns={[{ width: 'max-content', align: 'start' }, { align: 'start' }]}>
<TableHead>
<TableRow>
<TableHeader scope="col">Alignment Type</TableHeader>
<TableHeader scope="col">Description</TableHeader>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>`start`</TableCell>
<TableCell>Aligns all cells within the column to the start.</TableCell>
</TableRow>
<TableRow>
<TableCell>`center`</TableCell>
<TableCell>Aligns all cells within the column to the center.</TableCell>
</TableRow>
<TableRow>
<TableCell>`end`</TableCell>
<TableCell>Aligns all cells within the column to the center.</TableCell>
</TableRow>
</TableBody>
</Table>

For column sizing, the values are roughly equivalent to [track sizing for grid-template-columns](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns)
and the following values are supported:

<Table layout="grid" columns={[{ width: 'max-content', align: 'start' }, { align: 'start' }]}>
<TableHead>
<TableRow>
<TableHeader scope="col">Width Type</TableHeader>
<TableHeader scope="col">Description</TableHeader>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>`auto`</TableCell>
<TableCell>Sizes the column between a range of `min-content` and `max-content`. </TableCell>
</TableRow>
<TableRow>
<TableCell>`max-content`</TableCell>
<TableCell>Will size the column respective to the largest cell.</TableCell>
</TableRow>
<TableRow>
<TableCell>`min-content`</TableCell>
<TableCell>Will size the column respective to the smallest cell.</TableCell>
</TableRow>
<TableRow>
<TableCell>`number`</TableCell>
<TableCell>Applies a fixed width to the column.</TableCell>
</TableRow>
<TableRow>
<TableCell>`<number>fr`</TableCell>
<TableCell>Applies a flex value that sizes a column proportional to the remaining space from other columns.</TableCell>
</TableRow>
<TableRow>
<TableCell>`<number>%`</TableCell>
<TableCell>Applies a percentage width to the column respective to the size of the table.</TableCell>
</TableRow>
</TableBody>
</Table>

For more advanced usage, the width value does not need to be specified as part of a column but can be specified using `--table-grid-template-columns` to set the respective column sizes relative to the content displayed within the table.

## Props

### Table
Expand All @@ -346,6 +455,16 @@ function SortableTableExample() {
name: 'variant',
type: 'bordered',
description: 'Use the bordered variant of Table'
},
{
name: 'layout',
type: 'grid',
description: 'When set, uses a css grid layout instead of a table layout.'
},
{
name: 'columns',
type: ['Array<Column>', 'number'],
description: 'Only allowed when the table has a grid layout. Sets the column widths and alignments for each column.'
}
]}
/>
Expand Down Expand Up @@ -379,6 +498,11 @@ function SortableTableExample() {
type: 'function',
description: 'The function that the implementer passes in to control the change of sort direction.',
defaultValue: 'sorted ascending'
},
{
name: 'align',
type: ['start', 'center', 'end'],
description: 'Only allowed when the table has a grid layout. Overrides any column alignments for this table header.'
}
]}
/>
Expand Down Expand Up @@ -416,4 +540,11 @@ function SortableTableExample() {
<ComponentProps
children={true}
className={true}
props={[
{
name: 'align',
type: ['start', 'center', 'end'],
description: 'Only allowed when the table has a grid layout. Overrides any column alignments for this table cell.'
}
]}
/>
111 changes: 96 additions & 15 deletions packages/react/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,102 @@
import React, { TableHTMLAttributes } from 'react';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { TableProvider } from './TableContext';

interface TableProps extends TableHTMLAttributes<HTMLTableElement> {
export type Column = {
align: ColumnAlignment;
width?: ColumnWidth;
};
export type ColumnAlignment = 'start' | 'center' | 'end';
export type ColumnWidth =
| 'auto'
| 'min-content'
| 'max-content'
| `${number}`
| `${number}%`
| `${number}fr`;
export type RowAlignment = 'start' | 'center';

type TableBaseProps = {
layout: never;
columns: never;
variant?: 'border';
};

type TableGridProps = {
layout: 'grid';
columns?: Array<Column> | number;
variant?: 'border';
}

const Table = ({ children, className, variant, ...other }: TableProps) => (
<table
className={classNames(
'Table',
variant === 'border' && 'Table--border',
className
)}
{...other}
>
{children}
</table>
};

type TableProps = (TableBaseProps | Partial<TableGridProps>) &
React.TableHTMLAttributes<HTMLTableElement>;

const Table = React.forwardRef<HTMLTableElement, TableProps>(
(
{
children,
className,
variant,
layout,
columns: columnsProp = [],
style,
...other
},
ref
) => {
const isGridLayout = layout === 'grid';
const columns: Column[] = useMemo(() => {
if (typeof columnsProp === 'number') {
return columnsProp > 0
? Array(columnsProp).fill({ align: 'start' })
: [{ align: 'start' }];
}

return columnsProp;
}, [columnsProp]);

const styleTemplateColumns = useMemo(() => {
if (layout !== 'grid') {
return;
}

if (!columns) {
return 'auto';
}

return columns
.map(({ width }) => width || 'auto')
.join(' ');
}, [layout, columns]);

const tableGridStyles: React.CSSProperties = isGridLayout
? ({
'--table-grid-template-columns': styleTemplateColumns
} as React.CSSProperties)
: {};

return (
<table
ref={ref}
style={{
...tableGridStyles,
...style
}}
className={classNames('Table', className, {
'Table--border': variant === 'border',
TableGrid: isGridLayout
})}
{...other}
>
<TableProvider
layout={isGridLayout ? 'grid' : 'table'}
columns={columns}
>
{children}
</TableProvider>
</table>
);
}
);

Table.displayName = 'Table';
Expand Down
20 changes: 11 additions & 9 deletions packages/react/src/components/Table/TableBody.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { HTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import classNames from 'classnames';

const TableBody = ({
children,
className,
...other
}: HTMLAttributes<HTMLTableSectionElement>) => (
<tbody className={classNames('TableBody', className)} {...other}>
{children}
</tbody>
type TableBodyProps = React.HTMLAttributes<HTMLTableSectionElement> & {
className?: string;
};

const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
({ children, className, ...other }, ref) => (
<tbody ref={ref} className={classNames('TableBody', className)} {...other}>
{children}
</tbody>
)
);

TableBody.displayName = 'TableBody';
Expand Down
41 changes: 32 additions & 9 deletions packages/react/src/components/Table/TableCell.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
import React, { TdHTMLAttributes } from 'react';
import type { ColumnAlignment } from './Table';
import React, { forwardRef } from 'react';
import classNames from 'classnames';
import { useTable } from './TableContext';
import useTableGridStyles from './useTableGridStyles';
import useSharedRef from '../../utils/useSharedRef';

const TableCell = ({
children,
className,
...other
}: TdHTMLAttributes<HTMLTableCellElement>) => (
<td className={classNames('TableCell', className)} {...other}>
{children}
</td>
interface TableCellProps
extends Omit<React.TdHTMLAttributes<HTMLTableDataCellElement>, 'align'> {
align?: ColumnAlignment;
}

const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
({ children, className, align, style, ...other }, ref) => {
const tableCellRef = useSharedRef<HTMLTableCellElement>(ref);
const { layout, columns } = useTable();
const tableGridStyles = useTableGridStyles({
elementRef: tableCellRef,
align,
columns,
layout
});

return (
<td
ref={tableCellRef}
style={{ ...tableGridStyles, ...style }}
className={classNames('TableCell', className)}
{...other}
>
{children}
</td>
);
}
);

TableCell.displayName = 'TableCell';
Expand Down
47 changes: 47 additions & 0 deletions packages/react/src/components/Table/TableContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, {
createContext,
useContext,
useState,
useMemo,
useRef
} from 'react';
import type { Column } from './Table';

type TableContext = {
layout: 'table' | 'grid';
columns: Array<Column>;
};

type TableProvider = {
children: React.ReactNode;
layout: 'table' | 'grid';
columns: Array<Column>;
};

const TableContext = createContext<TableContext>({
layout: 'table',
columns: []
});

function TableProvider({
children,
layout,
columns
}: TableProvider): JSX.Element {
const { Provider } = TableContext as React.Context<TableContext>;
const contextValue: TableContext = useMemo(
() => ({
layout,
columns
}),
[layout, columns]
);

return <Provider value={contextValue}>{children}</Provider>;
}

function useTable(): TableContext {
return useContext(TableContext);
}

export { TableProvider, useTable };
Loading

0 comments on commit 983c89f

Please sign in to comment.