Skip to content

Commit

Permalink
feat: Table, custom classes and alignment, (#203)
Browse files Browse the repository at this point in the history
* supporting custom classes for header and cell

* adding align functionality

* tests to cover new cases

* removing code duplication per cc

* Update src/components/Table/Table.stories.mdx

Co-authored-by: Nathan Young <1447339+nathanyoung@users.noreply.github.com>

Co-authored-by: Nathan Young <1447339+nathanyoung@users.noreply.github.com>
  • Loading branch information
juanfabrega and nathanyoung authored Sep 2, 2020
1 parent 51feaf4 commit 1f5c14e
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 45 deletions.
97 changes: 93 additions & 4 deletions src/components/Table/Table.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ NOTE: all properties in a column object are optional, but we highly recommend in
backgroundColor: '#F8F8F8',
};
const columnInterfaceRows = [
{ name: 'heading', type: 'string', description: 'The heading text to be displayed' },
{ name: 'align', type: 'left | right | center', description: 'Text alignment for column cells (including header alignment). Cells will default to left if not defined.' },
{ name: 'cellClassName', type: 'string', description: 'A CSS class to be applied to all cells in the column' },
{ name: 'dataKey', type: 'string', description: 'The key in the rows object that matches this column' },
{ name: 'className', type: 'string', description: 'A custom classname to apply to all cells in the column' },
{ name: 'emptyCellPlaceholder', type: 'string | number | undefined', description: 'placeholder for empty cells' },
{ name: 'heading', type: 'string', description: 'The heading text to be displayed' },
{ name: 'headerClassName', type: 'string', description: 'The CSS class to be applied to the column header cell.' },
{ name: 'isSortable', type: 'boolean', description: 'Whether the column is sortable. Controls displaying the sort arrows and making the column header clickable' },
{ name: 'truncateOverflow', type: 'boolean', description: 'Whether the column content will get truncated (with ellipsis) if it surpasses the fixed-width' },
{ name: 'key', type: 'React.Key', description: 'a custom key to be passed to each column. This gets autogenerated if not supplued' },
{ name: 'width', type: 'number', description: 'Specify the width of a particular column. Use in conjunction with useFixedWidthColumns in <Table />'},
{ name: 'render', type: 'function: (cell, row, rowIndex) => ReactNode', description: 'Render any custom content based on the cell and row content'},
{ name: 'truncateOverflow', type: 'boolean', description: 'Whether the column content will get truncated (with ellipsis) if it surpasses the fixed-width' },
{ name: 'width', type: 'number', description: 'Specify the width of a particular column. Use in conjunction with useFixedWidthColumns in <Table />'},
];
const columnConfig = [
{ heading: 'Name', dataKey: 'name' },
Expand Down Expand Up @@ -743,3 +745,90 @@ You can combine any of these boolean props based on your UI needs.
}}
</Story>
</Canvas>

## Aligning Cell Text

Align cell content as needed with the `align` prop. Either pass the prop at the `<Table />` level for all columns, or individually
in a single `Column` object. NOTE: right alignment is best reserved for numerical data display.
We recommend refraining from specific alignment unless it serves the data, as alignment discrepancies in different columns
may interrupt the natural reading flow for a user.

<Canvas>
<Story name="Aligning Cell Text">
{() => {
const columnConfig = [
{ heading: 'Right aligned', dataKey: 'id', align: 'right' },
{ heading: 'Center aligned', dataKey: 'color', align: 'center' },
{ heading: 'Default Left', dataKey: 'flavor' },
];
const tableData = [
{ id: 1, color: 'red', flavor: 'vanilla' },
{ id: 2, color: 'blue', flavor: 'chocolate' },
{ id: 3, color: 'green', flavor: 'strawberry' },
];
return (
<Table
rowKey="id"
columns={columnConfig}
rows={tableData}
/>
);
}}
</Story>
</Canvas>

The below is an example of setting the `align` prop globally in the `<Table />` and overwriting its value on a specific column

<Canvas>
<Story name="Global Align">
{() => {
const columnConfig = [
{ heading: 'Center Aligned via Column Config', dataKey: 'id', align: 'center' },
{ heading: 'Globally aligned right', dataKey: 'color' },
{ heading: 'Also globally aligned right', dataKey: 'flavor' },
];
const tableData = [
{ id: 1, color: 'red', flavor: 'vanilla' },
{ id: 2, color: 'blue', flavor: 'chocolate' },
{ id: 3, color: 'green', flavor: 'strawberry' },
];
return (
<Table
rowKey="id"
columns={columnConfig}
rows={tableData}
align="right"
/>
);
}}
</Story>
</Canvas>

