Skip to content

Commit dbc24e4

Browse files
Support summary list row noBorder and noActions props (#305)
* Split summary list into child components * Add missing summary list row `noBorder` prop * Add missing summary list row `noActions` prop * Remove incorrect action link examples from summary lists * Use Jest text content matchers * Add summary list `<SummaryList.Action>` child component * Add stories for summary list row `noBorder` and `noActions` * Update migration guide * Add tests for summary list action refs * Add tests for summary list action as custom element
1 parent 624f97e commit dbc24e4

File tree

13 files changed

+370
-97
lines changed

13 files changed

+370
-97
lines changed

docs/upgrade-to-6.0.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,36 @@ The [panel](https://service-manual.nhs.uk/design-system/components/panel) compon
6060

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

63+
### Summary list rows and actions
64+
65+
The [summary list](https://service-manual.nhs.uk/design-system/components/summary-list) component now includes improvements from NHS.UK frontend v9.6.2:
66+
67+
- new props `noBorder` and `noActions` supported at `<SummaryList.Row>` level
68+
- new child component `<SummaryList.Action>` for row actions
69+
70+
```patch
71+
<SummaryList>
72+
- <SummaryList.Row>
73+
+ <SummaryList.Row noBorder>
74+
<SummaryList.Key>Name</SummaryList.Key>
75+
<SummaryList.Value>Karen Francis</SummaryList.Value>
76+
<SummaryList.Actions>
77+
- <a href="#">
78+
- Change<span className="nhsuk-u-visually-hidden"> name</span>
79+
- </a>
80+
+ <SummaryList.Action href="#" visuallyHiddenText="name">
81+
+ Change
82+
+ </SummaryList.Action>
83+
</SummaryList.Actions>
84+
</SummaryList.Row>
85+
- <SummaryList.Row>
86+
+ <SummaryList.Row noActions>
87+
<SummaryList.Key>Date of birth</SummaryList.Key>
88+
<SummaryList.Value>15 March 1984</SummaryList.Value>
89+
</SummaryList.Row>
90+
</SummaryList>
91+
```
92+
6393
### Support for React Server Components (RSC)
6494

6595
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:

src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe('Index', () => {
113113
'SelectOption',
114114
'SkipLink',
115115
'SummaryList',
116+
'SummaryListAction',
116117
'SummaryListActions',
117118
'SummaryListKey',
118119
'SummaryListRow',
Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
import classNames from 'classnames';
2-
import { forwardRef, type ComponentPropsWithoutRef, type FC } from 'react';
2+
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
33

4-
export const SummaryListRow: FC<ComponentPropsWithoutRef<'div'>> = ({ className, ...rest }) => (
5-
<div className={classNames('nhsuk-summary-list__row', className)} {...rest} />
6-
);
7-
8-
export const SummaryListKey: FC<ComponentPropsWithoutRef<'dt'>> = ({ className, ...rest }) => (
9-
<dt className={classNames('nhsuk-summary-list__key', className)} {...rest} />
10-
);
11-
12-
export const SummaryListValue: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
13-
<dd className={classNames('nhsuk-summary-list__value', className)} {...rest} />
14-
);
15-
16-
export const SummaryListActions: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
17-
<dd className={classNames('nhsuk-summary-list__actions', className)} {...rest} />
18-
);
4+
import {
5+
SummaryListAction,
6+
SummaryListActions,
7+
SummaryListKey,
8+
SummaryListRow,
9+
SummaryListValue,
10+
} from './components/index.js';
1911

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

3830
SummaryListComponent.displayName = 'SummaryList';
39-
SummaryListRow.displayName = 'SummaryList.Row';
40-
SummaryListKey.displayName = 'SummaryList.Key';
41-
SummaryListValue.displayName = 'SummaryList.Value';
42-
SummaryListActions.displayName = 'SummaryList.Actions';
4331

4432
export const SummaryList = Object.assign(SummaryListComponent, {
4533
Row: SummaryListRow,
4634
Key: SummaryListKey,
4735
Value: SummaryListValue,
36+
Action: SummaryListAction,
4837
Actions: SummaryListActions,
4938
});

src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { render } from '@testing-library/react';
2-
import { createRef } from 'react';
2+
import { createRef, type ComponentProps } from 'react';
33

44
import { SummaryList } from '..';
55

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

13+
it('matches snapshot without border', () => {
14+
const { container } = render(<SummaryList noBorder />);
15+
16+
expect(container).toMatchSnapshot();
17+
});
18+
1319
it('forwards refs', () => {
1420
const ref = createRef<HTMLDListElement>();
1521

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

34-
expect(container.textContent).toBe('Row');
40+
expect(container).toHaveTextContent('Row');
41+
expect(container).toMatchSnapshot();
42+
});
43+
44+
it('matches snapshot without border', () => {
45+
const { container } = render(<SummaryList.Row noBorder>Row</SummaryList.Row>);
46+
47+
expect(container).toHaveTextContent('Row');
3548
expect(container).toMatchSnapshot();
3649
});
3750
});
3851

3952
describe('SummaryList.Key', () => {
4053
it('matches snapshot', () => {
41-
const { container } = render(<SummaryList.Key>Key</SummaryList.Key>);
54+
const { container } = render(<SummaryList.Key>Example key</SummaryList.Key>);
4255

43-
expect(container.textContent).toBe('Key');
56+
expect(container).toHaveTextContent('Example key');
4457
expect(container).toMatchSnapshot();
4558
});
4659
});
4760

4861
describe('SummaryList.Value', () => {
4962
it('matches snapshot', () => {
50-
const { container } = render(<SummaryList.Value>Value</SummaryList.Value>);
63+
const { container } = render(<SummaryList.Value>Example value</SummaryList.Value>);
5164

52-
expect(container.textContent).toBe('Value');
65+
expect(container).toHaveTextContent('Example value');
5366
expect(container).toMatchSnapshot();
5467
});
5568
});
5669

5770
describe('SummaryList.Actions', () => {
5871
it('matches snapshot', () => {
59-
const { container } = render(<SummaryList.Actions>Actions</SummaryList.Actions>);
72+
const { container } = render(
73+
<SummaryList.Actions>
74+
<SummaryList.Action href="#" visuallyHiddenText="example key">
75+
Edit
76+
</SummaryList.Action>
77+
<SummaryList.Action href="#" visuallyHiddenText="example key">
78+
Delete
79+
</SummaryList.Action>
80+
</SummaryList.Actions>,
81+
);
82+
83+
expect(container).toMatchSnapshot();
84+
});
85+
});
86+
87+
describe('SummaryList.Action', () => {
88+
it('matches snapshot', () => {
89+
const { container } = render(
90+
<SummaryList.Action href="#" visuallyHiddenText="example key">
91+
Edit
92+
</SummaryList.Action>,
93+
);
6094

61-
expect(container.textContent).toBe('Actions');
95+
expect(container).toHaveTextContent('Edit example key');
6296
expect(container).toMatchSnapshot();
6397
});
98+
99+
it('renders as custom element', () => {
100+
function CustomLink({ children, href, ...rest }: ComponentProps<'a'>) {
101+
return (
102+
<a href={href} {...rest} data-custom-link="true">
103+
{children}
104+
</a>
105+
);
106+
}
107+
108+
const { container } = render(
109+
<SummaryList.Action href="#" visuallyHiddenText="example key" asElement={CustomLink}>
110+
Edit
111+
</SummaryList.Action>,
112+
);
113+
114+
const rowActionEl = container.querySelector('a');
115+
116+
expect(rowActionEl?.dataset).toHaveProperty('customLink', 'true');
117+
});
118+
119+
it('forwards refs', () => {
120+
const ref = createRef<HTMLAnchorElement>();
121+
122+
const { container } = render(
123+
<SummaryList.Action href="#" visuallyHiddenText="example key" ref={ref}>
124+
Edit
125+
</SummaryList.Action>,
126+
);
127+
128+
const rowActionEl = container.querySelector('a');
129+
130+
expect(ref.current).toBe(rowActionEl);
131+
expect(ref.current).toHaveAttribute('href', '#');
132+
});
64133
});
65134
});

