Skip to content

Commit

Permalink
Merge pull request #333 from lifeomic/PHC-4380-TableModule
Browse files Browse the repository at this point in the history
feat(PHC-4380): row selection to TableModule
  • Loading branch information
shawnzhu committed Mar 23, 2023
2 parents 2c5973d + c873ce9 commit 4029016
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 20 deletions.
85 changes: 81 additions & 4 deletions src/components/TableModule/TableModule.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import React, { useRef } from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { TableModule } from './TableModule';
import { TableConfiguration, TableSortClickProps } from './types';
import {
RowSelectionState,
TableConfiguration,
TableSortClickProps,
} from './types';
import { TableModuleActions } from './TableModuleActions';
import { Checkbox } from '../Checkbox';
import { IconButton } from '../IconButton';
import { Share, Trash } from '@lifeomic/chromicons';
import { Button } from '../Button';
Expand Down Expand Up @@ -259,10 +264,10 @@ Sticky.parameters = {
docs: {
description: {
story: `Columns can be made "sticky" or so they don't travel off-screen when scrolling the
table horizontally. This helps keep track of what row one is looking at in tables with more
table horizontally. This helps keep track of what row one is looking at in tables with more
columns than can be visible at one time in the document. \n \n Any number of columns can be made
sticky. They don't have to be consecutive, and can start and end at any column. However,
most common use-cases will likely just involve the first one or two consecutive columns
sticky. They don't have to be consecutive, and can start and end at any column. However,
most common use-cases will likely just involve the first one or two consecutive columns
being sticky.`,
},
},
Expand Down Expand Up @@ -443,3 +448,75 @@ actions toolbar.`,
},
},
};

export const RowSelection: ComponentStory<typeof TableModule> = (args) => {
const tableRef = useRef<HTMLTableElement | null>(null);
const initialRowSelection: RowSelectionState = { '0': true };
const [rowSelection, setRowSelection] = React.useState(initialRowSelection);
const selectionColumn = {
id: 'select',
header: {
content: () => '',
},
cell: {
content: (rowData: any) => (
<Checkbox
label=" "
checked={rowData.getIsSelected()}
disabled={!rowData.getCanSelect()}
onChange={rowData.getToggleSelectedHandler()}
/>
),
},
};
return (
<TableModule
{...args}
data={data}
config={[
selectionColumn,
{
header: {
label: 'Description',
},
cell: {
valuePath: 'description',
},
},
{
header: {
label: 'Calories',
},
cell: {
valuePath: 'calories',
},
},
{
header: {
label: 'Fat',
},
cell: {
valuePath: 'fat',
},
},
]}
ref={tableRef}
rowClickLabel="row-click-label"
enableRowSelection={true}
state={{ rowSelection }}
onRowSelectionChange={setRowSelection}
/>
);
};

RowSelection.parameters = {
docs: {
description: {
story: `It is a common design pattern to include multi-selection for a
table. The TableModule exposes properties like \`enableRowSelection\` to
enable this capability and use \`state.rowSelection\` to initialize row
selection state. In order to access row selection state, use the \`onRowSelectionChange\`
handler.`,
},
},
};
26 changes: 13 additions & 13 deletions src/components/TableModule/TableModule.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { log, error } from 'console';

const testId = 'TableModule';

const configWithCellContent: Array<TableConfiguration> = [
const configWithCellContent: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand All @@ -37,7 +37,7 @@ const configWithCellContent: Array<TableConfiguration> = [
},
];

