Skip to content

Commit

Permalink
chore: implemented table row selection
Browse files Browse the repository at this point in the history
  • Loading branch information
aferro-wwnorton committed Apr 9, 2024
1 parent b5ccb4f commit 274dab7
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 61 deletions.
2 changes: 0 additions & 2 deletions packages/react/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { TableContext, useInitTableState } from './context';

export const Table: React.FC<TableProps> = ({
className: tableClassName = 'nds-table',
// TODO: do we need this?
// selectableClass = `${tableClassName}--selectable`,
children,
selectable,
onSelect,
Expand Down
10 changes: 6 additions & 4 deletions packages/react/src/components/Table/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { TableHeaderProps } from './types';
import { useTableState } from './context';
import { TableHeadCell } from './TableHeadCell';
import { Checkbox } from '../Checkbox';
import { useId } from '../../utilities';
import { TableRow } from './TableRow';

export const TableHeader: React.FC<TableHeaderProps> = ({ children, className = 'nds-table__header' }) => {
const { selectable, onSelect } = useTableState();
const { selectable, onSelect, onSelectedAll, isSelectedAll } = useTableState();
const uniqueId = useId();

return (
<thead className={className}>
<TableRow isHeader>
{ selectable ? (
<TableHeadCell onSelect={onSelect}>
<Checkbox />
{ selectable && onSelect ? (
<TableHeadCell>
<Checkbox id={uniqueId} checked={isSelectedAll} onChange={onSelectedAll} />
</TableHeadCell>
) : null }
{children}
Expand Down
21 changes: 17 additions & 4 deletions packages/react/src/components/Table/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import React from 'react';
import classNames from 'classnames';
import { TableRowProps } from './types';
import React from "react";
import classNames from "classnames";
import { TableRowProps } from "./types";
import { useTableState } from './context';
import { Checkbox } from '../Checkbox';
import { useId } from '../../utilities';

export const TableRow: React.FC<TableRowProps> = ({
baseName = 'nds-table-row',
baseName = "nds-table-row",
headerClass = `${baseName}__header`,
sectionHeaderClass = `${baseName}__header--section`,
isHeader,
isSectionHeader,
children,
}) => {

const { selectable, onSelect, selected, onSelected, isSelectedAll } = useTableState();
const uniqueId = useId();

const trClassName = classNames(baseName, {
[`${headerClass}`]: isHeader,
[`${sectionHeaderClass}`]: isSectionHeader,
});
const isSelected =isSelectedAll || selected?.includes(`${uniqueId}`);
const showCheckbox = selectable && onSelect && !isHeader && !isSectionHeader

return (
<tr className={trClassName}>
{ showCheckbox ? (
<td>
<Checkbox id={uniqueId} checked={isSelected} disabled={isSelectedAll} onChange={onSelected} />
</td>) : null }
{children}
</tr>
);
Expand Down
55 changes: 37 additions & 18 deletions packages/react/src/components/Table/context.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
import React, { useMemo, useState } from 'react';
import React, { useState } from "react";
import { TableState } from "./types";

export interface TableState {
selectable?: boolean;
onSelect?: () => void;
}

export const TableContext = React.createContext<TableState | undefined>(undefined);
export const TableContext = React.createContext<TableState | undefined>(
undefined
);

export function useTableState() {
const result = React.useContext(TableContext);
if (!result) {
throw new Error('TableContext not initialized');
throw new Error("TableContext not initialized");
}

return result;
}

export function useInitTableState({ selectable }: TableState): TableState {
// TODO: should we do something with onSelect here?
const [innerSelectable, setSelectable] = useState(selectable);

return useMemo(
() => ({
selectable: innerSelectable,
setSelectable,
}),
[innerSelectable],
export function useInitTableState({
selectable,
onSelect,
selected,
}: TableState): TableState {
const [isSelectedAll, setIsSelectedAll] = React.useState<boolean>(false);
const [uniqueIds, setUniqueIds] = useState<Set<string>>(
new Set(selected || [])
);

const onSelectedAll = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsSelectedAll(event.target.checked || false);
onSelected(event);
};
const onSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
const newSet = new Set(uniqueIds); // Create a copy to avoid mutation
if (event.target.checked) {
newSet.add(event.target.id);
} else {
newSet.delete(event.target.id);
}
setUniqueIds(newSet);
onSelect?.([...newSet]);
};
return {
selectable,
onSelect,
selected: [...uniqueIds],
onSelected,
onSelectedAll,
isSelectedAll,
};
}
72 changes: 42 additions & 30 deletions packages/react/src/components/Table/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import {
Table, TableHeadCell, TableBody, TableRow, TableHeader,
} from './index';
Table,
TableHeadCell,
TableBody,
TableRow,
TableHeader,
} from "./index";

const meta: Meta<typeof Table> = {
title: 'Table',
title: "Table",
component: Table,
};
export default meta;
Expand All @@ -14,31 +18,39 @@ type Story = StoryObj<typeof Table>;

// default table - presentation only
export const Default: Story = {
render: () => (
<Table selectable>
<TableHeader>
<TableHeadCell sortType="ascending" onSort={() => { }}>
Header 1
</TableHeadCell>
<TableHeadCell sortType="descending" onSort={() => { }}>
Header 2
</TableHeadCell>
<TableHeadCell>Header 3</TableHeadCell>
</TableHeader>
<TableBody>
<TableRow>
<td>Row 1, Cell 1</td>
<td>Row 1, Cell 2</td>
<td>Row 1, Cell 3</td>
</TableRow>
<TableRow>
<td>Row 2, Cell 1</td>
<td>Row 2, Cell 2</td>
<td>Row 2, Cell 3</td>
</TableRow>
</TableBody>
</Table>
),
render: () => {
const [message, setMessage] = React.useState<string>("0 row(s) selected");
const handleSelect = (selected?: string[] ) => selected && setMessage(`${ selected.includes('check-all') ? 'All' : selected.length } row(s) selected!`);

return (
<>
<Table selectable onSelect={handleSelect}>
<TableHeader>
<TableHeadCell sortType="ascending" onSort={() => {}}>
Header 1
</TableHeadCell>
<TableHeadCell sortType="descending" onSort={() => {}}>
Header 2
</TableHeadCell>
<TableHeadCell>Header 3</TableHeadCell>
</TableHeader>
<TableBody>
<TableRow>
<td>Row 1, Cell 1</td>
<td>Row 1, Cell 2</td>
<td>Row 1, Cell 3</td>
</TableRow>
<TableRow>
<td>Row 2, Cell 1</td>
<td>Row 2, Cell 2</td>
<td>Row 2, Cell 3</td>
</TableRow>
</TableBody>
</Table>
<div aria-live="polite">{message}</div>
</>
);
},
};

// selectable table
Expand Down
13 changes: 10 additions & 3 deletions packages/react/src/components/Table/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
export interface TableState {
selectable?: boolean;
isSelectedAll?: boolean;
onSelect?: (data?: any) => void;
onSelected?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSelectedAll?: (event: React.ChangeEvent<HTMLInputElement>) => void;
selected?: string[];
}

export interface TableProps {
className?: string;
selectableClass?: string;
selectable?: boolean;
onSelect?: () => void;
onSelect?: (data?: any) => void;
}

export interface TableRowProps {
Expand All @@ -17,8 +26,6 @@ export interface TableHeadCellProps {
className?: string;
sortType?: 'none' | 'ascending' | 'descending' | 'other' | undefined;
onSort?: VoidFunction;
// TODO: do we need to include this here?
onSelect?: VoidFunction
}

export interface TableHeaderProps {
Expand Down

0 comments on commit 274dab7

Please sign in to comment.