Skip to content
Merged
30 changes: 30 additions & 0 deletions docs/upgrade-to-6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ The [panel](https://service-manual.nhs.uk/design-system/components/panel) compon

This replaces the [list panel component](#list-panel) which was removed in NHS.UK frontend v6.0.0.

### Summary list rows and actions

The [summary list](https://service-manual.nhs.uk/design-system/components/summary-list) component now includes improvements from NHS.UK frontend v9.6.2:

- new props `noBorder` and `noActions` supported at `<SummaryList.Row>` level
- new child component `<SummaryList.Action>` for row actions

```patch
<SummaryList>
- <SummaryList.Row>
+ <SummaryList.Row noBorder>
<SummaryList.Key>Name</SummaryList.Key>
<SummaryList.Value>Karen Francis</SummaryList.Value>
<SummaryList.Actions>
- <a href="#">
- Change<span className="nhsuk-u-visually-hidden"> name</span>
- </a>
+ <SummaryList.Action href="#" visuallyHiddenText="name">
+ Change
+ </SummaryList.Action>
</SummaryList.Actions>
</SummaryList.Row>
- <SummaryList.Row>
+ <SummaryList.Row noActions>
<SummaryList.Key>Date of birth</SummaryList.Key>
<SummaryList.Value>15 March 1984</SummaryList.Value>
</SummaryList.Row>
</SummaryList>
```

### Support for React Server Components (RSC)

All components have been tested as React Server Components (RSC) but due to [multipart namespace component limitations](https://ivicabatinic.from.hr/posts/multipart-namespace-components-addressing-rsc-and-dot-notation-issues) an alternative syntax (without dot notation) can be used as a workaround:
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ describe('Index', () => {
'SelectOption',
'SkipLink',
'SummaryList',
'SummaryListAction',
'SummaryListActions',
'SummaryListKey',
'SummaryListRow',
Expand Down
29 changes: 9 additions & 20 deletions src/components/content-presentation/summary-list/SummaryList.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import classNames from 'classnames';
import { forwardRef, type ComponentPropsWithoutRef, type FC } from 'react';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';

export const SummaryListRow: FC<ComponentPropsWithoutRef<'div'>> = ({ className, ...rest }) => (
<div className={classNames('nhsuk-summary-list__row', className)} {...rest} />
);

export const SummaryListKey: FC<ComponentPropsWithoutRef<'dt'>> = ({ className, ...rest }) => (
<dt className={classNames('nhsuk-summary-list__key', className)} {...rest} />
);

export const SummaryListValue: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
<dd className={classNames('nhsuk-summary-list__value', className)} {...rest} />
);

export const SummaryListActions: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
<dd className={classNames('nhsuk-summary-list__actions', className)} {...rest} />
);
import {
SummaryListAction,
SummaryListActions,
SummaryListKey,
SummaryListRow,
SummaryListValue,
} from './components/index.js';

export interface SummaryListProps extends ComponentPropsWithoutRef<'dl'> {
noBorder?: boolean;
Expand All @@ -36,14 +28,11 @@ const SummaryListComponent = forwardRef<HTMLDListElement, SummaryListProps>(
);

SummaryListComponent.displayName = 'SummaryList';
SummaryListRow.displayName = 'SummaryList.Row';
SummaryListKey.displayName = 'SummaryList.Key';
SummaryListValue.displayName = 'SummaryList.Value';
SummaryListActions.displayName = 'SummaryList.Actions';

export const SummaryList = Object.assign(SummaryListComponent, {
Row: SummaryListRow,
Key: SummaryListKey,
Value: SummaryListValue,
Action: SummaryListAction,
Actions: SummaryListActions,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render } from '@testing-library/react';
import { createRef } from 'react';
import { createRef, type ComponentProps } from 'react';

import { SummaryList } from '..';

Expand All @@ -10,6 +10,12 @@ describe('SummaryList', () => {
expect(container).toMatchSnapshot('SummaryList');
});

it('matches snapshot without border', () => {
const { container } = render(<SummaryList noBorder />);

expect(container).toMatchSnapshot();
});

it('forwards refs', () => {
const ref = createRef<HTMLDListElement>();

Expand All @@ -31,35 +37,98 @@ describe('SummaryList', () => {
it('matches snapshot', () => {
const { container } = render(<SummaryList.Row>Row</SummaryList.Row>);

expect(container.textContent).toBe('Row');
expect(container).toHaveTextContent('Row');
expect(container).toMatchSnapshot();
});

it('matches snapshot without border', () => {
const { container } = render(<SummaryList.Row noBorder>Row</SummaryList.Row>);

expect(container).toHaveTextContent('Row');
expect(container).toMatchSnapshot();
});
});

describe('SummaryList.Key', () => {
it('matches snapshot', () => {
const { container } = render(<SummaryList.Key>Key</SummaryList.Key>);
const { container } = render(<SummaryList.Key>Example key</SummaryList.Key>);

expect(container.textContent).toBe('Key');
expect(container).toHaveTextContent('Example key');
expect(container).toMatchSnapshot();
});
});

describe('SummaryList.Value', () => {
it('matches snapshot', () => {
const { container } = render(<SummaryList.Value>Value</SummaryList.Value>);
const { container } = render(<SummaryList.Value>Example value</SummaryList.Value>);

expect(container.textContent).toBe('Value');
expect(container).toHaveTextContent('Example value');
expect(container).toMatchSnapshot();
});
});

describe('SummaryList.Actions', () => {
it('matches snapshot', () => {
const { container } = render(<SummaryList.Actions>Actions</SummaryList.Actions>);
const { container } = render(
<SummaryList.Actions>
<SummaryList.Action href="#" visuallyHiddenText="example key">
Edit
</SummaryList.Action>
<SummaryList.Action href="#" visuallyHiddenText="example key">
Delete
</SummaryList.Action>
</SummaryList.Actions>,
);

expect(container).toMatchSnapshot();
});
});

describe('SummaryList.Action', () => {
it('matches snapshot', () => {
const { container } = render(
<SummaryList.Action href="#" visuallyHiddenText="example key">
Edit
</SummaryList.Action>,
);

expect(container.textContent).toBe('Actions');
expect(container).toHaveTextContent('Edit example key');
expect(container).toMatchSnapshot();
});

it('renders as custom element', () => {
function CustomLink({ children, href, ...rest }: ComponentProps<'a'>) {
return (
<a href={href} {...rest} data-custom-link="true">
{children}
</a>
);
}

const { container } = render(
<SummaryList.Action href="#" visuallyHiddenText="example key" asElement={CustomLink}>
Edit
</SummaryList.Action>,
);

const rowActionEl = container.querySelector('a');

expect(rowActionEl?.dataset).toHaveProperty('customLink', 'true');
});

it('forwards refs', () => {
const ref = createRef<HTMLAnchorElement>();

const { container } = render(
<SummaryList.Action href="#" visuallyHiddenText="example key" ref={ref}>
Edit
</SummaryList.Action>,
);

const rowActionEl = container.querySelector('a');

expect(ref.current).toBe(rowActionEl);
expect(ref.current).toHaveAttribute('href', '#');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`SummaryList SummaryList.Action matches snapshot 1`] = `
<div>
<a
href="#"
>
Edit
<span
class="nhsuk-u-visually-hidden"
>

example key
</span>
</a>
</div>
`;

exports[`SummaryList SummaryList.Actions matches snapshot 1`] = `
<div>
<dd
class="nhsuk-summary-list__actions"
>
Actions
<a
href="#"
>
Edit
<span
class="nhsuk-u-visually-hidden"
>

example key
</span>
</a>
<a
href="#"
>
Delete
<span
class="nhsuk-u-visually-hidden"
>

example key
</span>
</a>
</dd>
</div>
`;
Expand All @@ -15,7 +52,7 @@ exports[`SummaryList SummaryList.Key matches snapshot 1`] = `
<dt
class="nhsuk-summary-list__key"
>
Key
Example key
</dt>
</div>
`;
Expand All @@ -30,16 +67,34 @@ exports[`SummaryList SummaryList.Row matches snapshot 1`] = `
</div>
`;

exports[`SummaryList SummaryList.Row matches snapshot without border 1`] = `
<div>
<div
class="nhsuk-summary-list__row nhsuk-summary-list__row--no-border"
>
Row
</div>
</div>
`;

exports[`SummaryList SummaryList.Value matches snapshot 1`] = `
<div>
<dd
class="nhsuk-summary-list__value"
>
Value
Example value
</dd>
</div>
`;

exports[`SummaryList matches snapshot without border 1`] = `
<div>
<dl
class="nhsuk-summary-list nhsuk-summary-list--no-border"
/>
</div>
`;

exports[`SummaryList matches snapshot: SummaryList 1`] = `
<div>
<dl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { forwardRef } from 'react';

import { type AsElementLink } from '#util/types/LinkTypes.js';

export interface SummaryListActionProps extends AsElementLink<HTMLAnchorElement> {
visuallyHiddenText: string;
}

export const SummaryListAction = forwardRef<HTMLAnchorElement, SummaryListActionProps>(
(props, forwardedRef) => {
const { children, className, asElement: Element = 'a', visuallyHiddenText, ...rest } = props;

return (
<Element ref={forwardedRef} {...rest}>
{children}
<span className="nhsuk-u-visually-hidden"> {visuallyHiddenText}</span>
</Element>
);
},
);

SummaryListAction.displayName = 'SummaryList.Action';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import classNames from 'classnames';
import { type ComponentPropsWithoutRef, type FC } from 'react';

export const SummaryListActions: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
<dd className={classNames('nhsuk-summary-list__actions', className)} {...rest} />
);

SummaryListActions.displayName = 'SummaryList.Actions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import classNames from 'classnames';
import { type ComponentPropsWithoutRef, type FC } from 'react';

export const SummaryListKey: FC<ComponentPropsWithoutRef<'dt'>> = ({ className, ...rest }) => (
<dt className={classNames('nhsuk-summary-list__key', className)} {...rest} />
);

SummaryListKey.displayName = 'SummaryList.Key';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import classNames from 'classnames';
import { type ComponentPropsWithoutRef, type FC } from 'react';

export interface SummaryListRowProps extends ComponentPropsWithoutRef<'div'> {
noActions?: boolean;
noBorder?: boolean;
}

export const SummaryListRow: FC<SummaryListRowProps> = ({
className,
noActions,
noBorder,
...rest
}) => (
<div
className={classNames(
'nhsuk-summary-list__row',
{ 'nhsuk-summary-list__row--no-actions': noActions },
{ 'nhsuk-summary-list__row--no-border': noBorder },
className,
)}
{...rest}
/>
);

SummaryListRow.displayName = 'SummaryList.Row';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import classNames from 'classnames';
import { type ComponentPropsWithoutRef, type FC } from 'react';

export const SummaryListValue: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
<dd className={classNames('nhsuk-summary-list__value', className)} {...rest} />
);

SummaryListValue.displayName = 'SummaryList.Value';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './SummaryListAction.js';
export * from './SummaryListActions.js';
export * from './SummaryListKey.js';
export * from './SummaryListRow.js';
export * from './SummaryListValue.js';
1 change: 1 addition & 0 deletions src/components/content-presentation/summary-list/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './components/index.js';
export * from './SummaryList.js';
Loading