src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,48 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

3+
exports[`SummaryList SummaryList.Action matches snapshot 1`] = `
4+
<div>
5+
<a
6+
href="#"
7+
>
8+
Edit
9+
<span
10+
class="nhsuk-u-visually-hidden"
11+
>
12+
13+
example key
14+
</span>
15+
</a>
16+
</div>
17+
`;
18+
319
exports[`SummaryList SummaryList.Actions matches snapshot 1`] = `
420
<div>
521
<dd
622
class="nhsuk-summary-list__actions"
723
>
8-
Actions
24+
<a
25+
href="#"
26+
>
27+
Edit
28+
<span
29+
class="nhsuk-u-visually-hidden"
30+
>
31+
32+
example key
33+
</span>
34+
</a>
35+
<a
36+
href="#"
37+
>
38+
Delete
39+
<span
40+
class="nhsuk-u-visually-hidden"
41+
>
42+
43+
example key
44+
</span>
45+
</a>
946
</dd>
1047
</div>
1148
`;
@@ -15,7 +52,7 @@ exports[`SummaryList SummaryList.Key matches snapshot 1`] = `
1552
<dt
1653
class="nhsuk-summary-list__key"
1754
>
18-
Key
55+
Example key
1956
</dt>
2057
</div>
2158
`;
@@ -30,16 +67,34 @@ exports[`SummaryList SummaryList.Row matches snapshot 1`] = `
3067
</div>
3168
`;
3269

