diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8a3fef2..7f30cc86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,59 +2,65 @@
## Unreleased
+This version updates nhsuk-frontend to version 9.
+
+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-5.0.md).
+
+## 4.1.3 - 23 September 2024
+
:wrench: **Fixes**
-* Remove the unnecessary aria-labelledby tags from radio items. PR [#253](https://github.com/NHSDigital/nhsuk-react-components/pull/253)
+- Remove the unnecessary aria-labelledby tags from radio items. PR [#253](https://github.com/NHSDigital/nhsuk-react-components/pull/253)
## 4.1.2 - 3 September 2024
:wrench: **Fixes**
-* Fix issues with SkipLink (always set the href) and bring into line with NHSUK frontend. PR [#248](https://github.com/NHSDigital/nhsuk-react-components/pull/248)
+- Fix issues with SkipLink (always set the href) and bring into line with NHSUK frontend. PR [#248](https://github.com/NHSDigital/nhsuk-react-components/pull/248)
## 4.1.1 - 9 August 2024
:wrench: **Fixes**
-* Remove the unnecessary aria-labelledby tags from DateInput fields. PR [#246](https://github.com/NHSDigital/nhsuk-react-components/pull/246)
+- Remove the unnecessary aria-labelledby tags from DateInput fields. PR [#246](https://github.com/NHSDigital/nhsuk-react-components/pull/246)
## 4.1.0 - 11 June 2024
:wrench: **Fixes**
-* Add js shims for buttons. PR [#231](https://github.com/NHSDigital/nhsuk-react-components/pull/231), Fixes [#218](https://github.com/NHSDigital/nhsuk-react-components/issues/218)
-* Fix errors not being linked to inputs. PR [#230](https://github.com/NHSDigital/nhsuk-react-components/pull/230), Fixes [#227](https://github.com/NHSDigital/nhsuk-react-components/issues/227)
-* Fix inputs incorrectly using `aria-labelledby`. PR [#230](https://github.com/NHSDigital/nhsuk-react-components/pull/230), Fixes [#212](https://github.com/NHSDigital/nhsuk-react-components/issues/212)
-* Update Storybook docs for several components.
+- Add js shims for buttons. PR [#231](https://github.com/NHSDigital/nhsuk-react-components/pull/231), Fixes [#218](https://github.com/NHSDigital/nhsuk-react-components/issues/218)
+- Fix errors not being linked to inputs. PR [#230](https://github.com/NHSDigital/nhsuk-react-components/pull/230), Fixes [#227](https://github.com/NHSDigital/nhsuk-react-components/issues/227)
+- Fix inputs incorrectly using `aria-labelledby`. PR [#230](https://github.com/NHSDigital/nhsuk-react-components/pull/230), Fixes [#212](https://github.com/NHSDigital/nhsuk-react-components/issues/212)
+- Update Storybook docs for several components.
:new: **New features**
-* Added a CHANGELOG to keep track of changes between releases. [Keep a changelog](https://keepachangelog.com)
-* Added support for `preventDoubleClick` debouncing on buttons. PR [#231](https://github.com/NHSDigital/nhsuk-react-components/pull/231)
-* Error summaries now automatically set role, tabindex, and aria-labelledby. PR [#229](https://github.com/NHSDigital/nhsuk-react-components/pull/237), Fixes [#228](https://github.com/NHSDigital/nhsuk-react-components/issues/229)
-* Storybook link in readme now points to latest version. PR [#226](https://github.com/NHSDigital/nhsuk-react-components/pull/226)
+- Added a CHANGELOG to keep track of changes between releases. [Keep a changelog](https://keepachangelog.com)
+- Added support for `preventDoubleClick` debouncing on buttons. PR [#231](https://github.com/NHSDigital/nhsuk-react-components/pull/231)
+- Error summaries now automatically set role, tabindex, and aria-labelledby. PR [#229](https://github.com/NHSDigital/nhsuk-react-components/pull/237), Fixes [#228](https://github.com/NHSDigital/nhsuk-react-components/issues/229)
+- Storybook link in readme now points to latest version. PR [#226](https://github.com/NHSDigital/nhsuk-react-components/pull/226)
## 4.0.2 - 21 May 2024
:wrench: **Fixes**
-* Fix error message role by @edwardhorsford in [#219](https://github.com/NHSDigital/nhsuk-react-components/pull/219)
+- Fix error message role by @edwardhorsford in [#219](https://github.com/NHSDigital/nhsuk-react-components/pull/219)
## 4.0.1 - 20 May 2024
:wrench: **Fixes**
-* Fix issue with the footer copyright not being rendered in the correct location if there are multiple link columns by @jakeb-nhs in [#223](https://github.com/NHSDigital/nhsuk-react-components/pull/223)
+- Fix issue with the footer copyright not being rendered in the correct location if there are multiple link columns by @jakeb-nhs in [#223](https://github.com/NHSDigital/nhsuk-react-components/pull/223)
## 4.0.0 - 15 May 2024
This version updates nhsuk-frontend to version 8.
-For a full list of changes in this release please refer to the [migration doc](https://github.com/NHSDigital/nhsuk-react-components/blob/feature/nhsuk-frontend-v8/docs/upgrade-to-4.0.md).
+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-4.0.md).
-* Migrate enzyme to react-testing-library by @JoshuaBates-NHS in [#198](https://github.com/NHSDigital/nhsuk-react-components/pull/198)
-* Allow support for module directives in build process by @JoshuaBates-NHS in [#199](https://github.com/NHSDigital/nhsuk-react-components/pull/199)
-* Update modified components since NHS UK frontend v5 by @jakeb-nhs in [#197](https://github.com/NHSDigital/nhsuk-react-components/pull/197)
-* Add new components since NHS UK frontend v5 by @jakeb-nhs in [#202](https://github.com/NHSDigital/nhsuk-react-components/pull/202)
-* Migrate some patterns to components, rework removed components from frontend v8 by @jakeb-nhs in [#203](https://github.com/NHSDigital/nhsuk-react-components/pull/203)
-* Improve unit test coverage by @jakeb-nhs in [#204](https://github.com/NHSDigital/nhsuk-react-components/pull/204)
+- Migrate enzyme to react-testing-library by @JoshuaBates-NHS in [#198](https://github.com/NHSDigital/nhsuk-react-components/pull/198)
+- Allow support for module directives in build process by @JoshuaBates-NHS in [#199](https://github.com/NHSDigital/nhsuk-react-components/pull/199)
+- Update modified components since NHS UK frontend v5 by @jakeb-nhs in [#197](https://github.com/NHSDigital/nhsuk-react-components/pull/197)
+- Add new components since NHS UK frontend v5 by @jakeb-nhs in [#202](https://github.com/NHSDigital/nhsuk-react-components/pull/202)
+- Migrate some patterns to components, rework removed components from frontend v8 by @jakeb-nhs in [#203](https://github.com/NHSDigital/nhsuk-react-components/pull/203)
+- Improve unit test coverage by @jakeb-nhs in [#204](https://github.com/NHSDigital/nhsuk-react-components/pull/204)
diff --git a/README.md b/README.md
index 4dd88788..8d29ceec 100644
--- a/README.md
+++ b/README.md
@@ -38,10 +38,11 @@ class GetStartedButton extends PureComponent {
## Upgrading
-* [Upgrading to 1.0](/docs/upgrade-to-1.0.md)
-* [Upgrading to 2.0](/docs/upgrade-to-2.0.md)
-* [Upgrading to 3.0](/docs/upgrade-to-3.0.md)
-* [Upgrading to 4.0](/docs/upgrade-to-4.0.md)
+- [Upgrading to 1.0](/docs/upgrade-to-1.0.md)
+- [Upgrading to 2.0](/docs/upgrade-to-2.0.md)
+- [Upgrading to 3.0](/docs/upgrade-to-3.0.md)
+- [Upgrading to 4.0](/docs/upgrade-to-4.0.md)
+- [Upgrading to 5.0](/docs/upgrade-to-5.0.md)
## Maintainers
diff --git a/docs/upgrade-to-5.0.md b/docs/upgrade-to-5.0.md
new file mode 100644
index 00000000..a7654702
--- /dev/null
+++ b/docs/upgrade-to-5.0.md
@@ -0,0 +1,42 @@
+# Upgrading to 5.0
+
+## Breaking changes
+
+## New Features
+
+### FormGroup
+
+In order to provide consumers of this library more control over when to render fieldsets, some behaviour of the previous `Fieldset` component has been extracted into a new `FormGroup` component. This makes `FormGroup` responsible for rendering the error decorator line for groups of inputs, and `Fieldset` is now a simpler component which makes use of this component.
+
+No changes are required for existing usages of `Fieldset`. For examples of the usage of `FormGroup`, please see storybook.
+
+For example, this:
+
+```
+
+ What is your address?
+
+
+
+```
+
+Would become this:
+
+```
+
+
+ What is your address?
+
+
+
+
+```
+
+Which also allows consumers to omit the `Fieldset` if rendering a single input:
+
+```
+
+ What is your address?
+
+
+```
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
index 4b340050..80446756 100644
--- a/src/__tests__/index.test.ts
+++ b/src/__tests__/index.test.ts
@@ -36,6 +36,7 @@ describe('Index', () => {
'Fieldset',
'Footer',
'Form',
+ 'FormGroup',
'Header',
'Hero',
'HintText',
diff --git a/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx b/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx
index 38191e5d..28c037a2 100644
--- a/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx
+++ b/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx
@@ -2,7 +2,7 @@
import React, { FC, HTMLProps, createContext, useContext, ReactNode } from 'react';
import classNames from 'classnames';
import { Tick, Cross } from '@components/content-presentation/icons';
-import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
+import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel';
type ListType = 'do' | 'dont';
diff --git a/src/components/content-presentation/hero/Hero.tsx b/src/components/content-presentation/hero/Hero.tsx
index 15999056..09a60434 100644
--- a/src/components/content-presentation/hero/Hero.tsx
+++ b/src/components/content-presentation/hero/Hero.tsx
@@ -1,7 +1,7 @@
import React, { FC, HTMLProps } from 'react';
import classNames from 'classnames';
import { Container, Row, Col } from '../../layout';
-import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
+import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel';
interface HeroContentProps extends HTMLProps {
hasImage: boolean;
diff --git a/src/components/content-presentation/table/components/TablePanel.tsx b/src/components/content-presentation/table/components/TablePanel.tsx
index 7b60290b..7542a14e 100644
--- a/src/components/content-presentation/table/components/TablePanel.tsx
+++ b/src/components/content-presentation/table/components/TablePanel.tsx
@@ -1,6 +1,6 @@
import React, { FC, ComponentProps, HTMLProps } from 'react';
import classNames from 'classnames';
-import HeadingLevel from '@util/HeadingLevel';
+import HeadingLevel from '@components/utils/HeadingLevel';
export interface TablePanelProps extends HTMLProps {
heading?: string;
diff --git a/src/components/content-presentation/tabs/Tabs.tsx b/src/components/content-presentation/tabs/Tabs.tsx
index db921a0c..edfcf250 100644
--- a/src/components/content-presentation/tabs/Tabs.tsx
+++ b/src/components/content-presentation/tabs/Tabs.tsx
@@ -1,7 +1,7 @@
'use client';
import classNames from 'classnames';
import React, { FC, HTMLAttributes, useEffect } from 'react';
-import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
+import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel';
import TabsJs from '@resources/tabs';
type TabsProps = HTMLAttributes;
diff --git a/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx b/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx
index 33e5ee9c..7f332f2a 100644
--- a/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx
+++ b/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import Tabs from '../Tabs';
-import { HeadingLevelType } from '@util/HeadingLevel';
+import { HeadingLevelType } from '@components/utils/HeadingLevel';
describe('The tabs component', () => {
it('Matches the snapshot', () => {
diff --git a/src/components/content-presentation/warning-callout/WarningCallout.tsx b/src/components/content-presentation/warning-callout/WarningCallout.tsx
index d63f5283..e908c6ee 100644
--- a/src/components/content-presentation/warning-callout/WarningCallout.tsx
+++ b/src/components/content-presentation/warning-callout/WarningCallout.tsx
@@ -1,6 +1,6 @@
import React, { FC, HTMLProps } from 'react';
import classNames from 'classnames';
-import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
+import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel';
interface WarningCalloutLabelProps extends HTMLProps {
headingLevel?: HeadingLevelType;
diff --git a/src/components/form-elements/checkboxes/Checkboxes.tsx b/src/components/form-elements/checkboxes/Checkboxes.tsx
index b2b1083e..4b10e37b 100644
--- a/src/components/form-elements/checkboxes/Checkboxes.tsx
+++ b/src/components/form-elements/checkboxes/Checkboxes.tsx
@@ -3,7 +3,7 @@
import React, { HTMLProps, useEffect } from 'react';
import classNames from 'classnames';
import { FormElementProps } from '@util/types/FormTypes';
-import FormGroup from '@util/FormGroup';
+import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
import CheckboxContext, { ICheckboxContext } from './CheckboxContext';
import Box from './components/Box';
import Divider from './components/Divider';
@@ -53,7 +53,7 @@ const Checkboxes = ({ children, idPrefix, ...rest }: CheckboxesProps) => {
};
return (
- inputType="checkboxes" {...rest}>
+ inputType="checkboxes" {...rest}>
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
{({ className, name, id, idPrefix, error, ...restRenderProps }) => {
resetCheckboxIds();
@@ -69,7 +69,7 @@ const Checkboxes = ({ children, idPrefix, ...rest }: CheckboxesProps) => {
);
}}
-
+
);
};
diff --git a/src/components/form-elements/date-input/DateInput.tsx b/src/components/form-elements/date-input/DateInput.tsx
index a4f8adb3..50f2040b 100644
--- a/src/components/form-elements/date-input/DateInput.tsx
+++ b/src/components/form-elements/date-input/DateInput.tsx
@@ -3,7 +3,7 @@
import React, { HTMLProps, ChangeEvent, useEffect, useState } from 'react';
import classNames from 'classnames';
import { DayInput, MonthInput, YearInput } from './components/IndividualDateInputs';
-import FormGroup from '@util/FormGroup';
+import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
import DateInputContext, { IDateInputContext } from './DateInputContext';
import { FormElementProps } from '@util/types/FormTypes';
@@ -90,7 +90,10 @@ const DateInput = ({
};
return (
- > inputType="dateinput" {...rest}>
+ >
+ inputType="dateinput"
+ {...rest}
+ >
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
{({ className, name, id, error, autoSelectNext, ...restRenderProps }) => {
const contextValue: IDateInputContext = {
@@ -116,7 +119,7 @@ const DateInput = ({
);
}}
-
+
);
};
diff --git a/src/components/form-elements/fieldset/Fieldset.tsx b/src/components/form-elements/fieldset/Fieldset.tsx
index 2451c40a..6107096e 100644
--- a/src/components/form-elements/fieldset/Fieldset.tsx
+++ b/src/components/form-elements/fieldset/Fieldset.tsx
@@ -1,8 +1,8 @@
-import React, { FC, HTMLProps, MutableRefObject, useMemo, useState } from 'react';
+import React, { FC, HTMLProps, MutableRefObject } from 'react';
import classNames from 'classnames';
import { NHSUKSize } from '@util/types/NHSUKTypes';
-import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
-import FieldsetContext, { IFieldsetContext } from './FieldsetContext';
+import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel';
+import FormGroup from '@components/utils/FormGroup';
interface LegendProps extends Omit, 'size'> {
isPageHeading?: boolean;
@@ -41,63 +41,14 @@ const Legend: FC = ({
interface FieldsetProps extends HTMLProps {
fieldsetRef?: MutableRefObject;
- disableErrorLine?: boolean;
}
-const FieldSet = ({ className, disableErrorLine, fieldsetRef, ...rest }: FieldsetProps) => {
- const [registeredComponents, setRegisteredComponents] = useState([]);
- const [erroredComponents, setErroredComponents] = useState([]);
-
- const passError = (componentId: string, error: boolean): void => {
- const existingError = erroredComponents.includes(componentId);
- if (existingError && !error) {
- setErroredComponents(erroredComponents.filter((id) => id !== componentId));
- return;
- }
- if (!existingError && error) {
- setErroredComponents([...erroredComponents, componentId]);
- }
- };
-
- const registerComponent = (componentId: string, deregister = false): void => {
- let newComponents = [...registeredComponents];
- if (deregister) {
- newComponents = newComponents.filter((id) => id !== componentId);
- } else if (!registeredComponents.includes(componentId)) {
- newComponents = [...newComponents, componentId];
- }
- setRegisteredComponents(newComponents);
- };
-
- const contextValue: IFieldsetContext = useMemo(() => {
- return {
- isFieldset: true,
- registerComponent: registerComponent,
- passError: passError,
- };
- }, [registerComponent, passError]);
-
- const containsFormElements = registeredComponents.length > 0;
- const containsError = erroredComponents.length > 0;
-
+const FieldSet = ({ className, fieldsetRef, ...rest }: FieldsetProps) => {
+ rest.children;
return (
-
- {containsFormElements ? (
-
-
-
- ) : (
-
- )}
-
+
+
+
);
};
diff --git a/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx b/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx
index c71ada1f..ed69c322 100644
--- a/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx
+++ b/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx
@@ -5,7 +5,11 @@ import TextInput from '@components/form-elements/text-input';
describe('Fieldset', () => {
it('matches snapshot', () => {
- const { container } = render(Text );
+ const { container } = render(
+
+
+ ,
+ );
expect(container).toMatchSnapshot('Fieldset');
});
diff --git a/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap b/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap
index e3e9f750..347741cd 100644
--- a/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap
+++ b/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap
@@ -12,10 +12,23 @@ exports[`Fieldset Fieldset.Legend matches snapshot: FieldsetLegend 1`] = `
exports[`Fieldset matches snapshot: Fieldset 1`] = `
`;
diff --git a/src/components/form-elements/radios/Radios.tsx b/src/components/form-elements/radios/Radios.tsx
index b8bf2aa3..a2e59370 100644
--- a/src/components/form-elements/radios/Radios.tsx
+++ b/src/components/form-elements/radios/Radios.tsx
@@ -2,7 +2,7 @@ import React, { HTMLProps, useState } from 'react';
import classNames from 'classnames';
import { FormElementProps } from '@util/types/FormTypes';
import { RadiosContext, IRadiosContext } from './RadioContext';
-import FormGroup from '@util/FormGroup';
+import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
import Divider from './components/Divider';
import Radio from './components/Radio';
import { generateRandomName } from '@util/RandomID';
@@ -53,7 +53,7 @@ const Radios = ({ children, idPrefix, ...rest }: RadiosProps) => {
};
return (
- inputType="radios" {...rest}>
+ inputType="radios" {...rest}>
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
{({ className, inline, name, id, error, ...restRenderProps }) => {
resetRadioIds();
@@ -76,7 +76,7 @@ const Radios = ({ children, idPrefix, ...rest }: RadiosProps) => {
);
}}
-
+
);
};
diff --git a/src/components/form-elements/select/Select.tsx b/src/components/form-elements/select/Select.tsx
index 38ad6af7..daac61e5 100644
--- a/src/components/form-elements/select/Select.tsx
+++ b/src/components/form-elements/select/Select.tsx
@@ -2,7 +2,7 @@ import React, { FC, HTMLProps, MutableRefObject } from 'react';
import classNames from 'classnames';
import { FormElementProps } from '@util/types/FormTypes';
-import FormGroup from '@util/FormGroup';
+import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
// SelectProps = HTMLProps & FormElementProps;
interface ISelectProps extends HTMLProps, FormElementProps {
@@ -14,7 +14,7 @@ interface ISelect extends FC {
}
const Select: ISelect = ({ children, ...rest }) => (
- inputType="select" {...rest}>
+ inputType="select" {...rest}>
{({ className, error, selectRef, ...restRenderProps }) => (
(
{children}
)}
-
+
);
const Option: FC> = (props) => ;
diff --git a/src/components/form-elements/text-input/TextInput.tsx b/src/components/form-elements/text-input/TextInput.tsx
index 2c78bcd9..a95a8f83 100644
--- a/src/components/form-elements/text-input/TextInput.tsx
+++ b/src/components/form-elements/text-input/TextInput.tsx
@@ -1,6 +1,6 @@
import React, { FC, HTMLProps, MutableRefObject } from 'react';
import classNames from 'classnames';
-import FormGroup from '@util/FormGroup';
+import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
import { InputWidth } from '@util/types/NHSUKTypes';
import { FormElementProps } from '@util/types/FormTypes';
@@ -25,7 +25,7 @@ const TextInputSuffix: FC<{ suffix: string }> = ({ suffix }) => (
);
const TextInput: FC = (props) => (
- {...props} inputType="input">
+ {...props} inputType="input">
{({ width, className, error, inputRef, type = 'text', prefix, suffix, ...rest }) => {
const Input = (
= (props) => (
Input
);
}}
-
+
);
export default TextInput;
diff --git a/src/components/form-elements/textarea/Textarea.tsx b/src/components/form-elements/textarea/Textarea.tsx
index 08b94d5a..5adfa122 100644
--- a/src/components/form-elements/textarea/Textarea.tsx
+++ b/src/components/form-elements/textarea/Textarea.tsx
@@ -1,13 +1,13 @@
import React, { FC, HTMLProps, MutableRefObject } from 'react';
import classNames from 'classnames';
import { FormElementProps } from '@util/types/FormTypes';
-import FormGroup from '@util/FormGroup';
+import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
type TextareaProps = HTMLProps &
FormElementProps & { textareaRef?: MutableRefObject };
const Textarea: FC = ({ children, ...props }) => (
- inputType="textarea" {...props}>
+ inputType="textarea" {...props}>
{({ className, error, textareaRef, ...rest }) => (
)}
-
+
);
export default Textarea;
diff --git a/src/components/navigation/card/components/CardHeading.tsx b/src/components/navigation/card/components/CardHeading.tsx
index 7c773e25..9ce01d7a 100644
--- a/src/components/navigation/card/components/CardHeading.tsx
+++ b/src/components/navigation/card/components/CardHeading.tsx
@@ -1,7 +1,7 @@
'use client';
import React, { FC, HTMLProps, useContext } from 'react';
import classNames from 'classnames';
-import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
+import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel';
import CardContext from '../CardContext';
import { CareCardType } from '../../../../util/types/NHSUKTypes';
import { cardTypeIsCareCard } from '@util/types/TypeGuards';
diff --git a/src/components/utils/FormGroup.tsx b/src/components/utils/FormGroup.tsx
new file mode 100644
index 00000000..d61bcc3c
--- /dev/null
+++ b/src/components/utils/FormGroup.tsx
@@ -0,0 +1,65 @@
+import React, { HTMLProps, useMemo, useState } from 'react';
+import classNames from 'classnames';
+import FormGroupContext, { IFormGroupContext } from './FormGroupContext';
+
+interface FormGroupProps extends HTMLProps {
+ enableErrorLine?: boolean;
+}
+
+const FormGroup: React.FC = ({
+ enableErrorLine = false,
+ className,
+ children,
+ ...rest
+}: FormGroupProps) => {
+ const [registeredComponents, setRegisteredComponents] = useState([]);
+ const [erroredComponents, setErroredComponents] = useState([]);
+
+ const passError = (componentId: string, error: boolean): void => {
+ const existingError = erroredComponents.includes(componentId);
+ if (existingError && !error) {
+ setErroredComponents(erroredComponents.filter((id) => id !== componentId));
+ return;
+ }
+ if (!existingError && error) {
+ setErroredComponents([...erroredComponents, componentId]);
+ }
+ };
+
+ const registerComponent = (componentId: string, deregister = false): void => {
+ let newComponents = [...registeredComponents];
+ if (deregister) {
+ newComponents = newComponents.filter((id) => id !== componentId);
+ } else if (!registeredComponents.includes(componentId)) {
+ newComponents = [...newComponents, componentId];
+ }
+ setRegisteredComponents(newComponents);
+ };
+
+ const contextValue: IFormGroupContext = useMemo(() => {
+ return {
+ registerComponent: registerComponent,
+ passError: passError,
+ };
+ }, [registerComponent, passError]);
+
+ const containsFormElements = registeredComponents.length > 0;
+ const containsError = erroredComponents.length > 0;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default FormGroup;
diff --git a/src/components/form-elements/fieldset/FieldsetContext.ts b/src/components/utils/FormGroupContext.ts
similarity index 51%
rename from src/components/form-elements/fieldset/FieldsetContext.ts
rename to src/components/utils/FormGroupContext.ts
index 438b9f3b..7c9d4675 100644
--- a/src/components/form-elements/fieldset/FieldsetContext.ts
+++ b/src/components/utils/FormGroupContext.ts
@@ -1,16 +1,13 @@
import { createContext } from 'react';
-export type IFieldsetContext = {
- isFieldset: boolean;
+export type IFormGroupContext = {
passError: (componentId: string, error: boolean) => void;
registerComponent: (componentId: string, deregister?: boolean) => void;
};
-const FieldsetContext = createContext({
- /* eslint-disable @typescript-eslint/no-empty-function */
- isFieldset: false,
+const FormGroupContext = createContext({
passError: () => {},
registerComponent: () => {},
});
-export default FieldsetContext;
+export default FormGroupContext;
diff --git a/src/util/HeadingLevel.tsx b/src/components/utils/HeadingLevel.tsx
similarity index 100%
rename from src/util/HeadingLevel.tsx
rename to src/components/utils/HeadingLevel.tsx
diff --git a/src/util/LabelBlock.tsx b/src/components/utils/LabelBlock.tsx
similarity index 76%
rename from src/util/LabelBlock.tsx
rename to src/components/utils/LabelBlock.tsx
index d422279d..9e2f0046 100644
--- a/src/util/LabelBlock.tsx
+++ b/src/components/utils/LabelBlock.tsx
@@ -1,9 +1,7 @@
import React, { FC } from 'react';
-import HintText, { HintTextProps } from '../components/form-elements/hint-text/HintText';
-import Label, { LabelProps } from '../components/form-elements/label/Label';
-import ErrorMessage, {
- ErrorMessageProps,
-} from '../components/form-elements/error-message/ErrorMessage';
+import HintText, { HintTextProps } from '../form-elements/hint-text/HintText';
+import Label, { LabelProps } from '../form-elements/label/Label';
+import ErrorMessage, { ErrorMessageProps } from '../form-elements/error-message/ErrorMessage';
interface LabelBlockProps {
elementId?: string;
diff --git a/src/util/FormGroup.tsx b/src/components/utils/SingleInputFormGroup.tsx
similarity index 71%
rename from src/util/FormGroup.tsx
rename to src/components/utils/SingleInputFormGroup.tsx
index f08006d7..d0457c0c 100644
--- a/src/util/FormGroup.tsx
+++ b/src/components/utils/SingleInputFormGroup.tsx
@@ -1,15 +1,13 @@
'use client';
import React, { ReactNode, useState, useEffect, HTMLProps, useContext } from 'react';
import classNames from 'classnames';
-import HintText from '../components/form-elements/hint-text/HintText';
-import ErrorMessage from '../components/form-elements/error-message/ErrorMessage';
-import { generateRandomID } from './RandomID';
-import Label from '../components/form-elements/label/Label';
-import { FormElementProps } from './types/FormTypes';
-import FieldsetContext, {
- IFieldsetContext,
-} from '../components/form-elements/fieldset/FieldsetContext';
-import { useFormContext } from '../components/form-elements/form';
+import HintText from '../form-elements/hint-text/HintText';
+import ErrorMessage from '../form-elements/error-message/ErrorMessage';
+import { generateRandomID } from '../../util/RandomID';
+import Label from '../form-elements/label/Label';
+import { FormElementProps } from '../../util/types/FormTypes';
+import { useFormContext } from '../form-elements/form';
+import FormGroupContext, { IFormGroupContext } from './FormGroupContext';
type ExcludedProps =
| 'hint'
@@ -31,12 +29,14 @@ type FormElementRenderProps = Omit & {
name: string;
};
-export type FormGroupProps = FormElementProps & {
+export type SingleInputFormGroupProps = FormElementProps & {
children: (props: FormElementRenderProps) => ReactNode;
inputType: 'input' | 'radios' | 'select' | 'checkboxes' | 'dateinput' | 'textarea';
};
-const FormGroup = (props: FormGroupProps): JSX.Element => {
+const SingleInputFormGroup = (
+ props: SingleInputFormGroupProps,
+): JSX.Element => {
const {
children,
hint,
@@ -53,8 +53,7 @@ const FormGroup = (props: FormGroupProps(generateRandomID(inputType));
- const { isFieldset, registerComponent, passError } =
- useContext(FieldsetContext);
+ const { registerComponent, passError } = useContext(FormGroupContext);
const { disableErrorFromComponents } = useFormContext();
const elementID = id || generatedID;
@@ -62,10 +61,7 @@ const FormGroup = (props: FormGroupProps(props: FormGroupProps;
useEffect(() => {
- if (!isFieldset) return;
passError(elementID, disableErrorFromComponents ? false : Boolean(error));
return () => passError(elementID, false);
- }, [elementID, error, isFieldset]);
+ }, [elementID, error]);
useEffect(() => {
registerComponent(elementID);
@@ -119,4 +114,4 @@ const FormGroup = (props: FormGroupProps {
+ it('matches snapshot', () => {
+ const { container } = render(
+
+ Heading
+
+ ,
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders children', () => {
+ const { container } = render(Text );
+
+ expect(container.textContent).toBe('Text');
+ });
+
+ it('Wraps children in div if the fieldset does not contain form elements', () => {
+ const { container } = render(Text );
+
+ expect(container.firstChild).not.toHaveClass('nhsuk-form-group');
+ });
+
+ it('Wraps children in form group if the fieldset contains form elements', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.firstChild).toHaveClass('nhsuk-form-group');
+ });
+
+ it('Wraps children in form group error if the fieldset contains form elements in error', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.firstChild).toHaveClass('nhsuk-form-group');
+ expect(container.firstChild).toHaveClass('nhsuk-form-group--error');
+ });
+
+ it('Does not wrap children in form group error if the enableErrorLine prop is not passed', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.firstChild).toHaveClass('nhsuk-form-group');
+ expect(container.firstChild).not.toHaveClass('nhsuk-form-group--error');
+ });
+});
diff --git a/src/util/__tests__/HeadingLevel.test.tsx b/src/components/utils/__tests__/HeadingLevel.test.tsx
similarity index 100%
rename from src/util/__tests__/HeadingLevel.test.tsx
rename to src/components/utils/__tests__/HeadingLevel.test.tsx
diff --git a/src/util/__tests__/FormGroup.test.tsx b/src/components/utils/__tests__/SingleInputFormGroup.test.tsx
similarity index 94%
rename from src/util/__tests__/FormGroup.test.tsx
rename to src/components/utils/__tests__/SingleInputFormGroup.test.tsx
index 9c04be3f..8e435b3d 100644
--- a/src/util/__tests__/FormGroup.test.tsx
+++ b/src/components/utils/__tests__/SingleInputFormGroup.test.tsx
@@ -1,7 +1,7 @@
import React, { HTMLProps } from 'react';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
-import FormGroup, { FormGroupProps } from '../FormGroup';
+import SingleInputFormGroup, { SingleInputFormGroupProps } from '../SingleInputFormGroup';
expect.extend(toHaveNoViolations);
@@ -11,10 +11,10 @@ type Optional = Pick, K> & Omit;
const renderFormGroupComponent = ({
children = (props) => ,
...rest
-}: Optional, 'children'>) =>
- render( {...rest}>{children} );
+}: Optional, 'children'>) =>
+ render( {...rest}>{children} );
-describe('FormGroup', () => {
+describe('SingleInputFormGroup', () => {
it('matches snapshot', () => {
const { container } = renderFormGroupComponent({ inputType: 'input', id: 'testId' });
@@ -181,7 +181,6 @@ describe('FormGroup', () => {
expect(renderProps!.id).toBe('testID');
expect(renderProps!['aria-describedby']).toBe(`testID--error-message`);
-
expect(container.querySelector('.nhsuk-error-message')?.getAttribute('id')).toBe(
'testID--error-message',
);
@@ -229,10 +228,10 @@ describe('FormGroup', () => {
it('should produce an accessible component', async () => {
const { container } = render(
- inputType="input" error label="Form Label">
+ inputType="input" error label="Form Label">
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
{({ error, ...rest }) => }
-
+
,
);
const html = container.innerHTML;
@@ -252,8 +251,10 @@ describe('FormGroup', () => {
const inputElement = container.querySelector('input');
expect(inputElement).not.toBeNull();
- expect(inputElement?.getAttribute('aria-describedby')).toBe('error-and-hint--hint error-and-hint--error-message');
- })
+ expect(inputElement?.getAttribute('aria-describedby')).toBe(
+ 'error-and-hint--hint error-and-hint--error-message',
+ );
+ });
it('should have no aria-describedby when there is no hint or label', () => {
const { container } = renderFormGroupComponent({
diff --git a/src/components/utils/__tests__/__snapshots__/FormGroup.test.tsx.snap b/src/components/utils/__tests__/__snapshots__/FormGroup.test.tsx.snap
new file mode 100644
index 00000000..354401f9
--- /dev/null
+++ b/src/components/utils/__tests__/__snapshots__/FormGroup.test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FormGroup matches snapshot 1`] = `
+
+`;
diff --git a/src/util/__tests__/__snapshots__/FormGroup.test.tsx.snap b/src/components/utils/__tests__/__snapshots__/SingleInputFormGroup.test.tsx.snap
similarity index 75%
rename from src/util/__tests__/__snapshots__/FormGroup.test.tsx.snap
rename to src/components/utils/__tests__/__snapshots__/SingleInputFormGroup.test.tsx.snap
index 50a217ea..2fb78fc2 100644
--- a/src/util/__tests__/__snapshots__/FormGroup.test.tsx.snap
+++ b/src/components/utils/__tests__/__snapshots__/SingleInputFormGroup.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FormGroup matches snapshot 1`] = `
+exports[`SingleInputFormGroup matches snapshot 1`] = `
` element.
+ *
+ * ## Usage
+ *
+ * ### Standard
+ *
+ * ```jsx
+ * import { FormGroup, Fieldset, TextInput } from "nhsuk-react-components";
+ *
+ * const Element = () => {
+ * return (
+ *
+ * What is your weight, in kilograms?
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+
+const meta: Meta
= {
+ title: 'Utils/FormGroup',
+ component: FormGroup,
+ args: {
+ enableErrorLine: true,
+ },
+};
+export default meta;
+type Story = StoryObj;
+
+export const WithFormElement: Story = {
+ render: (args) => (
+
+ What is your weight, in kilograms?
+
+
+ ),
+};
+
+export const WithFormElementInError: Story = {
+ render: (args) => (
+
+ What is your weight, in kilograms?
+
+
+ ),
+};