Skip to content

Commit 3301620

Browse files
authored
Merge pull request #98 from NHSDigital/feature/v4-responsive-table
[NHSUK 4] - Responsive Table
2 parents 5511f75 + 6b1a69c commit 3301620

37 files changed

+945
-313
lines changed

src/components/button/Button.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ interface ButtonLinkProps extends HTMLProps<HTMLAnchorElement> {
1818

1919
export const Button: React.FC<ButtonProps> = ({
2020
className,
21-
type,
2221
disabled,
2322
secondary,
2423
reverse,
@@ -35,7 +34,6 @@ export const Button: React.FC<ButtonProps> = ({
3534
)}
3635
disabled={disabled}
3736
aria-disabled={disabled ? 'true' : 'false'}
38-
type={type}
3937
{...rest}
4038
/>
4139
);

src/components/checkboxes/Checkboxes.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,13 @@ class Checkboxes extends PureComponent<CheckboxesProps, CheckboxesState> {
8181
const { children, ...rest } = this.props;
8282
return (
8383
<FormGroup<CheckboxesProps> inputType="checkboxes" {...rest}>
84-
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
8584
{({
86-
className, name, id, idPrefix, ...restRenderProps
85+
className,
86+
name,
87+
id,
88+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
89+
idPrefix,
90+
...restRenderProps
8791
}) => {
8892
this.resetBoxIds();
8993
const containsConditional = this.state.conditionalBoxes.length > 0;

src/components/date-input/DateInput.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,19 @@ class DateInput extends PureComponent<DateInputProps, DateInputState> {
118118
};
119119

120120
render(): JSX.Element {
121-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
122121
const {
123-
children, onChange, value, defaultValue, ...rest
122+
children,
123+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
124+
onChange,
125+
value,
126+
defaultValue,
127+
...rest
124128
} = this.props;
125129

126130
return (
127131
<FormGroup<Omit<DateInputProps, 'value' | 'defaultValue'>> inputType="dateinput" {...rest}>
128132
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
129-
{({
130-
className, name, id, error, autoSelectNext, ...restRenderProps
131-
}) => {
133+
{({ className, name, id, error, autoSelectNext, ...restRenderProps }) => {
132134
const contextValue: IDateInputContext = {
133135
id,
134136
name,

src/components/fieldset/Fieldset.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ interface FieldsetProps extends HTMLProps<HTMLFieldSetElement> {
5050
type FieldsetState = { registeredComponents: Array<string>; erroredComponents: Array<string> };
5151

5252
class Fieldset extends PureComponent<FieldsetProps, FieldsetState> {
53+
static Legend = Legend;
54+
5355
constructor(props: FieldsetProps) {
5456
super(props);
5557
this.state = {
@@ -92,8 +94,6 @@ class Fieldset extends PureComponent<FieldsetProps, FieldsetState> {
9294
});
9395
};
9496

95-
static Legend = Legend;
96-
9797
render(): JSX.Element {
9898
const { className, disableErrorLine, ...rest } = this.props;
9999
const contextValue: IFieldsetContext = {

src/components/radios/Radios.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class Radios extends PureComponent<RadiosProps, RadiosState> {
2424

2525
private radioIds: Record<string, string> = {};
2626

27+
static Divider = Divider;
28+
29+
static Radio = Radio;
30+
2731
static defaultProps = {
2832
role: 'radiogroup',
2933
};
@@ -89,18 +93,12 @@ class Radios extends PureComponent<RadiosProps, RadiosState> {
8993
this.radioIds = {};
9094
};
9195

92-
static Divider = Divider;
93-
94-
static Radio = Radio;
95-
9696
render(): JSX.Element {
9797
const { children, ...rest } = this.props;
9898
return (
9999
<FormGroup<RadiosProps> inputType="radios" {...rest}>
100100
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
101-
{({
102-
className, inline, name, id, error, ...restRenderProps
103-
}) => {
101+
{({ className, inline, name, id, error, ...restRenderProps }) => {
104102
this.resetRadioIds();
105103
const contextValue: IRadiosContext = {
106104
getRadioId: (reference) => this.getRadioId(id, reference),

src/components/skip-link/SkipLink.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,13 @@ class SkipLink extends React.Component<SkipLinkProps> {
8585
};
8686

8787
render(): JSX.Element {
88-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8988
const {
90-
className, focusTargetRef, disableDefaultBehaviour, href, ...rest
89+
className,
90+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
91+
focusTargetRef,
92+
disableDefaultBehaviour,
93+
href,
94+
...rest
9195
} = this.props;
9296
return (
9397
<a

src/components/table/Table.tsx

Lines changed: 67 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,82 @@
1-
import React, { HTMLProps, createContext, useContext } from 'react';
1+
import React, { ComponentProps, HTMLProps, ReactNode } from 'react';
22
import classNames from 'classnames';
3-
import HeadingLevel, { HeadingLevelType } from '../../util/HeadingLevel';
4-
import isDev from '../../util/IsDev';
3+
import TableBody from './components/TableBody';
4+
import TableCaption from './components/TableCaption';
5+
import TableCell from './components/TableCell';
6+
import TableContainer from './components/TableContainer';
7+
import TableHead from './components/TableHead';
8+
import TableRow from './components/TableRow';
9+
import TablePanel from './components/TablePanel';
10+
import TableContext, { ITableContext } from './TableContext';
511

6-
enum TableSectionTypes {
7-
HEAD = 'HEADER',
8-
BODY = 'BODY',
12+
interface TableProps extends HTMLProps<HTMLTableElement> {
13+
responsive?: boolean;
14+
caption?: ReactNode;
15+
captionProps?: ComponentProps<typeof TableCaption>;
916
}
1017

11-
const TableSectionContext = createContext<TableSectionTypes>(TableSectionTypes.HEAD);
12-
13-
const TableHead: React.FC<HTMLProps<HTMLTableSectionElement>> = ({
14-
className,
15-
children,
16-
...rest
17-
}) => (
18-
<thead className={classNames('nhsuk-table__head', className)} {...rest}>
19-
<TableSectionContext.Provider value={TableSectionTypes.HEAD}>
20-
{children}
21-
</TableSectionContext.Provider>
22-
</thead>
23-
);
24-
25-
const TableBody: React.FC<HTMLProps<HTMLTableSectionElement>> = ({
26-
className,
27-
children,
28-
...rest
29-
}) => (
30-
<tbody className={classNames('nhsuk-table__body', className)} {...rest}>
31-
<TableSectionContext.Provider value={TableSectionTypes.BODY}>
32-
{children}
33-
</TableSectionContext.Provider>
34-
</tbody>
35-
);
36-
37-
const TableRow: React.FC<HTMLProps<HTMLTableRowElement>> = ({ className, ...rest }) => (
38-
<tr className={classNames('nhsuk-table__row', className)} {...rest} />
39-
);
40-
41-
interface TableCellProps extends HTMLProps<HTMLTableCellElement> {
42-
header?: boolean | undefined;
18+
interface TableState {
19+
headings: string[];
4320
}
4421

45-
const TableCell: React.FC<TableCellProps> = ({ className, header, ...rest }) => {
46-
const sectionType = useContext(TableSectionContext);
47-
if (header !== undefined) {
48-
if (header === true) {
49-
return <th className={classNames('nhsuk-table__header', className)} scope="col" {...rest} />;
50-
}
51-
return <td className={classNames('nhsuk-table__cell', className)} {...rest} />;
52-
}
53-
if (sectionType === TableSectionTypes.HEAD) {
54-
return <th className={classNames('nhsuk-table__header', className)} scope="col" {...rest} />;
55-
}
56-
if (sectionType === TableSectionTypes.BODY) {
57-
return <td className={classNames('nhsuk-table__cell', className)} {...rest} />;
22+
class Table extends React.PureComponent<TableProps, TableState> {
23+
static defaultProps = {
24+
responsive: false,
25+
};
26+
27+
static Container = TableContainer;
28+
29+
static Head = TableHead;
30+
31+
static Row = TableRow;
32+
33+
static Cell = TableCell;
34+
35+
static Body = TableBody;
36+
37+
static Panel = TablePanel;
38+
39+
constructor(props: TableProps) {
40+
super(props);
41+
this.state = {
42+
headings: [],
43+
};
5844
}
59-
if (isDev()) {
60-
console.warn(
61-
'TableCell used outside of TableHead or TableBody elements. Unable to determine section type from context.',
45+
46+
setHeadings = (headings: string[]): void => {
47+
const isEqual = headings.reduce(
48+
(prevValue, heading, index) => prevValue && heading === this.state.headings[index],
49+
true,
6250
);
63-
}
64-
return <td className={classNames('nhsuk-table__cell', className)} {...rest} />;
65-
};
6651

67-
interface TablePanelProps extends HTMLProps<HTMLDivElement> {
68-
heading?: string;
69-
headingProps?: HTMLProps<HTMLHeadingElement>;
70-
headingLevel?: HeadingLevelType;
71-
}
52+
if (!isEqual) this.setState({ headings });
53+
};
7254

73-
const TablePanel: React.FC<TablePanelProps> = ({
74-
className,
75-
children,
76-
heading,
77-
headingLevel,
78-
headingProps,
79-
...rest
80-
}) => (
81-
<div className={classNames('nhsuk-table__panel-with-heading-tab', className)} {...rest}>
82-
{heading ? (
83-
<HeadingLevel
84-
className="nhsuk-table__heading-tab"
85-
{...headingProps}
86-
headingLevel={headingLevel}
87-
>
88-
{heading}
89-
</HeadingLevel>
90-
) : null}
91-
{children}
92-
</div>
93-
);
94-
95-
TablePanel.defaultProps = {
96-
headingLevel: 'h3',
97-
};
55+
render(): JSX.Element {
56+
const { className, responsive, children, caption, captionProps, ...rest } = this.props;
9857

99-
interface TableProps extends HTMLProps<HTMLTableElement> {
100-
caption?: string;
101-
}
58+
const contextValue: ITableContext = {
59+
isResponsive: Boolean(responsive),
60+
headings: this.state.headings,
61+
setHeadings: this.setHeadings,
62+
};
10263

103-
interface Table extends React.FC<TableProps> {
104-
Body: React.FC<HTMLProps<HTMLTableSectionElement>>;
105-
Head: React.FC<HTMLProps<HTMLTableSectionElement>>;
106-
Row: React.FC<HTMLProps<HTMLTableRowElement>>;
107-
Cell: React.FC<TableCellProps>;
108-
Panel: React.FC<TablePanelProps>;
64+
return (
65+
<TableContext.Provider value={contextValue}>
66+
<table
67+
className={classNames(
68+
{ 'nhsuk-table': !responsive },
69+
{ 'nhsuk-table-responsive': responsive },
70+
className,
71+
)}
72+
{...rest}
73+
>
74+
{caption && <TableCaption {...captionProps}>{caption}</TableCaption>}
75+
{children}
76+
</table>
77+
</TableContext.Provider>
78+
);
79+
}
10980
}
11081

111-
const Table: Table = ({
112-
className, caption, children, ...rest
113-
}) => (
114-
<div className="nhsuk-table-responsive">
115-
<table className={classNames('nhsuk-table', className)} {...rest}>
116-
{caption ? <caption className="nhsuk-table__caption">{caption}</caption> : null}
117-
{children}
118-
</table>
119-
</div>
120-
);
121-
122-
Table.Body = TableBody;
123-
Table.Head = TableHead;
124-
Table.Row = TableRow;
125-
Table.Cell = TableCell;
126-
Table.Panel = TablePanel;
127-
12882
export default Table;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createContext } from 'react';
2+
3+
export interface ITableContext {
4+
isResponsive: boolean;
5+
headings: string[];
6+
setHeadings(headings: string[]): void;
7+
}
8+
9+
const TableContext = createContext<ITableContext>({
10+
/* eslint-disable @typescript-eslint/no-empty-function */
11+
isResponsive: false,
12+
headings: [],
13+
setHeadings: () => {},
14+
});
15+
16+
export default TableContext;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { isValidElement, ReactElement, ReactNode } from 'react';
2+
import TableCell from './components/TableCell';
3+
4+
export const isTableCell = (child: ReactNode): child is ReactElement => {
5+
return isValidElement(child) && child.type === TableCell;
6+
};
7+
8+
export const getHeadingsFromChildren = (children: ReactNode): string[] => {
9+
return React.Children
10+
.map(children, child => {
11+
if (isTableCell(child)) {
12+
return child.props.children.toString();
13+
}
14+
return null;
15+
})
16+
.filter(Boolean);
17+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createContext } from 'react';
2+
3+
export enum TableSection {
4+
NONE,
5+
HEAD,
6+
BODY,
7+
}
8+
9+
const TableSectionContext = createContext<TableSection>(TableSection.NONE);
10+
11+
export default TableSectionContext;

0 commit comments

Comments
 (0)