diff --git a/CHANGELOG.md b/CHANGELOG.md index 1573d819e..8fe1314da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # NHS.UK React components +## 6.0.0-beta.4 - 5 November 2025 + +This version provides support for NHS.UK frontend v10.1 and includes: + +- Support for HTML in legend, label and error props +- Default legend and label to `isPageHeading: true` when `headingLevel` is set + +For a full list of changes in this release please refer to the [migration doc](https://github.com/NHSDigital/nhsuk-react-components/blob/main/docs/upgrade-to-6.0.md). + ## 6.0.0-beta.3 - 27 October 2025 This version provides support for NHS.UK frontend v10.1 and includes: diff --git a/package.json b/package.json index 832f85d31..ca64d6e72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nhsuk-react-components", - "version": "6.0.0-beta.3", + "version": "6.0.0-beta.4", "license": "MIT", "author": { "name": "NHS England" diff --git a/src/components/form-elements/checkboxes/__tests__/Checkboxes.test.tsx b/src/components/form-elements/checkboxes/__tests__/Checkboxes.test.tsx index 3306460ab..5af15bb9d 100644 --- a/src/components/form-elements/checkboxes/__tests__/Checkboxes.test.tsx +++ b/src/components/form-elements/checkboxes/__tests__/Checkboxes.test.tsx @@ -7,7 +7,13 @@ import { renderClient, renderServer } from '#util/components'; describe('Checkboxes', () => { it('matches snapshot', async () => { const { container } = await renderClient( - + Waste from animal carcasses Waste from mines or quarries Farm or agricultural waste @@ -20,7 +26,59 @@ describe('Checkboxes', () => { it('matches snapshot with error message', async () => { const { container } = await renderClient( - + + Waste from animal carcasses + Waste from mines or quarries + Farm or agricultural waste + , + { moduleName: 'nhsuk-checkboxes' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with HTML in props', async () => { + const { container } = await renderClient( + + Example Legend text + + } + legendProps={{ + isPageHeading: true, + size: 'l', + }} + hint={ + <> + Hint text with HTML + + } + error={ + <> + Error text with HTML + + } + id="example" + name="example" + > + + Item hint text with HTML + + } + > + Waste from animal carcasses + Waste from animal carcasses Waste from mines or quarries Farm or agricultural waste @@ -33,7 +91,13 @@ describe('Checkboxes', () => { it('matches snapshot with an exclusive checkbox', async () => { const { container } = await renderClient( - + Waste from animal carcasses Waste from mines or quarries Farm or agricultural waste @@ -50,7 +114,13 @@ describe('Checkboxes', () => { it('matches snapshot (via server)', async () => { const { container, element } = await renderServer( - + Waste from animal carcasses Waste from mines or quarries Farm or agricultural waste diff --git a/src/components/form-elements/checkboxes/__tests__/__snapshots__/Checkboxes.test.tsx.snap b/src/components/form-elements/checkboxes/__tests__/__snapshots__/Checkboxes.test.tsx.snap index abc0c8d32..e807fe801 100644 --- a/src/components/form-elements/checkboxes/__tests__/__snapshots__/Checkboxes.test.tsx.snap +++ b/src/components/form-elements/checkboxes/__tests__/__snapshots__/Checkboxes.test.tsx.snap @@ -5,70 +5,86 @@ exports[`Checkboxes matches snapshot (via server): client 1`] = `
-
-
- - -
+ What types of waste do you transport regularly? +
- - + Select all that apply
- - + + +
+
+ + +
+
+ + +
-
+ `; @@ -78,69 +94,85 @@ exports[`Checkboxes matches snapshot (via server): server 1`] = `
-
-
- - -
+ What types of waste do you transport regularly? +
- - + Select all that apply
- -
+
- Farm or agricultural waste - + + +
+
+ + +
-
+ `; @@ -150,168 +182,345 @@ exports[`Checkboxes matches snapshot 1`] = `
-
-
- - -
+ What types of waste do you transport regularly? +
- - + Select all that apply
- -
+
+ + +
+
- Farm or agricultural waste - + + +
-
+ `; -exports[`Checkboxes matches snapshot with an exclusive checkbox 1`] = ` +exports[`Checkboxes matches snapshot with HTML in props 1`] = `
-
-
- - -
+ + Example + + Legend text + +
- - + Hint text + + with HTML +
+ + + Error: + + + Error text + + with HTML + +
- - + + +
+ Item hint text + + with HTML + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+`; + +exports[`Checkboxes matches snapshot with an exclusive checkbox 1`] = ` +
+
+
+ + What types of waste do you transport regularly? +
- or + Select all that apply
- - + + +
+
+ + +
+
+ + +
+
+ or +
+
+ + +
-
+ `; @@ -321,83 +530,98 @@ exports[`Checkboxes matches snapshot with error message 1`] = `
- - - Error: - - - Example error - -
+ What types of waste do you transport regularly? +
- - + Select all that apply
-
- - -
+ Error: + + + Example error +
- -
+
+ + +
+
- Farm or agricultural waste - + + +
-
+ `; diff --git a/src/components/form-elements/date-input/DateInputContext.ts b/src/components/form-elements/date-input/DateInputContext.ts index a520a1083..bce349a21 100644 --- a/src/components/form-elements/date-input/DateInputContext.ts +++ b/src/components/form-elements/date-input/DateInputContext.ts @@ -1,11 +1,11 @@ 'use client'; -import { createContext, type ChangeEvent } from 'react'; +import { createContext, type ChangeEvent, type ReactElement } from 'react'; export type IDateInputContext = { id: string; name: string; - error: string | undefined; + error: string | ReactElement | undefined; value?: { day?: string; month?: string; year?: string }; defaultValue?: { day?: string; month?: string; year?: string }; handleChange: (inputType: 'day' | 'month' | 'year', event: ChangeEvent) => void; diff --git a/src/components/form-elements/date-input/__tests__/DateInput.test.tsx b/src/components/form-elements/date-input/__tests__/DateInput.test.tsx index 60b32365a..23b76990d 100644 --- a/src/components/form-elements/date-input/__tests__/DateInput.test.tsx +++ b/src/components/form-elements/date-input/__tests__/DateInput.test.tsx @@ -9,9 +9,9 @@ describe('DateInput', () => { it('matches snapshot', async () => { const { container } = await renderClient( , { className: 'nhsuk-date-input' }, @@ -23,11 +23,41 @@ describe('DateInput', () => { it('matches snapshot with error message', async () => { const { container } = await renderClient( , + { className: 'nhsuk-date-input' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with HTML in props', async () => { + const { container } = await renderClient( + + Example Legend text + + } + legendProps={{ + isPageHeading: true, + size: 'l', + }} + hint={ + <> + Hint text with HTML + + } + error={ + <> + Error text with HTML + + } + id="date-input" />, { className: 'nhsuk-date-input' }, ); @@ -38,9 +68,9 @@ describe('DateInput', () => { it('matches snapshot with custom date fields', async () => { const { container } = await renderClient( @@ -56,11 +86,11 @@ describe('DateInput', () => { it('matches snapshot with custom date fields and error message', async () => { const { container } = await renderClient( @@ -75,9 +105,9 @@ describe('DateInput', () => { it('matches snapshot (via server)', async () => { const { container, element } = await renderServer( , { className: 'nhsuk-date-input' }, @@ -105,9 +135,9 @@ describe('DateInput', () => { const { container, modules } = await renderClient( diff --git a/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap b/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap index 6a871bd99..84617216f 100644 --- a/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap +++ b/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap @@ -291,6 +291,130 @@ exports[`DateInput matches snapshot 1`] = ` `; +exports[`DateInput matches snapshot with HTML in props 1`] = ` +
+
+
+ +

+ + Example + + Legend text +

+
+
+ Hint text + + with HTML + +
+ + + Error: + + + Error text + + with HTML + + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+`; + exports[`DateInput matches snapshot with custom date fields 1`] = `
, Pick { - error?: string | false; + error?: string | ReactElement | false; inputType: 'day' | 'month' | 'year'; } diff --git a/src/components/form-elements/error-message/ErrorMessage.tsx b/src/components/form-elements/error-message/ErrorMessage.tsx index 1ea1cb20b..14f767443 100644 --- a/src/components/form-elements/error-message/ErrorMessage.tsx +++ b/src/components/form-elements/error-message/ErrorMessage.tsx @@ -11,7 +11,7 @@ export const ErrorMessage: FC = ({ children, ...rest }) => { - if (!children || typeof children !== 'string') { + if (!children) { return null; } diff --git a/src/components/form-elements/label/Label.tsx b/src/components/form-elements/label/Label.tsx index 12b6a23e5..7f964dc1b 100644 --- a/src/components/form-elements/label/Label.tsx +++ b/src/components/form-elements/label/Label.tsx @@ -1,9 +1,12 @@ import classNames from 'classnames'; import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; import { type NHSUKSize } from '#util/types/NHSUKTypes.js'; -export interface LabelProps extends ComponentPropsWithoutRef<'label'> { +export interface LabelProps + extends ComponentPropsWithoutRef<'label'>, + Pick { isPageHeading?: boolean; size?: NHSUKSize; } @@ -16,16 +19,18 @@ const LabelComponent: FC> = ({ className, size /> ); -export const Label: FC = ({ isPageHeading, children, ...rest }) => { +export const Label: FC = (props) => { + const { children, isPageHeading, headingLevel = 'h1', ...rest } = props; + if (!children) { return null; } - if (isPageHeading) { + if (isPageHeading || props.headingLevel) { return ( -

+ {children} -

+ ); } diff --git a/src/components/form-elements/label/__tests__/Label.test.tsx b/src/components/form-elements/label/__tests__/Label.test.tsx index baee8c9a6..914dd0b5d 100644 --- a/src/components/form-elements/label/__tests__/Label.test.tsx +++ b/src/components/form-elements/label/__tests__/Label.test.tsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react'; -import { Label } from '..'; +import { Label, type LabelProps } from '..'; import { type NHSUKSize } from '#util/types'; @@ -50,6 +50,21 @@ describe('Label', () => { }, ); + it.each([ + { headingLevel: 'h1' }, + { headingLevel: 'h2' }, + { headingLevel: 'h3' }, + { headingLevel: 'h4' }, + ])('renders as page heading with custom heading level $headingLevel', (props) => { + const { container } = render(); + + const headingEl = container.querySelector('.nhsuk-label-wrapper'); + const labelEl = headingEl?.querySelector('.nhsuk-label'); + + expect(headingEl?.tagName).toBe(props?.headingLevel?.toUpperCase()); + expect(labelEl).toHaveTextContent('Text'); + }); + it('renders null with no children', () => { const { container } = render(
`; @@ -57,48 +73,64 @@ exports[`Radios matches snapshot (via server): server 1`] = `
-
+ + Have you changed your name? +
- - + This includes changing your last name or spelling your name differently
- -
+
- No - + + +
-
+ `; @@ -108,49 +140,160 @@ exports[`Radios matches snapshot 1`] = `
-
+ + Have you changed your name? +
- -
+
+`; + +exports[`Radios matches snapshot with HTML in props 1`] = ` +
+
+
+ +

+ + Example + + Legend text +

+
- -
+ + + Error: + + + Error text + + with HTML + + +
+
+ + +
+
- No - + + +
-
+
`; diff --git a/src/components/form-elements/text-input/__tests__/TextInput.test.tsx b/src/components/form-elements/text-input/__tests__/TextInput.test.tsx index 373dff485..450573e68 100644 --- a/src/components/form-elements/text-input/__tests__/TextInput.test.tsx +++ b/src/components/form-elements/text-input/__tests__/TextInput.test.tsx @@ -20,6 +20,36 @@ describe('TextInput', () => { expect(container).toMatchSnapshot('TextInput'); }); + it('matches snapshot with HTML in props', async () => { + const { container } = await renderClient( + + Example Label text + + } + labelProps={{ + isPageHeading: true, + size: 'l', + }} + hint={ + <> + Hint text with HTML + + } + error={ + <> + Error text with HTML + + } + id="nhs-number" + />, + { className: 'nhsuk-input' }, + ); + + expect(container).toMatchSnapshot(); + }); + it('matches snapshot (via server)', async () => { const { container, element } = await renderServer( , diff --git a/src/components/form-elements/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/form-elements/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap index a7c63816a..fe32e07ab 100644 --- a/src/components/form-elements/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/components/form-elements/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -44,6 +44,62 @@ exports[`TextInput matches snapshot (via server): server 1`] = ` `; +exports[`TextInput matches snapshot with HTML in props 1`] = ` +
+
+

+ +

+
+ Hint text + + with HTML + +
+ + + Error: + + + Error text + + with HTML + + + +
+
+`; + exports[`TextInput matches snapshot: TextInput 1`] = `
; 'id'?: string; diff --git a/stories/Form Elements/Checkboxes.stories.tsx b/stories/Form Elements/Checkboxes.stories.tsx index 0bfdf4729..6386dc6ec 100644 --- a/stories/Form Elements/Checkboxes.stories.tsx +++ b/stories/Form Elements/Checkboxes.stories.tsx @@ -43,6 +43,23 @@ export const Standard: Story = { ), }; +export const WithCaption: Story = { + args: { + legend: ( + <> + About you What is your nationality? + + ), + }, + render: (args) => ( + + British + Irish + Citizen of another country + + ), +}; + export const WithHintText: Story = { args: { legend: 'How do you want to sign in?', diff --git a/stories/Form Elements/Radios.stories.tsx b/stories/Form Elements/Radios.stories.tsx index 12a35ccf2..f9f817322 100644 --- a/stories/Form Elements/Radios.stories.tsx +++ b/stories/Form Elements/Radios.stories.tsx @@ -46,6 +46,24 @@ export const InlineRadios: Story = { ), }; +export const RadiosWithCaption: Story = { + args: { + legend: ( + <> + About you Have you changed your name? + + ), + }, + render: (args) => ( + + Yes + + No + + + ), +}; + export const RadiosWithConditionalContent: Story = { args: { legend: 'Impairment requirement',