## Custom Column Classes

You can add custom classes to both your table header cell as well as all the cells in a column by passing the Classes
in the column config.

<Canvas>
<Story name="Custom Column Classes">
{() => {
const columnConfig = [
{ heading: 'with cellClassName', dataKey: 'id', cellClassName: 'background-color-secondary-lightest' },
{ heading: 'With headerClassName', dataKey: 'color', headerClassName: 'background-color-tertiary-lightest' },
{ heading: 'Flavor', dataKey: 'flavor' },
];
const tableData = [
{ id: 1, color: 'red', flavor: 'vanilla' },
{ id: 2, color: 'blue', flavor: 'chocolate' },
{ id: 3, color: 'green', flavor: 'strawberry' },
];
return (
<Table
rowKey="id"
columns={columnConfig}
rows={tableData}
/>
);
}}
</Story>
</Canvas>
107 changes: 105 additions & 2 deletions src/components/Table/Table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ describe('Table', () => {
() => {
render(<Table columns={columnConfig} rows={tableData} rowKey="id" />);

const rows = screen.queryAllByRole('cell');
const cells = screen.queryAllByRole('cell');

expect(rows).toHaveLength(9);
expect(cells).toHaveLength(9);
},
);

Expand Down Expand Up @@ -283,5 +283,108 @@ describe('Table', () => {
expect(submitButton).toBeInTheDocument();
});
});

describe('Text alignment of columns', () => {
test('it renders column cells with text aligned based on global align prop', () => {
const { rerender } = render(
<Table
columns={columnConfig}
rows={tableData}
rowKey="id"
align="right"
/>,
);

const cellsRight = screen.queryAllByRole('cell');
cellsRight.forEach(cell => {
expect(cell).toHaveClass('align-right');
});

rerender(
<Table
columns={columnConfig}
rows={tableData}
rowKey="id"
align="center"
/>,
);

const cellsCenter = screen.queryAllByRole('cell');
cellsCenter.forEach(cell => {
expect(cell).toHaveClass('align-center');
});
});

test('It renders column cells with alignment based on column align prop', () => {
const columnConfigWithAlign = [
{ heading: 'ID', dataKey: 'id' },
{ heading: 'Color', dataKey: 'color', align: 'left' as const },
{ heading: 'Flavor', dataKey: 'flavor', align: 'right' as const },
];

const tableDataAlign = [
{ id: 1, color: 'red', flavor: 'vanilla' },
{ id: 2, color: 'blue', flavor: 'chocolate' },
{ id: 3, color: 'green', flavor: 'strawberry' },
];

render(
<Table
columns={columnConfigWithAlign}
rows={tableDataAlign}
rowKey="id"
align="center"
/>,
);

const cells = screen.queryAllByRole('cell');
// Checking cell classes based on where they are in the table.
cells.forEach((cell, index) => {
// First column
if (index === 0 || index === 3 || index === 6) {
expect(cell).toHaveClass('align-center');
}
// Second Column
if (index === 1 || index === 4 || index === 7) {
expect(cell).not.toHaveClass('align-center');
expect(cell).not.toHaveClass('align-right');
}
// Third Column
if (index === 2 || index === 5 || index === 8) {
expect(cell).toHaveClass('align-right');
}
});
});
});

