Skip to content

Commit b3a23fc

Browse files
Align table with NHS.UK frontend
1 parent 12ec82f commit b3a23fc

File tree

12 files changed

+677
-88
lines changed

12 files changed

+677
-88
lines changed

docs/upgrade-to-6.0.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,36 @@ For accessibility reasons, you must make the following changes:
402402
- remove the `disableHeadingFocus` prop
403403
- remove custom `onClick` handlers
404404
405+
### Tables
406+
407+
To align with NHS.UK frontend, you must make the following changes:
408+
409+
- rename the `Table` prop `isResponsive` to `responsive`
410+
- rename the `Table.Cell` prop `isNumeric` to `format="numeric"`
411+
- remove unnecessary role attributes from tables, sections, rows and cells
412+
413+
```patch
414+
- <Table caption="Number of cases" isResponsive>
415+
+ <Table caption="Number of cases" responsive>
416+
- <Table.Head role="rowgroup">
417+
+ <Table.Head>
418+
- <Table.Row role="row">
419+
+ <Table.Row>
420+
<Table.Cell>Location</Table.Cell>
421+
<Table.Cell>Number of cases</Table.Cell>
422+
</Table.Row>
423+
</Table.Head>
424+
<Table.Body>
425+
- <Table.Row role="row">
426+
+ <Table.Row>
427+
<Table.Cell>England</Table.Cell>
428+
- <Table.Cell isNumeric>4,000</Table.Cell>
429+
+ <Table.Cell format="numeric">4,000</Table.Cell>
430+
</Table.Row>
431+
</Table.Body>
432+
</Table>
433+
```
434+
405435
### Textarea
406436
407437
You must rename the `Textarea` prop `textareaRef` to `ref` for consistency with other components:

src/components/content-presentation/table/Table.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,30 @@ import TablePanel from './components/TablePanel';
1010
import TableContext, { ITableContext } from './TableContext';
1111

1212
export interface TableProps extends ComponentPropsWithoutRef<'table'> {
13+
firstCellIsHeader?: boolean;
1314
responsive?: boolean;
1415
caption?: ReactNode;
1516
captionProps?: TableCaptionProps;
1617
}
1718

