diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index bceab9fb..9c002616 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -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 `` level +- new child component `` for row actions + +```patch + +- ++ + Name + Karen Francis + +- +- Change name +- ++ ++ Change ++ + + +- ++ + Date of birth + 15 March 1984 + + +``` + ### 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: diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 5755e98d..c6a86f28 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -113,6 +113,7 @@ describe('Index', () => { 'SelectOption', 'SkipLink', 'SummaryList', + 'SummaryListAction', 'SummaryListActions', 'SummaryListKey', 'SummaryListRow', diff --git a/src/components/content-presentation/summary-list/SummaryList.tsx b/src/components/content-presentation/summary-list/SummaryList.tsx index c2cd6e70..668990f1 100644 --- a/src/components/content-presentation/summary-list/SummaryList.tsx +++ b/src/components/content-presentation/summary-list/SummaryList.tsx @@ -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> = ({ className, ...rest }) => ( -
-); - -export const SummaryListKey: FC> = ({ className, ...rest }) => ( -
-); - -export const SummaryListValue: FC> = ({ className, ...rest }) => ( -
-); - -export const SummaryListActions: FC> = ({ className, ...rest }) => ( -
-); +import { + SummaryListAction, + SummaryListActions, + SummaryListKey, + SummaryListRow, + SummaryListValue, +} from './components/index.js'; export interface SummaryListProps extends ComponentPropsWithoutRef<'dl'> { noBorder?: boolean; @@ -36,14 +28,11 @@ const SummaryListComponent = forwardRef( ); 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, }); diff --git a/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx b/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx index 02398c01..97276e7c 100644 --- a/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx +++ b/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import { createRef } from 'react'; +import { createRef, type ComponentProps } from 'react'; import { SummaryList } from '..'; @@ -10,6 +10,12 @@ describe('SummaryList', () => { expect(container).toMatchSnapshot('SummaryList'); }); + it('matches snapshot without border', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + it('forwards refs', () => { const ref = createRef(); @@ -31,35 +37,98 @@ describe('SummaryList', () => { it('matches snapshot', () => { const { container } = render(Row); - expect(container.textContent).toBe('Row'); + expect(container).toHaveTextContent('Row'); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot without border', () => { + const { container } = render(Row); + + expect(container).toHaveTextContent('Row'); expect(container).toMatchSnapshot(); }); }); describe('SummaryList.Key', () => { it('matches snapshot', () => { - const { container } = render(Key); + const { container } = render(Example key); - expect(container.textContent).toBe('Key'); + expect(container).toHaveTextContent('Example key'); expect(container).toMatchSnapshot(); }); }); describe('SummaryList.Value', () => { it('matches snapshot', () => { - const { container } = render(Value); + const { container } = render(Example value); - expect(container.textContent).toBe('Value'); + expect(container).toHaveTextContent('Example value'); expect(container).toMatchSnapshot(); }); }); describe('SummaryList.Actions', () => { it('matches snapshot', () => { - const { container } = render(Actions); + const { container } = render( + + + Edit + + + Delete + + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('SummaryList.Action', () => { + it('matches snapshot', () => { + const { container } = render( + + Edit + , + ); - 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 ( + + {children} + + ); + } + + const { container } = render( + + Edit + , + ); + + const rowActionEl = container.querySelector('a'); + + expect(rowActionEl?.dataset).toHaveProperty('customLink', 'true'); + }); + + it('forwards refs', () => { + const ref = createRef(); + + const { container } = render( + + Edit + , + ); + + const rowActionEl = container.querySelector('a'); + + expect(ref.current).toBe(rowActionEl); + expect(ref.current).toHaveAttribute('href', '#'); + }); }); }); diff --git a/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap b/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap index 5cf97f27..a350110c 100644 --- a/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap +++ b/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap @@ -1,11 +1,48 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`SummaryList SummaryList.Action matches snapshot 1`] = ` + +`; + exports[`SummaryList SummaryList.Actions matches snapshot 1`] = `
- Actions + + Edit + + + example key + + + + Delete + + + example key + +
`; @@ -15,7 +52,7 @@ exports[`SummaryList SummaryList.Key matches snapshot 1`] = `
- Key + Example key
`; @@ -30,16 +67,34 @@ exports[`SummaryList SummaryList.Row matches snapshot 1`] = ` `; +exports[`SummaryList SummaryList.Row matches snapshot without border 1`] = ` +
+
+ Row +
+
+`; + exports[`SummaryList SummaryList.Value matches snapshot 1`] = `
- Value + Example value
`; +exports[`SummaryList matches snapshot without border 1`] = ` +
+
+
+`; + exports[`SummaryList matches snapshot: SummaryList 1`] = `
{ + visuallyHiddenText: string; +} + +export const SummaryListAction = forwardRef( + (props, forwardedRef) => { + const { children, className, asElement: Element = 'a', visuallyHiddenText, ...rest } = props; + + return ( + + {children} + {visuallyHiddenText} + + ); + }, +); + +SummaryListAction.displayName = 'SummaryList.Action'; diff --git a/src/components/content-presentation/summary-list/components/SummaryListActions.tsx b/src/components/content-presentation/summary-list/components/SummaryListActions.tsx new file mode 100644 index 00000000..327dcf6c --- /dev/null +++ b/src/components/content-presentation/summary-list/components/SummaryListActions.tsx @@ -0,0 +1,8 @@ +import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; + +export const SummaryListActions: FC> = ({ className, ...rest }) => ( +
+); + +SummaryListActions.displayName = 'SummaryList.Actions'; diff --git a/src/components/content-presentation/summary-list/components/SummaryListKey.tsx b/src/components/content-presentation/summary-list/components/SummaryListKey.tsx new file mode 100644 index 00000000..1045f66e --- /dev/null +++ b/src/components/content-presentation/summary-list/components/SummaryListKey.tsx @@ -0,0 +1,8 @@ +import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; + +export const SummaryListKey: FC> = ({ className, ...rest }) => ( +
+); + +SummaryListKey.displayName = 'SummaryList.Key'; diff --git a/src/components/content-presentation/summary-list/components/SummaryListRow.tsx b/src/components/content-presentation/summary-list/components/SummaryListRow.tsx new file mode 100644 index 00000000..73ab2924 --- /dev/null +++ b/src/components/content-presentation/summary-list/components/SummaryListRow.tsx @@ -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 = ({ + className, + noActions, + noBorder, + ...rest +}) => ( +
+); + +SummaryListRow.displayName = 'SummaryList.Row'; diff --git a/src/components/content-presentation/summary-list/components/SummaryListValue.tsx b/src/components/content-presentation/summary-list/components/SummaryListValue.tsx new file mode 100644 index 00000000..091d3799 --- /dev/null +++ b/src/components/content-presentation/summary-list/components/SummaryListValue.tsx @@ -0,0 +1,8 @@ +import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; + +export const SummaryListValue: FC> = ({ className, ...rest }) => ( +
+); + +SummaryListValue.displayName = 'SummaryList.Value'; diff --git a/src/components/content-presentation/summary-list/components/index.ts b/src/components/content-presentation/summary-list/components/index.ts new file mode 100644 index 00000000..eb031e44 --- /dev/null +++ b/src/components/content-presentation/summary-list/components/index.ts @@ -0,0 +1,5 @@ +export * from './SummaryListAction.js'; +export * from './SummaryListActions.js'; +export * from './SummaryListKey.js'; +export * from './SummaryListRow.js'; +export * from './SummaryListValue.js'; diff --git a/src/components/content-presentation/summary-list/index.ts b/src/components/content-presentation/summary-list/index.ts index 7a6b7689..134bc45e 100644 --- a/src/components/content-presentation/summary-list/index.ts +++ b/src/components/content-presentation/summary-list/index.ts @@ -1 +1,2 @@ +export * from './components/index.js'; export * from './SummaryList.js'; diff --git a/stories/Content Presentation/SummaryList.stories.tsx b/stories/Content Presentation/SummaryList.stories.tsx index 8cac5990..497d0040 100644 --- a/stories/Content Presentation/SummaryList.stories.tsx +++ b/stories/Content Presentation/SummaryList.stories.tsx @@ -1,74 +1,55 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ import { type Meta, type StoryObj } from '@storybook/react-vite'; import { SummaryList } from '#components/content-presentation/summary-list/index.js'; import { BodyText } from '#components/typography/BodyText.js'; -/** - * ## Implementation notes - * - * When providing action links, you must include visually hidden text. This means a screen reader user will hear a meaningful action, like "Change name" or "Change date of birth".' - * - * Example of an action link: - * - * ```jsx - * - * Change - * - * {' '}name - * - * - * ``` - */ - const meta: Meta = { title: 'Content Presentation/SummaryList', component: SummaryList, -}; -export default meta; -type Story = StoryObj; - -export const Standard: Story = { argTypes: { noBorder: { type: 'boolean', defaultValue: false, }, }, - args: { noBorder: false }, - render: ({ noBorder }) => ( - +}; +export default meta; +type Story = StoryObj; + +export const Standard: Story = { + render: (args) => ( + Name - Sarah Philips + Karen Francis - - Change name - + + Change + Date of birth - 5 January 1978 + 15 March 1984 - - Change date of birth - + + Change + Contact information - 72 Guild Street + 73 Roman Rd
- London + Leeds
- SE23 6FH + LS2 5ZN
- - Change contact information - + + Change +
@@ -78,46 +59,116 @@ export const Standard: Story = { sarah.phillips@example.com - - Change contact details - + + Change +
), - parameters: { - docs: { - description: { - story: - 'Change links must include visually hidden text. This means a screen reader user will hear a meaningful action, like "Change name" or "Change date of birth".', - }, - }, - }, +}; + +export const SummaryListWithoutActionsOnLastRow: Story = { + render: (args) => ( + + + Name + Karen Francis + + + Change + + + + + Date of birth + 15 March 1984 + + + Change + + + + + Contact information + + 73 Roman Rd +
+ Leeds +
+ LS2 5ZN +
+ + + Change + + +
+ + Contact details + + 07700 900457 + sarah.phillips@example.com + + +
+ ), }; export const SummaryListWithoutActions: Story = { - args: { noBorder: false }, - render: ({ noBorder }) => ( - + render: (args) => ( + Name - Sarah Philips + Karen Francis Date of birth - 5 January 1978 + 15 March 1984 Contact information - 72 Guild Street + 73 Roman Rd
- London + Leeds
- SE23 6FH + LS2 5ZN +
+
+ + Contact details + + 07700 900457 + sarah.phillips@example.com +
+ ), +}; + +export const SummaryListWithoutBorderOnLastRow: Story = { + render: (args) => ( + + + Name + Karen Francis + + + Date of birth + 15 March 1984 + + Contact information + + 73 Roman Rd +
+ Leeds +
+ LS2 5ZN +
+
+ Contact details 07700 900457 @@ -132,24 +183,24 @@ export const SummaryListWithoutBorder: Story = { args: { noBorder: true, }, - render: ({ noBorder }) => ( - + render: (args) => ( + Name - Sarah Philips + Karen Francis Date of birth - 5 January 1978 + 15 March 1984 Contact information - 72 Guild Street + 73 Roman Rd
- London + Leeds
- SE23 6FH + LS2 5ZN