describe('Custom cell classes', () => {
test('It renders columns with classes passed in the column config', () => {
const columnConfigClasses = [
{ heading: 'ID', dataKey: 'id', headerClassName: 'header-class' },
{ heading: 'Color', dataKey: 'color', cellClassName: 'cell-class' },
{ heading: 'Flavor', dataKey: 'flavor', align: 'right' as const },
];

render(
<Table
columns={columnConfigClasses}
rows={tableData}
rowKey="id"
/>,
);

const headerCells = screen.queryAllByRole('columnheader');
expect(headerCells[0]).toHaveClass('header-class'); // Header(th) for first column.

const cells = screen.queryAllByRole('cell');
// Checking cell classes based on where they are in the table.
cells.forEach((cell, index) => {
// Second Column
if (index === 1 || index === 4 || index === 7) {
expect(cell).toHaveClass('cell-class');
}
});
});
});
});
});
8 changes: 8 additions & 0 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ interface TableProps {
* Additional classes to add.
*/
className?: string;
/**
* Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
* for a specific column.
*/
align?: 'left' | 'right' | 'center';
/**
* Global placeholder for empty cells. Can be overwritten by setting the same attribute
* in the `Column` config.
Expand Down Expand Up @@ -85,6 +90,7 @@ const Table: FC<TableProps> = ({
columns,
rows,
rowKey,
align = 'left',
className = undefined,
emptyCellPlaceholder = undefined,
hoverableRows = false,
Expand Down Expand Up @@ -137,6 +143,7 @@ const Table: FC<TableProps> = ({
<table className={tableClasses}>
<TableHead
columns={columns}
align={align}
onSort={onSort}
isBorderless={isBorderless}
isCompact={isCompact}
Expand All @@ -148,6 +155,7 @@ const Table: FC<TableProps> = ({
rows={rows}
columns={columns}
rowKey={rowKey}
align={align}
isStriped={isStriped}
emptyCellPlaceholder={emptyCellPlaceholder}
hoverableRows={hoverableRows}
Expand Down
7 changes: 7 additions & 0 deletions src/components/Table/TableBody/TableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ interface TableBodyProps {
* The table rows to be rendered
*/
rows: Row[];
/**
* Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
* for a specific column.
*/
align?: 'left' | 'right' | 'center';
/**
* A custom class to apply to the table body.
*/
Expand Down Expand Up @@ -58,6 +63,7 @@ const TableBody: FC<TableBodyProps> = ({
columns,
rowKey,
rows,
align = 'left',
className = '',
emptyCellPlaceholder = '',
hoverableRows = false,
Expand All @@ -83,6 +89,7 @@ const TableBody: FC<TableBodyProps> = ({
columns={columns}
row={row}
rowIndex={rowIndex}
align={align}
key={row[rowKey]}
emptyCellPlaceholder={emptyCellPlaceholder}
truncateOverflow={truncateOverflow}
Expand Down
8 changes: 8 additions & 0 deletions src/components/Table/TableCell/TableCell.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@
text-overflow: ellipsis;
white-space: nowrap;
}

&.align-right {
text-align: right;
}

&.align-center {
text-align: center;
}
}
8 changes: 8 additions & 0 deletions src/components/Table/TableCell/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import classNames from 'classnames';
import styles from './TableCell.module.scss';

interface TableCellProps {
/**
* Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
* for a specific column.
*/
align?: 'left' | 'right' | 'center';
/**
* Children node to be rendered.
*/
Expand Down Expand Up @@ -37,6 +42,7 @@ interface TableCellProps {
}

const TableCell: FC<TableCellProps> = ({
align = 'left',
children = null,
className = '',
emptyCellPlaceholder = null,
Expand All @@ -51,6 +57,8 @@ const TableCell: FC<TableCellProps> = ({
[styles.compact]: isCompact,
[styles.borderless]: isBorderless,
[styles.truncated]: truncateOverflow,
[styles['align-right']]: align === 'right',
[styles['align-center']]: align === 'center',
},
className,
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/Table/TableHead/TableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ interface TableHeadProps {
* The table columns to be rendered
*/
columns: Column[];
/**
* Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
* for a specific column.
*/
align?: 'left' | 'right' | 'center';
/**
* Custom class to be applied to the `<thead>` element.
*/
Expand Down Expand Up @@ -45,6 +50,7 @@ interface TableHeadProps {

const TableHead: FC<TableHeadProps> = ({
columns,
align = 'left',
className = '',
isBorderless = false,
isCompact = false,
Expand All @@ -59,6 +65,7 @@ const TableHead: FC<TableHeadProps> = ({
<thead className={tableHeadClasses}>
<TableRow
columns={columns}
align={align}
isTableHead
isBorderless={isBorderless}
isCompact={isCompact}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
text-overflow: ellipsis;
white-space: nowrap;
}

&.align-right {
text-align: right;
}

&.align-center {
text-align: center;
}
}

.sort-icon {
Expand Down
Loading

0 comments on commit 1f5c14e

Please sign in to comment.