1819
const TableComponent = forwardRef<HTMLTableElement, TableProps>((props, forwardedRef) => {
19-
const { caption, captionProps, children, className, responsive = false, ...rest } = props;
20+
const {
21+
caption,
22+
captionProps,
23+
children,
24+
className,
25+
firstCellIsHeader = false,
26+
responsive = false,
27+
...rest
28+
} = props;
2029

21-
const [headings, setHeadings] = useState<string[]>([]);
30+
const [headings, setHeadings] = useState<ReactNode[]>([]);
2231

2332
const contextValue: ITableContext = useMemo(() => {
2433
return {
25-
isResponsive: Boolean(responsive),
34+
firstCellIsHeader,
2635
headings,
36+
responsive,
2737
setHeadings,
2838
};
2939
}, [responsive, headings, setHeadings]);
@@ -36,6 +46,7 @@ const TableComponent = forwardRef<HTMLTableElement, TableProps>((props, forwarde
3646
{ 'nhsuk-table-responsive': responsive },
3747
className,
3848
)}
49+
role={responsive ? 'table' : undefined}
3950
ref={forwardedRef}
4051
{...rest}
4152
>

src/components/content-presentation/table/TableContext.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { createContext } from 'react';
1+
import { ReactNode, createContext } from 'react';
22

33
export interface ITableContext {
4-
isResponsive: boolean;
5-
headings: string[];
6-
setHeadings(headings: string[]): void;
4+
firstCellIsHeader: boolean;
5+
headings: ReactNode[];
6+
responsive: boolean;
7+
setHeadings(headings: ReactNode[]): void;
78
}
89

910
const TableContext = createContext<ITableContext>({
1011
/* eslint-disable @typescript-eslint/no-empty-function */
11-
isResponsive: false,
12+
firstCellIsHeader: false,
1213
headings: [],
14+
responsive: false,
1315
setHeadings: () => {},
1416
});
1517

src/components/content-presentation/table/TableHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const getHeadingsFromChildren = (children: ReactNode): string[] => {
99
const headings: string[] = [];
1010
Children.map(children, (child) => {
1111
if (isTableCell(child)) {
12-
headings.push(child.props.children.toString());
12+
headings.push(child.props.children);
1313
}
1414
});
1515
return headings;

src/components/content-presentation/table/components/TableCell.tsx

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import classNames from 'classnames';
22
import React, { ComponentPropsWithoutRef, FC, useContext } from 'react';
33
import useDevWarning from '@util/hooks/UseDevWarning';
4+
import TableContext, { ITableContext } from '../TableContext';
45
import TableSectionContext, { TableSection } from '../TableSectionContext';
56

67
const CellOutsideOfSectionWarning =
@@ -9,36 +10,47 @@ const CellOutsideOfSectionWarning =
910
export interface TableCellProps
1011
extends ComponentPropsWithoutRef<'th'>,
1112
ComponentPropsWithoutRef<'td'> {
12-
_responsive?: boolean;
13-
_responsiveHeading?: string;
14-
isNumeric?: boolean;
13+
index?: number;
14+
format?: 'numeric';
1515
}
1616

17-
const TableCell: FC<TableCellProps> = ({
18-
className,
19-
_responsive = false,
20-
_responsiveHeading = '',
21-
isNumeric,
22-
children,
23-
...rest
24-
}) => {
25-
const section = useContext(TableSectionContext);
17+
const TableCell: FC<TableCellProps> = ({ className, format, children, index = -1, ...rest }) => {
18+
const { firstCellIsHeader, headings, responsive } = useContext<ITableContext>(TableContext);
19+
const section = useContext<TableSection>(TableSectionContext);
20+
2621
useDevWarning(CellOutsideOfSectionWarning, () => section === TableSection.NONE);
2722

28-
const cellClass = section === TableSection.HEAD ? 'nhsuk-table__header' : 'nhsuk-table__cell';
29-
const classes = classNames(cellClass, { [`${cellClass}--numeric`]: isNumeric }, className);
23+
const isColHeader = section === TableSection.HEAD;
24+
const isRowHeader = section === TableSection.BODY && firstCellIsHeader && index === 0;
3025

3126
return (
3227
<>
33-
{section === TableSection.HEAD ? (
34-
<th className={classes} scope="col" {...rest}>
28+
{isColHeader || isRowHeader ? (
29+
<th
30+
className={classNames(
31+
'nhsuk-table__header',
32+
{ [`nhsuk-table__header--${format}`]: !!format && !isRowHeader },
33+
className,
34+
)}
35+
scope={isRowHeader ? 'row' : 'col'}
36+
role={isRowHeader ? 'rowheader' : responsive ? 'columnheader' : undefined}
37+
{...rest}
38+
>
3539
{children}
3640
</th>
3741
) : (
38-
<td className={classes} role={_responsive ? 'cell' : undefined} {...rest}>
39-
{_responsive && (
42+
<td
43+
className={classNames(
44+
'nhsuk-table__cell',
45+
{ [`nhsuk-table__cell--${format}`]: !!format },
46+
className,
47+
)}
48+
role={responsive ? 'cell' : undefined}
49+
{...rest}
50+
>
51+
{responsive && !!headings[index] && (
4052
<span className="nhsuk-table-responsive__heading" aria-hidden>
41-
{_responsiveHeading}
53+
{headings[index]}{' '}
4254
</span>
4355
)}
4456
{children}

src/components/content-presentation/table/components/TableHead.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import React, { ComponentPropsWithoutRef, FC } from 'react';
1+
import React, { ComponentPropsWithoutRef, FC, useContext } from 'react';
22
import classNames from 'classnames';
3+
import TableContext, { ITableContext } from '../TableContext';
34
import TableSectionContext, { TableSection } from '../TableSectionContext';
45

5-
const TableHead: FC<ComponentPropsWithoutRef<'thead'>> = ({ children, className, ...rest }) => (
6-
<thead className={classNames('nhsuk-table__head', className)} {...rest}>
7-
<TableSectionContext.Provider value={TableSection.HEAD}>
8-
{children}
9-
</TableSectionContext.Provider>
10-
</thead>
11-
);
6+
const TableHead: FC<ComponentPropsWithoutRef<'thead'>> = ({ children, className, ...rest }) => {
7+
const { responsive } = useContext<ITableContext>(TableContext);
8+
9+
return (
10+
<thead
11+
className={classNames('nhsuk-table__head', className)}
12+
role={responsive ? 'rowgroup' : undefined}
13+
{...rest}
14+
>
15+
<TableSectionContext.Provider value={TableSection.HEAD}>
16+
{children}
17+
</TableSectionContext.Provider>
18+
</thead>
19+
);
20+
};
1221

1322
TableHead.displayName = 'Table.Head';
1423

src/components/content-presentation/table/components/TableRow.tsx

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,27 @@ import TableSectionContext, { TableSection } from '../TableSectionContext';
1313

1414
const TableRow: FC<ComponentPropsWithoutRef<'tr'>> = ({ children, className, ...rest }) => {
1515
const section = useContext(TableSectionContext);
16-
const { isResponsive, headings, setHeadings } = useContext(TableContext);
16+
const { responsive, setHeadings } = useContext(TableContext);
1717

1818
useEffect(() => {
19-
if (isResponsive && section === TableSection.HEAD) {
19+
if (responsive && section === TableSection.HEAD) {
2020
setHeadings(getHeadingsFromChildren(children));
2121
}
22-
}, [isResponsive, section, children]);
22+
}, [responsive, section, children]);
2323

24-
if (isResponsive && section === TableSection.BODY) {
25-
const tableCells = Children.map(children, (child, index) => {
26-
if (isTableCell(child)) {
27-
return cloneElement(child, {
28-
_responsive: isResponsive,
29-
_responsiveHeading: `${headings[index] || ''} `,
30-
});
31-
}
32-
return child;
33-
});
34-
35-
return (
36-
<tr className={classNames('nhsuk-table__row', className)} {...rest}>
37-
{tableCells}
38-
</tr>
39-
);
40-
}
24+
const tableCells = Children.map(children, (child, index) => {
25+
return section === TableSection.BODY && isTableCell(child)
26+
? cloneElement(child, { index })
27+
: child;
28+
});
4129

4230
return (
43-
<tr className={classNames('nhsuk-table__row', className)} {...rest}>
44-
{children}
31+
<tr
32+
className={classNames('nhsuk-table__row', className)}
33+
role={responsive ? 'row' : undefined}
34+
{...rest}
35+
>
36+
{tableCells}
4537
</tr>
4638
);
4739
};

src/components/content-presentation/table/components/__tests__/Table.test.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,57 @@ import { renderClient, renderServer } from '@util/components';
33
import Table from '../..';
44

55
describe('Table', () => {
6+
const Example = (props: Parameters<typeof Table>[0]) => (
7+
<Table caption="Skin symptoms and possible causes" {...props}>
8+
<Table.Head>
9+
<Table.Row>
10+
<Table.Cell>Skin Symptoms</Table.Cell>
11+
<Table.Cell>Possible cause</Table.Cell>
12+
</Table.Row>
13+
</Table.Head>
14+
<Table.Body>
15+
<Table.Row>
16+
<Table.Cell>Blisters on lips or around the mouth</Table.Cell>
17+
<Table.Cell>cold sores</Table.Cell>
18+
</Table.Row>
19+
<Table.Row>
20+
<Table.Cell>Itchy, dry, cracked, sore</Table.Cell>
21+
<Table.Cell>eczema</Table.Cell>
22+
</Table.Row>
23+
<Table.Row>
24+
<Table.Cell>Itchy blisters</Table.Cell>
25+
<Table.Cell>shingles, chickenpox</Table.Cell>
26+
</Table.Row>
27+
</Table.Body>
28+
</Table>
29+
);
30+
631
it('matches snapshot', async () => {
7-
const { container } = await renderClient(<Table />, {
32+
const { container } = await renderClient(<Example />, {
33+
className: 'nhsuk-table',
34+
});
35+
36+
expect(container).toMatchSnapshot();
37+
});
38+
39+
it('matches snapshot when responsive', async () => {
40+
const { container } = await renderClient(<Example responsive />, {
41+
className: 'nhsuk-table-responsive',
42+
});
43+
44+
expect(container).toMatchSnapshot();
45+
});
46+
47+
it('matches snapshot when first cell is header', async () => {
48+
const { container } = await renderClient(<Example firstCellIsHeader />, {
849
className: 'nhsuk-table',
950
});
1051

1152
expect(container).toMatchSnapshot();
1253
});
1354

1455
it('matches snapshot (via server)', async () => {
15-
const { container, element } = await renderServer(<Table />, {
56+
const { container, element } = await renderServer(<Example />, {
1657
className: 'nhsuk-table',
1758
});
1859

src/components/content-presentation/table/components/__tests__/TableCell.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('Table.Cell', () => {
6868
expect(cellWrapper).toBeTruthy();
6969
});
7070

71-
it('adds responsive heading when _responsive=True', () => {
71+
it('adds responsive heading when responsive', () => {
7272
const { container } = render(
7373
<Table responsive>
7474
<Table.Head>
@@ -90,12 +90,12 @@ describe('Table.Cell', () => {
9090
expect(spanWrapper?.textContent).toBe('TestHeading ');
9191
});
9292

93-
it('adds the numeric class when isNumeric is true', () => {
93+
it('adds the numeric class when `format: numeric` is set', () => {
9494
const { container } = render(
9595
<table>
9696
<tbody>
9797
<tr>
98-
<Table.Cell data-test="cell" isNumeric />
98+
<Table.Cell data-test="cell" format="numeric" />
9999
</tr>
100100
</tbody>
101101
</table>,
@@ -105,12 +105,12 @@ describe('Table.Cell', () => {
105105
expect(cell).toBeTruthy();
106106
});
107107

108-
it('adds the numeric header class when isNumeric is true', () => {
108+
it('adds the numeric header class when `format: numeric` is set', () => {
109109
const { container } = render(
110110
<table>
111111
<Table.Head>
112112
<tr>
113-
<Table.Cell data-test="cell" isNumeric />
113+
<Table.Cell data-test="cell" format="numeric" />
114114
</tr>
115115
</Table.Head>
116116
</table>,
@@ -120,7 +120,7 @@ describe('Table.Cell', () => {
120120
expect(cell).toBeTruthy();
121121
});
122122

123-
it('does not add the numeric header when isNumeric is false', () => {
123+
it('does not add the numeric header when `format: numeric` is omitted', () => {
124124
const { container } = render(
125125
<table>
126126
<Table.Head>

0 commit comments

Comments
 (0)