70+
exports[`SummaryList SummaryList.Row matches snapshot without border 1`] = `
71+
<div>
72+
<div
73+
class="nhsuk-summary-list__row nhsuk-summary-list__row--no-border"
74+
>
75+
Row
76+
</div>
77+
</div>
78+
`;
79+
3380
exports[`SummaryList SummaryList.Value matches snapshot 1`] = `
3481
<div>
3582
<dd
3683
class="nhsuk-summary-list__value"
3784
>
38-
Value
85+
Example value
3986
</dd>
4087
</div>
4188
`;
4289

90+
exports[`SummaryList matches snapshot without border 1`] = `
91+
<div>
92+
<dl
93+
class="nhsuk-summary-list nhsuk-summary-list--no-border"
94+
/>
95+
</div>
96+
`;
97+
4398
exports[`SummaryList matches snapshot: SummaryList 1`] = `
4499
<div>
45100
<dl
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { forwardRef } from 'react';
2+
3+
import { type AsElementLink } from '#util/types/LinkTypes.js';
4+
5+
export interface SummaryListActionProps extends AsElementLink<HTMLAnchorElement> {
6+
visuallyHiddenText: string;
7+
}
8+
9+
export const SummaryListAction = forwardRef<HTMLAnchorElement, SummaryListActionProps>(
10+
(props, forwardedRef) => {
11+
const { children, className, asElement: Element = 'a', visuallyHiddenText, ...rest } = props;
12+
13+
return (
14+
<Element ref={forwardedRef} {...rest}>
15+
{children}
16+
<span className="nhsuk-u-visually-hidden"> {visuallyHiddenText}</span>
17+
</Element>
18+
);
19+
},
20+
);
21+
22+
SummaryListAction.displayName = 'SummaryList.Action';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import classNames from 'classnames';
2+
import { type ComponentPropsWithoutRef, type FC } from 'react';
3+
4+
export const SummaryListActions: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
5+
<dd className={classNames('nhsuk-summary-list__actions', className)} {...rest} />
6+
);
7+
8+
SummaryListActions.displayName = 'SummaryList.Actions';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import classNames from 'classnames';
2+
import { type ComponentPropsWithoutRef, type FC } from 'react';
3+
4+
export const SummaryListKey: FC<ComponentPropsWithoutRef<'dt'>> = ({ className, ...rest }) => (
5+
<dt className={classNames('nhsuk-summary-list__key', className)} {...rest} />
6+
);
7+
8+
SummaryListKey.displayName = 'SummaryList.Key';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import classNames from 'classnames';
2+
import { type ComponentPropsWithoutRef, type FC } from 'react';
3+
4+
export interface SummaryListRowProps extends ComponentPropsWithoutRef<'div'> {
5+
noActions?: boolean;
6+
noBorder?: boolean;
7+
}
8+
9+
export const SummaryListRow: FC<SummaryListRowProps> = ({
10+
className,
11+
noActions,
12+
noBorder,
13+
...rest
14+
}) => (
15+
<div
16+
className={classNames(
17+
'nhsuk-summary-list__row',
18+
{ 'nhsuk-summary-list__row--no-actions': noActions },
19+
{ 'nhsuk-summary-list__row--no-border': noBorder },
20+
className,
21+
)}
22+
{...rest}
23+
/>
24+
);
25+
26+
SummaryListRow.displayName = 'SummaryList.Row';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import classNames from 'classnames';
2+
import { type ComponentPropsWithoutRef, type FC } from 'react';
3+
4+
export const SummaryListValue: FC<ComponentPropsWithoutRef<'dd'>> = ({ className, ...rest }) => (
5+
<dd className={classNames('nhsuk-summary-list__value', className)} {...rest} />
6+
);
7+
8+
SummaryListValue.displayName = 'SummaryList.Value';

0 commit comments

Comments
 (0)