From a00d653c7a0fb0b31fd9d495879eafcf5ac2df4f Mon Sep 17 00:00:00 2001 From: Rob Kerry Date: Thu, 9 Oct 2025 09:14:47 +0100 Subject: [PATCH 01/13] Add the Notification Banner introduced in NHS Frontend v10 (#283) --- docs/upgrade-to-6.0.md | 11 + src/__tests__/index.test.ts | 1 + src/components/content-presentation/index.ts | 1 + .../NotificationBanner.tsx | 44 +++ .../__tests__/NotificationBanner.test.tsx | 130 +++++++ .../NotificationBanner.test.tsx.snap | 341 ++++++++++++++++++ .../components/NotificationBannerHeading.tsx | 22 ++ .../components/NotificationBannerLink.tsx | 21 ++ .../notification-banner/components/index.ts | 2 + .../notification-banner/index.ts | 1 + .../NotificationBanner.stories.tsx | 87 +++++ 11 files changed, 661 insertions(+) create mode 100644 src/components/content-presentation/notification-banner/NotificationBanner.tsx create mode 100644 src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx create mode 100644 src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap create mode 100644 src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx create mode 100644 src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx create mode 100644 src/components/content-presentation/notification-banner/components/index.ts create mode 100644 src/components/content-presentation/notification-banner/index.ts create mode 100644 stories/Content Presentation/NotificationBanner.stories.tsx diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index 5cc03627..dba1425c 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -31,6 +31,17 @@ 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. +### Notification banner component + +The [notification banner](https://service-manual.nhs.uk/design-system/components/notification-banner) component from NHS.UK frontend v10 has been added: + +```jsx + + Upcoming Maintenance +

The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025.

+
+``` + ### 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 ebc22f7d..d6e3badd 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -91,6 +91,7 @@ describe('Index', () => { 'NavAZ', 'NavAZDisabledItem', 'NavAZLinkItem', + 'NotificationBanner', 'Pagination', 'PaginationLink', 'Panel', diff --git a/src/components/content-presentation/index.ts b/src/components/content-presentation/index.ts index 55a38c07..e410fcc9 100644 --- a/src/components/content-presentation/index.ts +++ b/src/components/content-presentation/index.ts @@ -4,6 +4,7 @@ export * from './hero/index.js'; export * from './icons/index.js'; export * from './images/index.js'; export * from './inset-text/index.js'; +export * from './notification-banner/index.js'; export * from './panel/index.js'; export * from './summary-list/index.js'; export * from './table/index.js'; diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx new file mode 100644 index 00000000..c7eaf4e4 --- /dev/null +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -0,0 +1,44 @@ +import { type ComponentPropsWithoutRef, forwardRef } from 'react'; +import classNames from 'classnames'; +import { HeadingLevel } from '#components'; +import { NotificationBannerHeading, NotificationBannerLink } from './components/index.js'; + +export interface NotificationBannerProps extends ComponentPropsWithoutRef<'div'> { + success?: boolean; +} + +const NotificationBannerComponent = forwardRef( + ({ children, className, title, success, ...rest }, forwardedRef) => { + return ( +
+
+ + {title || (success ? 'Success' : 'Important')} + +
+
+ {children} +
+
+ ); + }, +); + +NotificationBannerComponent.displayName = 'NotificationBanner'; + +export const NotificationBanner = Object.assign(NotificationBannerComponent, { + Heading: NotificationBannerHeading, + Link: NotificationBannerLink, +}); diff --git a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx new file mode 100644 index 00000000..b4c70a73 --- /dev/null +++ b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx @@ -0,0 +1,130 @@ +import { renderClient, renderServer } from '#util/components'; +import { NotificationBanner } from '#components/content-presentation/notification-banner'; + +describe('NotificationBanner', () => { + it('matches snapshot', async () => { + const { container } = await renderClient( + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot success', async () => { + const { container } = await renderClient( + + Patient record updated + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot custom title', async () => { + const { container } = await renderClient( + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot with link in heading', async () => { + const { container } = await renderClient( + + + You have 7 days left to send your application.{' '} + View application. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot with link in body', async () => { + const { container } = await renderClient( + + Patient record updated +

+ Contact{' '} + example@department.nhs.uk if + you think there's a problem. +

+
, + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot (via server)', async () => { + const { container } = await renderServer( + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot success (via server)', async () => { + const { container } = await renderServer( + + Patient record updated + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot custom title (via server)', async () => { + const { container } = await renderServer( + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot with link in heading (via server)', async () => { + const { container } = await renderServer( + + + You have 7 days left to send your application.{' '} + View application. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot with link in body (via server)', async () => { + const { container } = await renderServer( + + Patient record updated +

+ Contact{' '} + example@department.nhs.uk if + you think there's a problem. +

+
, + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap new file mode 100644 index 00000000..336146b5 --- /dev/null +++ b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap @@ -0,0 +1,341 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`NotificationBanner matches snapshot (via server) 1`] = ` +
+
+
+

+ Important +

+
+
+

+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot 1`] = ` +
+
+
+

+ Important +

+
+
+

+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot custom title (via server) 1`] = ` +
+
+
+

+ Upcoming Maintenance +

+
+
+

+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot custom title 1`] = ` +
+
+
+

+ Upcoming Maintenance +

+
+
+

+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot success (via server) 1`] = ` +
+
+
+

+ Success +

+
+
+

+ Patient record updated +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot success 1`] = ` +
+
+
+

+ Success +

+
+
+

+ Patient record updated +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot with link in body (via server) 1`] = ` +
+
+
+

+ Important +

+
+
+

+ Patient record updated +

+

+ Contact + + + + example@department.nhs.uk + + if you think there's a problem. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot with link in body 1`] = ` +
+
+
+

+ Important +

+
+
+

+ Patient record updated +

+

+ Contact + + + example@department.nhs.uk + + if you think there's a problem. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot with link in heading (via server) 1`] = ` +
+
+
+

+ Important +

+
+
+

+ You have 7 days left to send your application. + + + + View application + + . +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot with link in heading 1`] = ` +
+
+
+

+ Important +

+
+
+

+ You have 7 days left to send your application. + + + View application + + . +

+
+
+
+`; diff --git a/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx b/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx new file mode 100644 index 00000000..b1878cdc --- /dev/null +++ b/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx @@ -0,0 +1,22 @@ +import { HeadingLevel, type HeadingLevelProps } from '#components'; +import type { FC } from 'react'; + +export type NotificationBannerHeadingProps = HeadingLevelProps; + +const NotificationBannerHeadingComponent: FC = ({ + children, + headingLevel = 'h3', + ...rest +}) => ( + + {children} + +); + +NotificationBannerHeadingComponent.displayName = 'NotificationBanner.Heading'; + +export const NotificationBannerHeading = Object.assign(NotificationBannerHeadingComponent); diff --git a/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx b/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx new file mode 100644 index 00000000..4b0873a1 --- /dev/null +++ b/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx @@ -0,0 +1,21 @@ +import type { AsElementLink } from '#util/types/index.js'; +import { forwardRef } from 'react'; +import classNames from 'classnames'; + +export type NotificationBannerLinkProps = AsElementLink; + +const NotificationBannerLinkComponent = forwardRef( + ({ children, className, asElement: Element = 'a', ...rest }, forwardedRef) => ( + + {children} + + ), +); + +NotificationBannerLinkComponent.displayName = 'NotificationBanner.Link'; + +export const NotificationBannerLink = Object.assign(NotificationBannerLinkComponent); diff --git a/src/components/content-presentation/notification-banner/components/index.ts b/src/components/content-presentation/notification-banner/components/index.ts new file mode 100644 index 00000000..a2edb989 --- /dev/null +++ b/src/components/content-presentation/notification-banner/components/index.ts @@ -0,0 +1,2 @@ +export * from './NotificationBannerHeading.js'; +export * from './NotificationBannerLink.js'; diff --git a/src/components/content-presentation/notification-banner/index.ts b/src/components/content-presentation/notification-banner/index.ts new file mode 100644 index 00000000..9da940f0 --- /dev/null +++ b/src/components/content-presentation/notification-banner/index.ts @@ -0,0 +1 @@ +export * from './NotificationBanner.js'; diff --git a/stories/Content Presentation/NotificationBanner.stories.tsx b/stories/Content Presentation/NotificationBanner.stories.tsx new file mode 100644 index 00000000..91ce6d79 --- /dev/null +++ b/stories/Content Presentation/NotificationBanner.stories.tsx @@ -0,0 +1,87 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { NotificationBanner } from '#components'; + +/** + * This component can be found in the `nhsuk-frontend` repository here. + * + * ## Implementation Notes + * + * The `NotificationBanner` component has two subcomponents: + * + * - `NotificationBanner.Heading` + * - `NotificationBanner.Link` + * + * ## Usage + * + * ### Standard + * + * ```jsx + * import { NotificationBanner } from "nhsuk-react-components"; + * + * const Element = () => { + * return ( + * + * Patient record updated + *

+ * Contact example@department.nhs.uk if you think there's a problem. + *

+ *
+ * ); + * } + * ``` + */ +const meta: Meta = { + title: 'Content Presentation/Notification Banner', + component: NotificationBanner, +}; +export default meta; +type Story = StoryObj; + +export const StandardPanel: Story = { + args: {}, + render: (args) => ( + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + + ), +}; + +export const SuccessPanel: Story = { + args: {}, + render: (args) => ( + + Patient record updated +

+ Contact{' '} + example@department.nhs.uk if + you think there's a problem. +

+
+ ), +}; + +export const StandardPanelWithLink: Story = { + args: {}, + render: (args) => ( + + + You have 7 days left to send your application. + View application. + + + ), +}; + +export const StandardPanelWithCustomTitle: Story = { + args: { + title: 'Important Message', + }, + render: (args) => ( + + Upcoming Maintenance +

The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025.

+
+ ), +}; From 161cb572c58594f01ab905282e12b7df3c118004 Mon Sep 17 00:00:00 2001 From: Rob Kerry Date: Thu, 9 Oct 2025 13:55:08 +0100 Subject: [PATCH 02/13] Fix circular dependency --- .../notification-banner/NotificationBanner.tsx | 2 +- .../components/NotificationBannerHeading.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index c7eaf4e4..5e4d0e1d 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -1,6 +1,6 @@ import { type ComponentPropsWithoutRef, forwardRef } from 'react'; import classNames from 'classnames'; -import { HeadingLevel } from '#components'; +import { HeadingLevel } from '#components/utils/HeadingLevel.js'; import { NotificationBannerHeading, NotificationBannerLink } from './components/index.js'; export interface NotificationBannerProps extends ComponentPropsWithoutRef<'div'> { diff --git a/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx b/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx index b1878cdc..4adf4704 100644 --- a/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx +++ b/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx @@ -1,4 +1,4 @@ -import { HeadingLevel, type HeadingLevelProps } from '#components'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; import type { FC } from 'react'; export type NotificationBannerHeadingProps = HeadingLevelProps; From 737c284249c7e78d6f76608e883fa8be9860c6c3 Mon Sep 17 00:00:00 2001 From: Rob Kerry Date: Fri, 10 Oct 2025 11:56:52 +0100 Subject: [PATCH 03/13] Address review comments --- docs/upgrade-to-6.0.md | 4 +- .../NotificationBanner.tsx | 74 ++- .../__tests__/NotificationBanner.test.tsx | 260 ++++++++++ .../NotificationBanner.test.tsx.snap | 463 +++++++++++++++++- .../components/NotificationBannerHeading.tsx | 6 +- .../components/NotificationBannerLink.tsx | 6 +- .../components/NotificationBannerTitle.tsx | 27 + .../notification-banner/components/index.ts | 1 + .../NotificationBanner.stories.tsx | 69 ++- 9 files changed, 860 insertions(+), 50 deletions(-) create mode 100644 src/components/content-presentation/notification-banner/components/NotificationBannerTitle.tsx diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index dba1425c..381e5107 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -36,7 +36,7 @@ This replaces the [list panel component](#list-panel) which was removed in NHS.U The [notification banner](https://service-manual.nhs.uk/design-system/components/notification-banner) component from NHS.UK frontend v10 has been added: ```jsx - + Upcoming Maintenance

The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025.

@@ -451,7 +451,7 @@ You must rename the `Select` prop `selectRef` to `ref` for consistency with othe To align with NHS.UK frontend, the skip link component focuses the main content rather than the first heading on the page: ```html -
+
``` diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index 5e4d0e1d..c02df791 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -1,37 +1,74 @@ -import { type ComponentPropsWithoutRef, forwardRef } from 'react'; +import { + Children, + type ComponentPropsWithoutRef, + createRef, + forwardRef, + useEffect, + useState, +} from 'react'; import classNames from 'classnames'; -import { HeadingLevel } from '#components/utils/HeadingLevel.js'; -import { NotificationBannerHeading, NotificationBannerLink } from './components/index.js'; +import { + NotificationBannerHeading, + NotificationBannerLink, + NotificationBannerTitle, +} from './components/index.js'; +import { type NotificationBanner as NotificationBannerModule } from 'nhsuk-frontend'; +import { childIsOfComponentType } from '#util/types/TypeGuards.js'; export interface NotificationBannerProps extends ComponentPropsWithoutRef<'div'> { success?: boolean; + disableAutoFocus?: boolean; } const NotificationBannerComponent = forwardRef( - ({ children, className, title, success, ...rest }, forwardedRef) => { + ({ children, className, title, success, role, disableAutoFocus, ...rest }, forwardedRef) => { + const [moduleRef] = useState(() => forwardedRef || createRef()); + const [instance, setInstance] = useState(); + + useEffect(() => { + if (!('current' in moduleRef) || !moduleRef.current || instance) { + return; + } + + const { current: $root } = moduleRef; + + import('nhsuk-frontend').then(({ NotificationBanner }) => { + setInstance(new NotificationBanner($root)); + }); + }, [moduleRef, instance]); + + const items = Children.toArray(children); + const titleElement = items.find((child) => + childIsOfComponentType(child, NotificationBannerTitle), + ) || {title}; + const nonTitleItems = items.filter( + (child) => !childIsOfComponentType(child, NotificationBannerTitle), + ); + const headerElement = nonTitleItems.find((child) => + childIsOfComponentType(child, NotificationBannerHeading), + ); + const bodyItems = nonTitleItems.filter( + (child) => !childIsOfComponentType(child, NotificationBannerHeading), + ); return ( -
-
- - {title || (success ? 'Success' : 'Important')} - -
-
- {children} + {titleElement} +
+ {headerElement} + {bodyItems}
-
+ ); }, ); @@ -39,6 +76,7 @@ const NotificationBannerComponent = forwardRef { it('matches snapshot', async () => { @@ -14,6 +16,7 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot success', async () => { const { container } = await renderClient( @@ -24,6 +27,7 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot custom title', async () => { const { container } = await renderClient( @@ -36,6 +40,23 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + + it('matches snapshot custom title html', async () => { + const { container } = await renderClient( + + + Very important information + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot with link in heading', async () => { const { container } = await renderClient( @@ -49,6 +70,7 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot with link in body', async () => { const { container } = await renderClient( @@ -65,6 +87,87 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot with no sub elements', async () => { + const { container } = await renderClient( + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with list', async () => { + const { container } = await renderClient( + + 4 files uploaded +
    +
  • + government-strategy.pdf +
  • +
  • + government-strategy-v2.pdf +
  • +
  • + government-strategy-v3-FINAL.pdf +
  • +
  • + government-strategy-v4-FINAL-v2.pdf +
  • +
+
, + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with long heading', async () => { + const { container } = await renderClient( + + + The patient record was withdrawn on 7 March 2014, before being sent in, sent back, + queried, lost, found, subjected to public inquiry, lost again, and finally buried in soft + peat for three months and recycled as firelighters. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with lots of content', async () => { + const { container } = await renderClient( + + + Check if you need to apply the reverse charge to this application + +

+ You will have to apply the{' '} + reverse charge if the applicant + supplies any of these services: +

+
    +
  • + constructing, altering, repairing, extending, demolishing or dismantling buildings or + structures (whether permanent or not), including offshore installation services +
  • +
  • + constructing, altering, repairing, extending, demolishing of any works forming, or + planned to form, part of the land, including (in particular) walls, roadworks, power + lines, electronic communications equipment, aircraft runways, railways, inland + waterways, docks and harbours +
  • +
+
, + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot (via server)', async () => { const { container } = await renderServer( @@ -77,6 +180,7 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot success (via server)', async () => { const { container } = await renderServer( @@ -87,6 +191,7 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot custom title (via server)', async () => { const { container } = await renderServer( @@ -99,6 +204,23 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + + it('matches snapshot custom title html (via server)', async () => { + const { container } = await renderServer( + + + Very important information + + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot with link in heading (via server)', async () => { const { container } = await renderServer( @@ -112,6 +234,7 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + it('matches snapshot with link in body (via server)', async () => { const { container } = await renderServer( @@ -127,4 +250,141 @@ describe('NotificationBanner', () => { expect(container).toMatchSnapshot(); }); + + it('matches snapshot with no sub elements (via server)', async () => { + const { container } = await renderServer( + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with list (via server)', async () => { + const { container } = await renderServer( + + 4 files uploaded +
    +
  • + government-strategy.pdf +
  • +
  • + government-strategy-v2.pdf +
  • +
  • + government-strategy-v3-FINAL.pdf +
  • +
  • + government-strategy-v4-FINAL-v2.pdf +
  • +
+
, + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with long heading (via server)', async () => { + const { container } = await renderServer( + + + The patient record was withdrawn on 7 March 2014, before being sent in, sent back, + queried, lost, found, subjected to public inquiry, lost again, and finally buried in soft + peat for three months and recycled as firelighters. + + , + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with lots of content (via server)', async () => { + const { container } = await renderServer( + + + Check if you need to apply the reverse charge to this application + +

+ You will have to apply the{' '} + reverse charge if the applicant + supplies any of these services: +

+
    +
  • + constructing, altering, repairing, extending, demolishing or dismantling buildings or + structures (whether permanent or not), including offshore installation services +
  • +
  • + constructing, altering, repairing, extending, demolishing of any works forming, or + planned to form, part of the land, including (in particular) walls, roadworks, power + lines, electronic communications equipment, aircraft runways, railways, inland + waterways, docks and harbours +
  • +
+
, + { className: 'nhsuk-notification-banner' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('forwards refs', async () => { + const ref = createRef(); + + const { modules } = await renderClient( + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + , + { className: 'nhsuk-notification-banner' }, + ); + + const [notificationBannerEl] = modules; + + expect(ref.current).toBe(notificationBannerEl); + expect(ref.current).toHaveClass('nhsuk-notification-banner'); + }); + + it('has default role and autofocus', async () => { + const { modules } = await renderClient( + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + , + { className: 'nhsuk-notification-banner' }, + ); + + const [notificationBannerEl] = modules; + + expect(notificationBannerEl?.getAttribute('role')).toBe('region'); + expect(notificationBannerEl?.getAttribute('data-disable-auto-focus')).toBe(null); + }); + + it('has alert role', async () => { + const { modules } = await renderClient( + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + , + { className: 'nhsuk-notification-banner' }, + ); + + const [notificationBannerEl] = modules; + + expect(notificationBannerEl?.getAttribute('role')).toBe('alert'); + }); + + it('has disabled autofocus', async () => { + const { modules } = await renderClient( + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + , + { className: 'nhsuk-notification-banner' }, + ); + + const [notificationBannerEl] = modules; + + expect(notificationBannerEl?.getAttribute('data-disable-auto-focus')).toBe('true'); + }); }); diff --git a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap index 336146b5..28577703 100644 --- a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap +++ b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap @@ -2,10 +2,11 @@ exports[`NotificationBanner matches snapshot (via server) 1`] = `
-
-
+
`; exports[`NotificationBanner matches snapshot 1`] = `
-
-
+
`; exports[`NotificationBanner matches snapshot custom title (via server) 1`] = `
-
-
+
`; exports[`NotificationBanner matches snapshot custom title 1`] = `
-
-
+
+ +`; + +exports[`NotificationBanner matches snapshot custom title html (via server) 1`] = ` +
+
+
+

+ + Very + + important information +

+
+
+

+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot custom title html 1`] = ` +
+
+
+

+ + Very + + important information +

+
+
+

+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +

+
+
`; exports[`NotificationBanner matches snapshot success (via server) 1`] = `
- +
`; exports[`NotificationBanner matches snapshot success 1`] = `
- +
`; exports[`NotificationBanner matches snapshot with link in body (via server) 1`] = `
-
-
+
`; exports[`NotificationBanner matches snapshot with link in body 1`] = `
-
-
+
`; exports[`NotificationBanner matches snapshot with link in heading (via server) 1`] = `
-
-
+
`; exports[`NotificationBanner matches snapshot with link in heading 1`] = `
-
-
+
+ +`; + +exports[`NotificationBanner matches snapshot with list (via server) 1`] = ` + +`; + +exports[`NotificationBanner matches snapshot with list 1`] = ` + +`; + +exports[`NotificationBanner matches snapshot with long heading (via server) 1`] = ` +
+
+
+

+ Important +

+
+
+

+ The patient record was withdrawn on 7 March 2014, before being sent in, sent back, queried, lost, found, subjected to public inquiry, lost again, and finally buried in soft peat for three months and recycled as firelighters. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot with long heading 1`] = ` +
+
+
+

+ Important +

+
+
+

+ The patient record was withdrawn on 7 March 2014, before being sent in, sent back, queried, lost, found, subjected to public inquiry, lost again, and finally buried in soft peat for three months and recycled as firelighters. +

+
+
+
+`; + +exports[`NotificationBanner matches snapshot with lots of content (via server) 1`] = ` +
+
+
+

+ Important +

+
+
+

+ Check if you need to apply the reverse charge to this application +

+

+ You will have to apply the + + reverse charge + + if the applicant supplies any of these services: +

+
    +
  • + constructing, altering, repairing, extending, demolishing or dismantling buildings or structures (whether permanent or not), including offshore installation services +
  • +
  • + constructing, altering, repairing, extending, demolishing of any works forming, or planned to form, part of the land, including (in particular) walls, roadworks, power lines, electronic communications equipment, aircraft runways, railways, inland waterways, docks and harbours +
  • +
+
+
+
+`; + +exports[`NotificationBanner matches snapshot with lots of content 1`] = ` +
+
+
+

+ Important +

+
+
+

+ Check if you need to apply the reverse charge to this application +

+

+ You will have to apply the + + reverse charge + + if the applicant supplies any of these services: +

+
    +
  • + constructing, altering, repairing, extending, demolishing or dismantling buildings or structures (whether permanent or not), including offshore installation services +
  • +
  • + constructing, altering, repairing, extending, demolishing of any works forming, or planned to form, part of the land, including (in particular) walls, roadworks, power lines, electronic communications equipment, aircraft runways, railways, inland waterways, docks and harbours +
  • +
+
+
+
+`; + +exports[`NotificationBanner matches snapshot with no sub elements (via server) 1`] = ` +
+
+
+

+ Important +

+
+
+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +
+
+
+`; + +exports[`NotificationBanner matches snapshot with no sub elements 1`] = ` +
+
+
+

+ Important +

+
+
+ The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. +
+
`; diff --git a/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx b/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx index 4adf4704..5b0bd4fe 100644 --- a/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx +++ b/src/components/content-presentation/notification-banner/components/NotificationBannerHeading.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react'; export type NotificationBannerHeadingProps = HeadingLevelProps; -const NotificationBannerHeadingComponent: FC = ({ +export const NotificationBannerHeading: FC = ({ children, headingLevel = 'h3', ...rest @@ -17,6 +17,4 @@ const NotificationBannerHeadingComponent: FC = ( ); -NotificationBannerHeadingComponent.displayName = 'NotificationBanner.Heading'; - -export const NotificationBannerHeading = Object.assign(NotificationBannerHeadingComponent); +NotificationBannerHeading.displayName = 'NotificationBanner.Heading'; diff --git a/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx b/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx index 4b0873a1..bc145aac 100644 --- a/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx +++ b/src/components/content-presentation/notification-banner/components/NotificationBannerLink.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; export type NotificationBannerLinkProps = AsElementLink; -const NotificationBannerLinkComponent = forwardRef( +export const NotificationBannerLink = forwardRef( ({ children, className, asElement: Element = 'a', ...rest }, forwardedRef) => ( = ({ + children, + headingLevel = 'h2', + id = 'nhsuk-notification-banner-title', + success, + ...rest +}) => ( +
+ + {children || (success ? 'Success' : 'Important')} + +
+); + +NotificationBannerTitle.displayName = 'NotificationBanner.Title'; diff --git a/src/components/content-presentation/notification-banner/components/index.ts b/src/components/content-presentation/notification-banner/components/index.ts index a2edb989..e9dad42b 100644 --- a/src/components/content-presentation/notification-banner/components/index.ts +++ b/src/components/content-presentation/notification-banner/components/index.ts @@ -1,2 +1,3 @@ export * from './NotificationBannerHeading.js'; export * from './NotificationBannerLink.js'; +export * from './NotificationBannerTitle.js'; diff --git a/stories/Content Presentation/NotificationBanner.stories.tsx b/stories/Content Presentation/NotificationBanner.stories.tsx index 91ce6d79..19165454 100644 --- a/stories/Content Presentation/NotificationBanner.stories.tsx +++ b/stories/Content Presentation/NotificationBanner.stories.tsx @@ -1,13 +1,15 @@ import { type Meta, type StoryObj } from '@storybook/react-vite'; import { NotificationBanner } from '#components'; +import { NotificationBannerLink } from '#components/content-presentation/notification-banner/components'; /** * This component can be found in the `nhsuk-frontend` repository here. * * ## Implementation Notes * - * The `NotificationBanner` component has two subcomponents: + * The `NotificationBanner` component has three subcomponents: * + * - `NotificationBanner.Title` * - `NotificationBanner.Heading` * - `NotificationBanner.Link` * @@ -51,7 +53,7 @@ export const StandardPanel: Story = { export const SuccessPanel: Story = { args: {}, render: (args) => ( - + Patient record updated

Contact{' '} @@ -85,3 +87,66 @@ export const StandardPanelWithCustomTitle: Story = { ), }; + +export const StandardPanelWithCustomTitleElement: Story = { + args: {}, + render: (args) => ( + + + Very Important Message + + Upcoming Maintenance +

The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025.

+
+ ), +}; + +export const StandardPanelWithList: Story = { + args: {}, + render: (args) => ( + + 4 files uploaded +
    +
  • + government-strategy.pdf +
  • +
  • + government-strategy-v2.pdf +
  • +
  • + government-strategy-v3-FINAL.pdf +
  • +
  • + government-strategy-v4-FINAL-v2.pdf +
  • +
+
+ ), +}; + +export const StandardPanelWithLotsOfContent: Story = { + args: {}, + render: (args) => ( + + + Check if you need to apply the reverse charge to this application + +

+ You will have to apply the reverse charge{' '} + if the applicant supplies any of these services: +

+
    +
  • + constructing, altering, repairing, extending, demolishing or dismantling buildings or + structures (whether permanent or not), including offshore installation services +
  • +
  • + constructing, altering, repairing, extending, demolishing of any works forming, or planned + to form, part of the land, including (in particular) walls, roadworks, power lines, + electronic communications equipment, aircraft runways, railways, inland waterways, docks + and harbours +
  • +
+
+ ), +}; From 5c38de7ea895191991c2c2ff9b123f9afe12b57a Mon Sep 17 00:00:00 2001 From: Rob Kerry Date: Fri, 10 Oct 2025 12:05:01 +0100 Subject: [PATCH 04/13] Address review comments (sonar) --- docs/upgrade-to-6.0.md | 1 + .../__tests__/NotificationBanner.test.tsx | 12 ++++++------ .../NotificationBanner.test.tsx.snap | 15 +++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index 381e5107..2155f519 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -453,6 +453,7 @@ To align with NHS.UK frontend, the skip link component focuses the main content ```html
+
``` For accessibility reasons, you must make the following changes: diff --git a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx index bd1d8289..115d6e4b 100644 --- a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx +++ b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx @@ -268,16 +268,16 @@ describe('NotificationBanner', () => { 4 files uploaded
  • - government-strategy.pdf + file.pdf
  • - government-strategy-v2.pdf + file-v2.pdf
  • - government-strategy-v3-FINAL.pdf + file-v3-FINAL.pdf
  • - government-strategy-v4-FINAL-v2.pdf + file-v4-FINAL-v2.pdf
, @@ -359,7 +359,7 @@ describe('NotificationBanner', () => { const [notificationBannerEl] = modules; expect(notificationBannerEl?.getAttribute('role')).toBe('region'); - expect(notificationBannerEl?.getAttribute('data-disable-auto-focus')).toBe(null); + expect(notificationBannerEl?.dataset?.disableAutoFocus).toBe(undefined); }); it('has alert role', async () => { @@ -385,6 +385,6 @@ describe('NotificationBanner', () => { const [notificationBannerEl] = modules; - expect(notificationBannerEl?.getAttribute('data-disable-auto-focus')).toBe('true'); + expect(notificationBannerEl?.dataset?.disableAutoFocus).toBe('true'); }); }); diff --git a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap index 28577703..8b1982fc 100644 --- a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap +++ b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap @@ -456,28 +456,28 @@ exports[`NotificationBanner matches snapshot with list (via server) 1`] = ` - government-strategy.pdf + file.pdf
  • - government-strategy-v2.pdf + file-v2.pdf
  • - government-strategy-v3-FINAL.pdf + file-v3-FINAL.pdf
  • - government-strategy-v4-FINAL-v2.pdf + file-v4-FINAL-v2.pdf
  • @@ -638,7 +638,9 @@ exports[`NotificationBanner matches snapshot with lots of content (via server) 1 Check if you need to apply the reverse charge to this application

    - You will have to apply the + You will have to apply the + + @@ -687,7 +689,8 @@ exports[`NotificationBanner matches snapshot with lots of content 1`] = ` Check if you need to apply the reverse charge to this application

    - You will have to apply the + You will have to apply the + From 480887773ba803e24f6bcd66682458055f12cc6b Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Mon, 13 Oct 2025 15:02:04 +0100 Subject: [PATCH 05/13] Move notification banner header bar to main component --- .../notification-banner/NotificationBanner.tsx | 12 +++++++++--- .../components/NotificationBannerTitle.tsx | 18 ++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index c02df791..a2384d05 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -40,7 +40,7 @@ const NotificationBannerComponent = forwardRef childIsOfComponentType(child, NotificationBannerTitle), - ) || {title}; + ); const nonTitleItems = items.filter( (child) => !childIsOfComponentType(child, NotificationBannerTitle), ); @@ -57,13 +57,19 @@ const NotificationBannerComponent = forwardRef - {titleElement} +

    {headerElement} {bodyItems} diff --git a/src/components/content-presentation/notification-banner/components/NotificationBannerTitle.tsx b/src/components/content-presentation/notification-banner/components/NotificationBannerTitle.tsx index c596e46d..7cf033d5 100644 --- a/src/components/content-presentation/notification-banner/components/NotificationBannerTitle.tsx +++ b/src/components/content-presentation/notification-banner/components/NotificationBannerTitle.tsx @@ -12,16 +12,14 @@ export const NotificationBannerTitle: FC = ({ success, ...rest }) => ( -
    - - {children || (success ? 'Success' : 'Important')} - -
    + + {children || (success ? 'Success' : 'Important')} + ); NotificationBannerTitle.displayName = 'NotificationBanner.Title'; From dcbd029517877deea548813400e3d81892aa8c2b Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Mon, 13 Oct 2025 15:10:04 +0100 Subject: [PATCH 06/13] Export notification banner child components --- src/__tests__/index.test.ts | 3 +++ .../notification-banner/NotificationBanner.tsx | 4 +++- .../content-presentation/notification-banner/index.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index d6e3badd..5eab14f0 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -92,6 +92,9 @@ describe('Index', () => { 'NavAZDisabledItem', 'NavAZLinkItem', 'NotificationBanner', + 'NotificationBannerHeading', + 'NotificationBannerLink', + 'NotificationBannerTitle', 'Pagination', 'PaginationLink', 'Panel', diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index a2384d05..1936eb2e 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -1,10 +1,12 @@ +'use client'; + import { Children, - type ComponentPropsWithoutRef, createRef, forwardRef, useEffect, useState, + type ComponentPropsWithoutRef, } from 'react'; import classNames from 'classnames'; import { diff --git a/src/components/content-presentation/notification-banner/index.ts b/src/components/content-presentation/notification-banner/index.ts index 9da940f0..4dfa21ab 100644 --- a/src/components/content-presentation/notification-banner/index.ts +++ b/src/components/content-presentation/notification-banner/index.ts @@ -1 +1,2 @@ +export * from './components/index.js'; export * from './NotificationBanner.js'; From 8f123833147d05dee4960bf084b39451d81c2332 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Mon, 13 Oct 2025 15:03:29 +0100 Subject: [PATCH 07/13] Allow NHS.UK frontend errors to be caught --- .../notification-banner/NotificationBanner.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index 1936eb2e..d1149826 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -25,6 +25,7 @@ export interface NotificationBannerProps extends ComponentPropsWithoutRef<'div'> const NotificationBannerComponent = forwardRef( ({ children, className, title, success, role, disableAutoFocus, ...rest }, forwardedRef) => { const [moduleRef] = useState(() => forwardedRef || createRef()); + const [instanceError, setInstanceError] = useState(); const [instance, setInstance] = useState(); useEffect(() => { @@ -32,11 +33,9 @@ const NotificationBannerComponent = forwardRef { - setInstance(new NotificationBanner($root)); - }); + import('nhsuk-frontend') + .then(({ NotificationBanner }) => setInstance(new NotificationBanner(moduleRef.current))) + .catch(setInstanceError); }, [moduleRef, instance]); const items = Children.toArray(children); @@ -52,6 +51,11 @@ const NotificationBannerComponent = forwardRef !childIsOfComponentType(child, NotificationBannerHeading), ); + + if (instanceError) { + throw instanceError; + } + return (
    Date: Mon, 13 Oct 2025 15:05:21 +0100 Subject: [PATCH 08/13] Add fallback `className` to component guards --- .../NotificationBanner.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index d1149826..0247da7f 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -39,19 +39,21 @@ const NotificationBannerComponent = forwardRef - childIsOfComponentType(child, NotificationBannerTitle), - ); - const nonTitleItems = items.filter( - (child) => !childIsOfComponentType(child, NotificationBannerTitle), - ); - const headerElement = nonTitleItems.find((child) => - childIsOfComponentType(child, NotificationBannerHeading), + childIsOfComponentType(child, NotificationBannerTitle, { + className: 'nhsuk-notification-banner__title', + }), ); - const bodyItems = nonTitleItems.filter( - (child) => !childIsOfComponentType(child, NotificationBannerHeading), + + const headerElement = items.find((child) => + childIsOfComponentType(child, NotificationBannerHeading, { + className: 'nhsuk-notification-banner__heading', + }), ); + const bodyItems = items.filter((child) => child !== titleElement && child !== headerElement); + if (instanceError) { throw instanceError; } From 1c43ecdcf9f95320dffaa792e91dc54b6d901d2e Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Mon, 13 Oct 2025 15:12:48 +0100 Subject: [PATCH 09/13] Reduce component filters --- .../notification-banner/NotificationBanner.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index 0247da7f..2e673aab 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -46,13 +46,7 @@ const NotificationBannerComponent = forwardRef - childIsOfComponentType(child, NotificationBannerHeading, { - className: 'nhsuk-notification-banner__heading', - }), - ); - - const bodyItems = items.filter((child) => child !== titleElement && child !== headerElement); + const contentItems = items.filter((child) => child !== titleElement); if (instanceError) { throw instanceError; @@ -79,8 +73,7 @@ const NotificationBannerComponent = forwardRef
    - {headerElement} - {bodyItems} + {contentItems}
    ); From 6ebc62801e5e53e6046b86b3201ffaea1e2a4162 Mon Sep 17 00:00:00 2001 From: Rob Kerry Date: Mon, 13 Oct 2025 15:43:02 +0100 Subject: [PATCH 10/13] Address further review comments --- docs/upgrade-to-6.0.md | 2 +- .../NotificationBanner.tsx | 12 +++-- .../__tests__/NotificationBanner.test.tsx | 20 ++++---- .../NotificationBanner.test.tsx.snap | 6 ++- .../components/NotificationBannerHeading.tsx | 2 + .../NotificationBanner.stories.tsx | 49 ++++++++++--------- 6 files changed, 51 insertions(+), 40 deletions(-) diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index 2155f519..c4d16572 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -37,7 +37,7 @@ The [notification banner](https://service-manual.nhs.uk/design-system/components ```jsx - Upcoming Maintenance + Upcoming maintenance

    The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025.

    ``` diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index 2e673aab..508b538b 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -20,10 +20,14 @@ import { childIsOfComponentType } from '#util/types/TypeGuards.js'; export interface NotificationBannerProps extends ComponentPropsWithoutRef<'div'> { success?: boolean; disableAutoFocus?: boolean; + titleId?: string; } const NotificationBannerComponent = forwardRef( - ({ children, className, title, success, role, disableAutoFocus, ...rest }, forwardedRef) => { + ( + { children, className, title, titleId, success, role, disableAutoFocus, ...rest }, + forwardedRef, + ) => { const [moduleRef] = useState(() => forwardedRef || createRef()); const [instanceError, setInstanceError] = useState(); const [instance, setInstance] = useState(); @@ -59,7 +63,7 @@ const NotificationBannerComponent = forwardRef{titleElement} ) : ( - {title} + + {title} + )}
    diff --git a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx index 115d6e4b..0be95909 100644 --- a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx +++ b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx @@ -30,7 +30,7 @@ describe('NotificationBanner', () => { it('matches snapshot custom title', async () => { const { container } = await renderClient( - + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. @@ -62,7 +62,7 @@ describe('NotificationBanner', () => { You have 7 days left to send your application.{' '} - View application. + View application. , { className: 'nhsuk-notification-banner' }, @@ -77,7 +77,7 @@ describe('NotificationBanner', () => { Patient record updated

    Contact{' '} - example@department.nhs.uk if + example@department.nhs.uk if you think there's a problem.

    , @@ -146,8 +146,8 @@ describe('NotificationBanner', () => {

    You will have to apply the{' '} - reverse charge if the applicant - supplies any of these services: + reverse charge if the + applicant supplies any of these services:

    • @@ -194,7 +194,7 @@ describe('NotificationBanner', () => { it('matches snapshot custom title (via server)', async () => { const { container } = await renderServer( - + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. @@ -226,7 +226,7 @@ describe('NotificationBanner', () => { You have 7 days left to send your application.{' '} - View application. + View application. , { className: 'nhsuk-notification-banner' }, @@ -241,7 +241,7 @@ describe('NotificationBanner', () => { Patient record updated

      Contact{' '} - example@department.nhs.uk if + example@department.nhs.uk if you think there's a problem.

      , @@ -310,8 +310,8 @@ describe('NotificationBanner', () => {

      You will have to apply the{' '} - reverse charge if the applicant - supplies any of these services: + reverse charge if the + applicant supplies any of these services:

      • diff --git a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap index 8b1982fc..2040b917 100644 --- a/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap +++ b/src/components/content-presentation/notification-banner/__tests__/__snapshots__/NotificationBanner.test.tsx.snap @@ -78,7 +78,7 @@ exports[`NotificationBanner matches snapshot custom title (via server) 1`] = ` class="nhsuk-notification-banner__title" id="nhsuk-notification-banner-title" > - Upcoming Maintenance + Upcoming maintenance
    - Upcoming Maintenance + Upcoming maintenance
    ); }, From 7eb6bf4403a0499530a901142f14c6477e86a261 Mon Sep 17 00:00:00 2001 From: Rob Kerry Date: Mon, 13 Oct 2025 16:46:11 +0100 Subject: [PATCH 12/13] Address further review comments --- .../notification-banner/NotificationBanner.tsx | 5 +++-- .../__tests__/NotificationBanner.test.tsx | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index a70c3dbe..0da757f6 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -47,6 +47,7 @@ const NotificationBannerComponent = forwardRef child !== titleElement); @@ -61,7 +62,7 @@ const NotificationBannerComponent = forwardRef{titleElement} ) : ( - + {title} )} diff --git a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx index 0be95909..1f984ab8 100644 --- a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx +++ b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx @@ -387,4 +387,20 @@ describe('NotificationBanner', () => { expect(notificationBannerEl?.dataset?.disableAutoFocus).toBe('true'); }); + + it('prioritises id of title element over provided title id', async () => { + const { modules } = await renderClient( + + Important information + + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. + + , + { className: 'nhsuk-notification-banner' }, + ); + + const [notificationBannerEl] = modules; + + expect(notificationBannerEl?.getAttribute('aria-labelledby')).toEqual('correct-id'); + }); }); From abc9b8f877bb5768003740e9cf8f951583c7189f Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Mon, 13 Oct 2025 16:54:42 +0100 Subject: [PATCH 13/13] Formatting --- .../notification-banner/NotificationBanner.tsx | 6 ++++-- .../__tests__/NotificationBanner.test.tsx | 10 +++++----- .../NotificationBanner.stories.tsx | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/content-presentation/notification-banner/NotificationBanner.tsx b/src/components/content-presentation/notification-banner/NotificationBanner.tsx index 0da757f6..33eb003f 100644 --- a/src/components/content-presentation/notification-banner/NotificationBanner.tsx +++ b/src/components/content-presentation/notification-banner/NotificationBanner.tsx @@ -26,6 +26,7 @@ export interface NotificationBannerProps extends ComponentPropsWithoutRef<'div'> const NotificationBannerComponent = forwardRef( (props, forwardedRef) => { const { children, className, title, titleId, success, role, disableAutoFocus, ...rest } = props; + const [moduleRef] = useState(() => forwardedRef || createRef()); const [instanceError, setInstanceError] = useState(); const [instance, setInstance] = useState(); @@ -47,6 +48,7 @@ const NotificationBannerComponent = forwardRef child !== titleElement); @@ -73,12 +75,12 @@ const NotificationBannerComponent = forwardRef{titleElement} ) : ( - + {title} )} -
    {contentItems}
    +
    {contentItems}
    ); }, diff --git a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx index 1f984ab8..537c5d08 100644 --- a/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx +++ b/src/components/content-presentation/notification-banner/__tests__/NotificationBanner.test.tsx @@ -30,7 +30,7 @@ describe('NotificationBanner', () => { it('matches snapshot custom title', async () => { const { container } = await renderClient( - + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. @@ -194,7 +194,7 @@ describe('NotificationBanner', () => { it('matches snapshot custom title (via server)', async () => { const { container } = await renderServer( - + The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. @@ -208,7 +208,7 @@ describe('NotificationBanner', () => { it('matches snapshot custom title html (via server)', async () => { const { container } = await renderServer( - + Very important information @@ -390,8 +390,8 @@ describe('NotificationBanner', () => { it('prioritises id of title element over provided title id', async () => { const { modules } = await renderClient( - - Important information + + Important information The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025. diff --git a/stories/Content Presentation/NotificationBanner.stories.tsx b/stories/Content Presentation/NotificationBanner.stories.tsx index 8e341b7f..d247ba9d 100644 --- a/stories/Content Presentation/NotificationBanner.stories.tsx +++ b/stories/Content Presentation/NotificationBanner.stories.tsx @@ -93,7 +93,7 @@ export const StandardPanelWithCustomTitleElement: Story = { render: () => ( - Maintenance + Maintenance Upcoming maintenance

    The service will be unavailable from 8pm to 9pm on Thursday 1 January 2025.