Skip to content

Commit

Permalink
fix(nhsuk-frontend-react): support for firstCellIsHeader in table
Browse files Browse the repository at this point in the history
  • Loading branch information
rowellx68 committed Sep 27, 2024
1 parent b007e86 commit b592be8
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { ReactNode, createContext, useContext } from 'react';

export type TableContextValue = {
variant?: 'default' | 'responsive';
firstCellIsHeader: boolean;
responsiveHeadings: ReactNode[];
registerHeadings: (heading: ReactNode[]) => void;
};

const TableContext = createContext<TableContextValue>({
responsiveHeadings: [],
firstCellIsHeader: false,
registerHeadings: () => {},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { composeStory } from '@storybook/react';
import meta, {
TwoColumn as TwoColumnStory,
ThreeColumn as ThreeColumnStory,
ThreeColumnWithFirstCellAsHeader as ThreeColumnWithFirstCellAsHeaderStory,
} from './Table.stories';

const TwoColumn = composeStory(TwoColumnStory, meta);
const ThreeColumn = composeStory(ThreeColumnStory, meta);
const ThreeColumnWithFirstCellAsHeader = composeStory(
ThreeColumnWithFirstCellAsHeaderStory,
meta,
);

it('should render a two column table', () => {
const { container } = render(<TwoColumn />);
Expand All @@ -30,3 +35,13 @@ it('should render a three column table', () => {
).toBeInTheDocument();
expect(container).toMatchSnapshot();
});

it('should render a three column table with the first cell as a header', () => {
const { container } = render(<ThreeColumnWithFirstCellAsHeader />);

expect(container.querySelector('tbody > tr > th')).toBeInTheDocument();
expect(
container.querySelector('table.nhsuk-table-responsive'),
).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Table } from './Table';
import { Column, Row } from '@/components/styles/layout/grid/Grid';
import { Container } from '@/components/styles/layout/container/Container';

/**
* Use a table to make it easier for users to compare and scan information.
Expand Down Expand Up @@ -95,3 +97,47 @@ export const ThreeColumn: Story = {
</Table>
),
};

export const ThreeColumnWithFirstCellAsHeader: Story = {
args: {
firstCellIsHeader: true,
variant: 'responsive',
},
render: (args) => (
<Container>
<Row>
<Column width="two-thirds">
<Table {...args}>
<Table.Caption>
Prescription prepayment certificate (PPC) charges
</Table.Caption>
<Table.Head>
<Table.Row>
<Table.Cell>Item</Table.Cell>
<Table.Cell variant="numeric">Current charge</Table.Cell>
<Table.Cell variant="numeric">New charge</Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
<Table.Row>
<Table.Cell>3-month</Table.Cell>
<Table.Cell variant="numeric">£31.25</Table.Cell>
<Table.Cell variant="numeric">£32.05</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>12-month</Table.Cell>
<Table.Cell variant="numeric">£111.60</Table.Cell>
<Table.Cell variant="numeric">£114.50</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>HRT</Table.Cell>
<Table.Cell variant="numeric">£19.30</Table.Cell>
<Table.Cell variant="numeric">£19.80</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
</Column>
</Row>
</Container>
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export type TableProps = {
* The variant of the table. Defaults to a non-responsive table.
*/
variant?: 'default' | 'responsive';
/**
* Whether the first cell in the table is a header cell. Defaults to false.
* @default false
*/
firstCellIsHeader?: boolean;
} & ElementProps<'table'>;

type TableFactory = Factory<{
Expand All @@ -40,30 +45,37 @@ type TableFactory = Factory<{
};
}>;

const Table = factory<TableFactory>(({ variant, className, ...props }, ref) => {
const [responsiveHeadings, registerHeadings] = useState<ReactNode[]>([]);
const Table = factory<TableFactory>(
({ variant, className, firstCellIsHeader = false, ...props }, ref) => {
const [responsiveHeadings, registerHeadings] = useState<ReactNode[]>([]);

const value = useMemo(
() => ({ variant, responsiveHeadings, registerHeadings }),
[variant, responsiveHeadings, registerHeadings],
);
const value = useMemo(
() => ({
variant,
responsiveHeadings,
firstCellIsHeader,
registerHeadings,
}),
[variant, responsiveHeadings, registerHeadings],
);

return (
<TableProvider value={value}>
<table
className={clsx(
{
'nhsuk-table': !variant,
[`nhsuk-table-${variant}`]: variant,
},
className,
)}
{...props}
ref={ref}
/>
</TableProvider>
);
});
return (
<TableProvider value={value}>
<table
className={clsx(
{
'nhsuk-table': !variant,
[`nhsuk-table-${variant}`]: variant,
},
className,
)}
{...props}
ref={ref}
/>
</TableProvider>
);
},
);

export type TableCaptionProps = ElementProps<'caption'>;

Expand Down Expand Up @@ -110,6 +122,7 @@ const TableRow = ({
const {
variant: tableVariant,
responsiveHeadings,
firstCellIsHeader,
registerHeadings,
} = useTableContext();

Expand Down Expand Up @@ -147,14 +160,27 @@ const TableRow = ({
});
}

if (variant === 'default' || !head) {
_children = Children.map(_children, (child, index) => {
if (isValidElement(child) && child.type === TableCell) {
return cloneElement(child as ReactElement<TableCellProps>, {
__firstCellIsHeader: index === 0 && firstCellIsHeader,
});
}
return child;
});
}

return (
<tr
className={clsx(
{
'nhsuk-table__row': variant === 'default' || !head,
},
className,
)}
className={
clsx(
{
'nhsuk-table__row': variant === 'default' || !head,
},
className,
) || undefined
}
role={role}
{...props}
>
Expand All @@ -176,6 +202,11 @@ export type TableCellProps = {
* For internal use only
*/
__responsiveHeading?: ReactNode;

/**
* For internal use only
*/
__firstCellIsHeader?: boolean;
} & ElementProps<'td'>;

const TableCell = ({
Expand All @@ -185,34 +216,46 @@ const TableCell = ({
children,
responsiveHeading,
__responsiveHeading,
__firstCellIsHeader,
...props
}: TableCellProps) => {
const { variant: tableVariant } = useTableContext();
const { head } = useTableHeadContext();

const baseProps = head
? {
as: 'th',
scope: 'col',
role: role || 'columnheader',
className: className,
'data-responsive-heading': responsiveHeading,
}
: {
as: 'td',
role: role || 'cell',
className: clsx(
'nhsuk-table__cell',
{ 'nhsuk-table__cell--numeric': variant === 'numeric' },
className,
),
};
const headerCellClassNames = clsx(
{
'nhsuk-table__header': __firstCellIsHeader,
'nhsuk-table__header--numeric': variant === 'numeric',
},
className,
);

const cellClassNames = clsx(
'nhsuk-table__cell',
{
'nhsuk-table__cell--numeric': variant === 'numeric',
},
className,
);

const baseProps =
head || __firstCellIsHeader
? {
as: 'th',
scope: __firstCellIsHeader ? 'row' : 'col',
role: __firstCellIsHeader ? role : role || 'columnheader',
...(headerCellClassNames && { className: headerCellClassNames }),
}
: {
as: 'td',
...(cellClassNames && { className: cellClassNames }),
};

return (
<Base<any> {...baseProps} {...props}>
{tableVariant === 'responsive' && !head && (
<span className="nhsuk-table-responsive__heading" aria-hidden="true">
{__responsiveHeading}&nbsp;
{responsiveHeading || __responsiveHeading}&nbsp;
</span>
)}
{children}
Expand Down
Loading

0 comments on commit b592be8

Please sign in to comment.