Skip to content

Commit

Permalink
refactor: row selection
Browse files Browse the repository at this point in the history
  • Loading branch information
aferro-wwnorton committed Apr 9, 2024
1 parent ede3d4e commit 064aa34
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 50 deletions.
5 changes: 1 addition & 4 deletions packages/react/src/components/Table/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { TableHeaderProps } from './types';
import { useTableState } from './context';
import { TableHeadCell } from './TableHeadCell';
import { Checkbox } from '../Checkbox';
import { useId } from '../../utilities';
import { TableRow } from './TableRow';

const SELECT_ALL_HEADER_LABEL = 'Select All';
Expand All @@ -13,16 +12,14 @@ export const TableHeader: React.FC<TableHeaderProps> = ({
className = 'nds-table__header',
}) => {
const { selectable, onSelect, onSelectedAll, isSelectedAll } = useTableState();
const uniqueId = useId();

return (
<thead className={className}>
<TableRow isHeader>
{selectable && onSelect ? (
<TableHeadCell aria-label={SELECT_ALL_HEADER_LABEL}>
<Checkbox

Check failure on line 21 in packages/react/src/components/Table/TableHeader.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Replace `⏎↹↹↹↹↹↹↹checked={isSelectedAll()}⏎↹↹↹↹↹↹↹labelClass="nds-sr-only"⏎↹↹↹↹↹↹↹onChange={onSelectedAll}⏎↹↹↹↹↹↹` with `·checked={isSelectedAll()}·labelClass="nds-sr-only"·onChange={onSelectedAll}`
id={uniqueId}
checked={isSelectedAll}
checked={isSelectedAll()}
labelClass="nds-sr-only"
onChange={onSelectedAll}
>
Expand Down
27 changes: 18 additions & 9 deletions packages/react/src/components/Table/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";

Check failure on line 1 in packages/react/src/components/Table/TableRow.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Replace `"react"` with `'react'`
import classNames from "classnames";

Check failure on line 2 in packages/react/src/components/Table/TableRow.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Replace `"classnames"` with `'classnames'`
import { TableRowProps } from "./types";

Check failure on line 3 in packages/react/src/components/Table/TableRow.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Replace `"./types"` with `'./types'`
import { useTableState } from './context';
Expand All @@ -11,24 +11,33 @@ export const TableRow: React.FC<TableRowProps> = ({
sectionHeaderClass = `${baseName}__header--section`,
isHeader,
isSectionHeader,
id,
children,
}) => {

Check failure on line 17 in packages/react/src/components/Table/TableRow.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Replace `⏎↹const·{·selectable,·onSelect,·onSelected,·isSelected,·isSelectedAll,·registerId·}·=·` with `↹const·{·selectable,·onSelect,·onSelected,·isSelected,·isSelectedAll,·registerId·}·=⏎↹↹`
const { selectable, onSelect, selected, onSelected, isSelectedAll } = useTableState();
const uniqueId = useId();
const { selectable, onSelect, onSelected, isSelected, isSelectedAll, registerId } = useTableState();
const uniqueId = id || useId() as string;
const isSelectable = selectable && onSelect && !isHeader && !isSectionHeader
const isChecked =isSelectedAll() || isSelected(uniqueId);

const trClassName = classNames(baseName, {
useEffect(() => {
if (!isSelectable)
return;
registerId(uniqueId);
}, []);

const onChange = () => onSelected?.(uniqueId, !isChecked);

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 ? (
<tr className={trClassName} id={uniqueId}>
{ isSelectable ? (
<td>
<Checkbox id={uniqueId} checked={isSelected} disabled={isSelectedAll} onChange={onSelected} />
<Checkbox checked={isChecked} onChange={onChange} />
</td>) : null }
{children}
</tr>
Expand Down
39 changes: 20 additions & 19 deletions packages/react/src/components/Table/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { TableState } from "./types";
import React, { useState, ChangeEvent } from "react";
import { Choices, TableState } from "./types";

export const TableContext = React.createContext<TableState | undefined>(
undefined
Expand All @@ -19,31 +19,32 @@ export function useInitTableState({
onSelect,
selected,
}: TableState): TableState {
const [isSelectedAll, setIsSelectedAll] = React.useState<boolean>(false);
const [uniqueIds, setUniqueIds] = useState<Set<string>>(
new Set(selected || [])
);
const [choices, setChoices] = useState<Choices>(selected || {});

const onSelectedAll = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsSelectedAll(event.target.checked || false);
onSelected(event);
const onSelectedAll = (event: ChangeEvent<HTMLInputElement>) => {
setChoices((old: Choices) => ({
...old,
...Object.fromEntries(Object.entries(old).map(([key]) => [key, event.target.checked])),
}));
};
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]);
const onSelected = (id: string, checked: boolean) => {
setChoices((old: Choices) => ({ ...old, [id]: checked, }));
onSelect?.(event);
};
const isSelected = (key:string):boolean => (choices && choices[key]);
const isSelectedAll = ():boolean => (choices && Object.values(choices).every(value => value === true));

const registerId = (key: string, value: boolean = false) => {
setChoices((old: Choices) => ({ ...old, [key]: value, }));
};
return {
selectable,
onSelect,
selected: [...uniqueIds],
selected: choices,
onSelected,
onSelectedAll,
isSelected,
isSelectedAll,
registerId,
};
}
20 changes: 6 additions & 14 deletions packages/react/src/components/Table/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, {useState} from "react";
import type { Meta, StoryObj } from "@storybook/react";
import {
Table,
Expand All @@ -18,13 +18,8 @@ type Story = StoryObj<typeof Table>;

// default table - presentation only
export const Default: Story = {
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}>
render: () => (
<Table selectable onSelect={() => {}}>
<TableHeader>
<TableHeadCell sortType="ascending" onSort={() => {}}>
Header 1
Expand All @@ -35,22 +30,19 @@ export const Default: Story = {
<TableHeadCell>Header 3</TableHeadCell>
</TableHeader>
<TableBody>
<TableRow>
<TableRow id='1'>
<td>Row 1, Cell 1</td>
<td>Row 1, Cell 2</td>
<td>Row 1, Cell 3</td>
</TableRow>
<TableRow>
<TableRow id='2'>
<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: 9 additions & 4 deletions packages/react/src/components/Table/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export interface TableState {
selectable?: boolean;
isSelectedAll?: boolean;
isSelected: (id:string) => boolean;
isSelectedAll: () => boolean;
onSelect?: (data?: any) => void;

Check warning on line 5 in packages/react/src/components/Table/types.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type
onSelected?: (event: React.ChangeEvent<HTMLInputElement>) => void;
registerId: (key: string, value?: boolean) => void;
onSelected?: (id: string, checked: boolean) => void;
onSelectedAll?: (event: React.ChangeEvent<HTMLInputElement>) => void;
selected?: string[];
selected: Choices;
}

export interface TableProps {
Expand All @@ -20,12 +22,13 @@ export interface TableRowProps {
sectionHeaderClass?: string;
isHeader?: boolean;
isSectionHeader?: boolean;
id?: string;
}

export interface TableHeadCellProps {
className?: string;
sortType?: 'none' | 'ascending' | 'descending' | 'other' | undefined;
onSort?: VoidFunction;
onSort?: VoidFunction |undefined;
'aria-label'?: string;
}

Expand All @@ -38,3 +41,5 @@ export interface TableBodyProps {
collapsedClass?: string;
isCollapsed?: boolean;
}

export type Choices = Record<string, boolean>;

0 comments on commit 064aa34

Please sign in to comment.