const configWithCellValuePath: Array<TableConfiguration> = [
const configWithCellValuePath: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -125,7 +125,7 @@ test('it renders the provided "noResultsMessage"', async () => {
});

test('it renders columns using "header.label"', async () => {
const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -154,7 +154,7 @@ test('it renders columns using "header.label"', async () => {
});

test('it renders columns using "header.content"', async () => {
const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
content: (header: TableHeader) => {
Expand Down Expand Up @@ -228,7 +228,7 @@ test('it renders a table with data using "cell.valuePath"', async () => {
});

test('it applies the provided class to a cell with data using "cell.className"', async () => {
const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'foo',
Expand Down Expand Up @@ -261,7 +261,7 @@ test('it applies the provided class to a cell with data using "cell.className"',
});

test('it respects the "align" properties on "config"', async () => {
const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
align: 'right',
Expand Down Expand Up @@ -310,7 +310,7 @@ test('it respects the "align" properties on "config"', async () => {
});

test('it renders a single column table appropriately', async () => {
const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -414,7 +414,7 @@ test('it applies "maxCellWidth=2"', async () => {
test('it sorts on column click from no sort => sort asc', async () => {
const sortFn = jest.fn();

const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -468,7 +468,7 @@ test('it sorts on column click from no sort => sort asc', async () => {
test('it sorts on column click from no sort => sort asc => sort desc', async () => {
const sortFn = jest.fn();

const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -527,7 +527,7 @@ test('it sorts on column click from no sort => sort asc => sort desc', async ()
test('it sorts on column click from no sort => sort asc => sort desc => no sort', async () => {
const sortFn = jest.fn();

const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -590,7 +590,7 @@ test('it sorts by first column click, then resets sort when a secondary column i
const col1SortFn = jest.fn();
const col2SortFn = jest.fn();

const config: Array<TableConfiguration> = [
const config: Array<TableConfiguration<any>> = [
{
header: {
label: 'Description',
Expand Down Expand Up @@ -671,7 +671,7 @@ test('it uses the provided "sortState"', async () => {
});

test('it sets isSticky class on all sticky columns', async () => {
const config: Array<TableConfiguration> = configWithStickyColumns;
const config: Array<TableConfiguration<any>> = configWithStickyColumns;

const { findByTestId } = renderWithTheme(
<TableModule data-testid={testId} config={config} data={data} />
Expand Down Expand Up @@ -705,7 +705,7 @@ test('it sets isSticky class on all sticky columns', async () => {
});

test('it sets "isStickyLast" class on last sticky column', async () => {
const config: Array<TableConfiguration> = configWithStickyColumns;
const config: Array<TableConfiguration<any>> = configWithStickyColumns;

const { findByTestId } = renderWithTheme(
<TableModule data-testid={testId} config={config} data={data} />
Expand Down
63 changes: 62 additions & 1 deletion src/components/TableModule/TableModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { TableHeaderCell } from './TableHeaderCell';
import { TableModuleRow } from './TableModuleRow';
import { warning } from '../../utils';
import {
RowSelectionState,
RowSelectionRow,
TableSortState,
TableHeader,
TableCell,
TableConfiguration,
TableSortClickProps,
TableState,
} from './types';
import * as React from 'react';
import clsx from 'clsx';
Expand Down Expand Up @@ -227,6 +230,10 @@ export interface TableModuleProps<Item = any>
maxCellWidth?: 1 | 2;
rowActions?: (row: any) => React.ReactNode;
rowClickLabel?: string;
state?: Partial<TableState>;
enableRowSelection?: boolean;
getRowId?: (row: Item, index: number) => string;
onRowSelectionChange?: (state: RowSelectionState) => void;
}

/**
Expand Down Expand Up @@ -263,6 +270,10 @@ export const TableModule = React.memo(
maxCellWidth,
rowActions,
rowClickLabel,
state,
enableRowSelection = false,
getRowId = (_, index) => index.toString(),
onRowSelectionChange,
...rootProps
},
forwardedRef
Expand All @@ -278,6 +289,10 @@ export const TableModule = React.memo(

const [sort, setSort] = React.useState<TableSortState>(sortState);

const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(
state?.rowSelection || {}
);

const [headings, setHeadings] = React.useState<Array<TableHeader>>(
config?.map((c) => c.header) || []
);
Expand Down Expand Up @@ -412,6 +427,51 @@ export const TableModule = React.memo(
}
};

const isRowSelected = (row: any, index: number): boolean => {
return rowSelection[getRowId(row, index)] ?? false;
};

const createRow = React.useCallback(
(row: any, index: number): RowSelectionRow => ({
getIsSelected: () => {
return isRowSelected(row, index);
},
getCanSelect: () => enableRowSelection,
toggleSelected: (value?: boolean) => {
const isSelected = isRowSelected(row, index);
value = typeof value !== 'undefined' ? value : !isSelected;

if (isSelected === value) {
return;
}

const newRowSelection = { ...rowSelection };

if (isSelected) {
delete newRowSelection[getRowId(row, index)];
} else {
newRowSelection[getRowId(row, index)] = true;
}

setRowSelection(newRowSelection);

if (onRowSelectionChange) {
onRowSelectionChange(newRowSelection);
}
},
getToggleSelectedHandler: () => {
return (e: unknown) => {
if (enableRowSelection) {
row.toggleSelected(
((e as MouseEvent).target as HTMLInputElement).checked
);
}
};
},
}),
[setRowSelection, isRowSelected, onRowSelectionChange]
);

return (
<table
role="table"
Expand Down Expand Up @@ -483,14 +543,15 @@ export const TableModule = React.memo(
</tr>
)}
{data?.map((row, rowIndex) => {
const rowData = createRow(row, rowIndex);
return (
<TableModuleRow
key={`tableRow-${rowIndex}`}
data={row}
onRowClick={onRowClick}
rowRole={rowRole}
maxCellWidth={maxCellWidth}
row={row}
row={Object.assign(row, rowData)}
headingsLength={headings?.length}
cells={cells}
rowActions={rowActions}
Expand Down
18 changes: 16 additions & 2 deletions src/components/TableModule/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,26 @@ export interface TableHeader extends TableAlignOptions {

export interface TableCell<Item = any> extends TableAlignOptions {
valuePath?: string;
content?(cell: Item): any;
content?(row: Item): any;
className?: string;
}

export interface TableConfiguration<Item = any> {
export interface TableConfiguration<Item extends RowSelectionRow> {
header: TableHeader;
cell: TableCell<Item>;
isSticky?: boolean;
}

// stanStack table core APIs
export type RowSelectionState = Record<string, boolean>;

export interface TableState {
rowSelection: RowSelectionState;
}

export interface RowSelectionRow {
getIsSelected?: () => boolean;
getCanSelect?: () => boolean;
toggleSelected?: (value?: boolean) => void;
getToggleSelectedHandler?: () => (event: unknown) => void;
}

0 comments on commit 4029016

Please sign in to comment.