Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# NHS.UK React components

## Unreleased

This version provides support for NHS.UK frontend v10.2 and includes:

- [Password input component](https://service-manual.nhs.uk/design-system/components/password-input)
- [Smaller and inline buttons](https://service-manual.nhs.uk/design-system/components/buttons#smaller-buttons)
- [Text input styles for codes and sequences](https://service-manual.nhs.uk/design-system/components/text-input#codes-and-sequences)
- [Select](https://service-manual.nhs.uk/design-system/components/select) dividers using `<Select.Divider />`

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.4 - 5 November 2025

This version provides support for NHS.UK frontend v10.1 and includes:
Expand Down
60 changes: 60 additions & 0 deletions docs/upgrade-to-6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,70 @@ The updated [header](https://service-manual.nhs.uk/design-system/components/head
- update NHS logo in the header to have higher contrast when focused
- refactor CSS classes and BEM naming, use hidden attributes instead of modifier classes, use generic search element

#### Use the password input component to help users accessibly enter passwords

The [password input](https://service-manual.nhs.uk/design-system/components/password-input) component from NHS.UK frontend v10.2 allows users to choose:

- whether their passwords are visible or not
- to enter their passwords in plain text

This helps users use longer and more complex passwords without needing to remember what they've already typed.

#### Smaller versions of radio buttons and checkboxes

You can now use smaller versions of the [radios](https://service-manual.nhs.uk/design-system/components/radios) and [checkboxes](https://service-manual.nhs.uk/design-system/components/checkboxes) components by adding the `small` prop.

#### Smaller versions of buttons

You can now use smaller versions of [buttons](https://service-manual.nhs.uk/design-system/components/buttons) by adding the `small` prop.

#### Add inline buttons to text inputs and select menus

You can now add inline buttons to text inputs and select menus using the `formGroupProps.afterInput` prop.

```jsx
<TextInput
formGroupProps={{
afterInput: () => (
<Button secondary small>
Search
</Button>
),
}},
/>
```

#### Add a 'code' prop for text inputs that accept codes and sequences

We've added a new `code` prop for the [text input](https://service-manual.nhs.uk/design-system/components/text-input) component. This improves readability of text inputs that receive codes and sequences (like NHS numbers, security codes or booking references).

```patch
<TextInput
label="What is your NHS number?"
labelProps={{ isPageHeading: true, size: 'l' }}
inputMode="numeric"
spellCheck="false"
width="10"
+ code
/>
```

#### Add a 'divider' between select options

Newer browsers support [using `<hr>` (horizontal rule) elements inside a `<select>` element](https://developer.chrome.com/blog/hr-in-select/) to help visually break up options for better readability.

We've added a new `<Select.Divider />` child component for select menus to support this feature. For example:

```patch
<Select>
<Select.Option value="first-name-ascending">First name (A to Z)</Select.Option>
<Select.Option value="first-name-descending">First name (Z to A)</Select.Option>
+ <Select.Divider />
<Select.Option value="last-name-ascending">Last name (A to Z)</Select.Option>
<Select.Option value="last-name-descending">Last name (Z to A)</Select.Option>
</Select>
```

### Numbered pagination component

The [pagination](https://service-manual.nhs.uk/design-system/components/pagination) component from NHS.UK frontend v10.1 has been updated to support numbered pagination:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"jest-axe": "^10.0.0",
"jest-environment-jsdom": "^30.2.0",
"lodash": "^4.17.21",
"nhsuk-frontend": "^10.1.0",
"nhsuk-frontend": "^10.2.2",
"outdent": "^0.8.0",
"prettier": "^3.7.4",
"react": "^19.2.3",
Expand All @@ -116,7 +116,7 @@
},
"peerDependencies": {
"classnames": ">=2.5.0",
"nhsuk-frontend": ">=10.1.0 <11.0.0",
"nhsuk-frontend": ">=10.2.0 <11.0.0",
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
"tslib": ">=2.8.0"
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('Index', () => {
'PaginationLinkText',
'Panel',
'PanelTitle',
'PasswordInput',
'Radios',
'RadiosContext',
'RadiosDivider',
Expand All @@ -110,6 +111,7 @@ describe('Index', () => {
'Row',
'SearchIcon',
'Select',
'SelectDivider',
'SelectOption',
'SkipLink',
'SummaryList',
Expand Down
47 changes: 26 additions & 21 deletions src/components/form-elements/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,38 @@ import {

import { type AsElementLink } from '#util/types/LinkTypes.js';

export interface ButtonProps extends AsElementLink<HTMLButtonElement> {
export interface ButtonProps extends ButtonBaseProps, AsElementLink<HTMLButtonElement> {
href?: never;
secondary?: boolean;
reverse?: boolean;
warning?: boolean;
as?: 'button';
preventDoubleClick?: boolean;
}

export interface ButtonLinkProps extends AsElementLink<HTMLAnchorElement> {
export interface ButtonLinkProps extends ButtonBaseProps, AsElementLink<HTMLAnchorElement> {
href: string;
type?: never;
as?: 'a';
}

interface ButtonBaseProps {
secondary?: boolean;
reverse?: boolean;
warning?: boolean;
as?: 'a';
login?: boolean;
small?: boolean;
preventDoubleClick?: boolean;
}

function getButtonClassNames(props: ButtonProps | ButtonLinkProps) {
return classNames(
'nhsuk-button',
{ 'nhsuk-button--secondary': props.secondary },
{ 'nhsuk-button--reverse': props.reverse },
{ 'nhsuk-button--warning': props.warning },
{ 'nhsuk-button--login': props.login },
{ 'nhsuk-button--small': props.small },
props.className,
);
}

const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>((props, forwardedRef) => {
const {
className,
Expand All @@ -41,6 +54,8 @@ const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>((props, forwa
secondary,
reverse,
warning,
login,
small,
type = 'submit',
preventDoubleClick,
onClick,
Expand Down Expand Up @@ -70,13 +85,7 @@ const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>((props, forwa

return (
<Element
className={classNames(
'nhsuk-button',
{ 'nhsuk-button--secondary': secondary },
{ 'nhsuk-button--reverse': reverse },
{ 'nhsuk-button--warning': warning },
className,
)}
className={getButtonClassNames(props)}
data-module="nhsuk-button"
data-prevent-double-click={preventDoubleClick === true ? 'true' : undefined}
disabled={disabled}
Expand Down Expand Up @@ -104,6 +113,8 @@ const ButtonLinkComponent = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
secondary,
reverse,
warning,
login,
small,
preventDoubleClick,
onClick,
...rest
Expand Down Expand Up @@ -132,13 +143,7 @@ const ButtonLinkComponent = forwardRef<HTMLAnchorElement, ButtonLinkProps>(

return (
<Element
className={classNames(
'nhsuk-button',
{ 'nhsuk-button--secondary': secondary },
{ 'nhsuk-button--reverse': reverse },
{ 'nhsuk-button--warning': warning },
className,
)}
className={getButtonClassNames(props)}
data-module="nhsuk-button"
data-prevent-double-click={preventDoubleClick === true ? 'true' : undefined}
role="button"
Expand Down
13 changes: 8 additions & 5 deletions src/components/form-elements/character-count/CharacterCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ import {
import { FormGroup } from '#components/utils/index.js';
import { type FormElementProps } from '#util/types/FormTypes.js';

export interface CharacterCountProps
extends
ComponentPropsWithoutRef<'textarea'>,
Omit<FormElementProps, 'fieldsetProps' | 'legend' | 'legendProps'> {
export interface CharacterCountElementProps extends ComponentPropsWithoutRef<'textarea'> {
maxLength?: number;
maxWords?: number;
threshold?: number;
}

export type CharacterCountProps = CharacterCountElementProps &
Omit<
FormElementProps<CharacterCountElementProps, 'textarea'>,
'fieldsetProps' | 'legend' | 'legendProps'
>;

export const CharacterCount = forwardRef<HTMLTextAreaElement, CharacterCountProps>(
({ maxLength, maxWords, threshold, formGroupProps, ...rest }, forwardedRef) => {
const moduleRef = useRef<HTMLDivElement>(null);
Expand All @@ -47,7 +50,7 @@ export const CharacterCount = forwardRef<HTMLTextAreaElement, CharacterCountProp
}

return (
<FormGroup<CharacterCountProps>
<FormGroup<CharacterCountProps, 'textarea'>
inputType="textarea"
formGroupProps={{
...formGroupProps,
Expand Down
8 changes: 5 additions & 3 deletions src/components/form-elements/checkboxes/Checkboxes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { FormGroup } from '#components/utils/index.js';
import { generateRandomName } from '#util/tools/index.js';
import { type FormElementProps } from '#util/types/FormTypes.js';

export interface CheckboxesProps
extends ComponentPropsWithoutRef<'div'>, Omit<FormElementProps, 'label' | 'labelProps'> {
export interface CheckboxesElementProps extends ComponentPropsWithoutRef<'div'> {
idPrefix?: string;
small?: boolean;
}

export type CheckboxesProps = CheckboxesElementProps &
Omit<FormElementProps<CheckboxesElementProps, 'div'>, 'label' | 'labelProps'>;

const CheckboxesComponent = forwardRef<HTMLDivElement, CheckboxesProps>((props, forwardedRef) => {
const { children, idPrefix, ...rest } = props;

Expand Down Expand Up @@ -82,7 +84,7 @@ const CheckboxesComponent = forwardRef<HTMLDivElement, CheckboxesProps>((props,
}

return (
<FormGroup<CheckboxesProps> inputType="checkboxes" {...rest}>
<FormGroup<CheckboxesProps, 'div'> inputType="checkboxes" {...rest}>
{({ className, small, name, id, idPrefix, error, ...restRenderProps }) => {
resetCheckboxIds();
const contextValue: ICheckboxesContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ import { HintText } from '#components/form-elements/hint-text/index.js';
import { Label } from '#components/form-elements/label/index.js';
import { type ComponentPropsWithDataAttributes, type FormElementProps } from '#util/types/index.js';

export interface CheckboxesItemProps
extends
ComponentPropsWithoutRef<'input'>,
Pick<FormElementProps, 'hint' | 'hintProps' | 'labelProps'> {
export interface CheckboxesItemElementProps extends ComponentPropsWithoutRef<'input'> {
conditional?: ReactNode;
forceShowConditional?: boolean;
conditionalProps?: ComponentPropsWithRef<'div'>;
exclusive?: boolean;
}

export type CheckboxesItemProps = CheckboxesItemElementProps &
Omit<
FormElementProps<CheckboxesItemElementProps, 'input'>,
'fieldsetProps' | 'label' | 'legend' | 'legendProps'
>;

export const CheckboxesItem = forwardRef<HTMLInputElement, CheckboxesItemProps>(
(props, forwardedRef) => {
const {
Expand Down
16 changes: 11 additions & 5 deletions src/components/form-elements/date-input/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,21 @@ export interface DateInputElement extends Omit<HTMLInputElement, 'value' | 'onCh
onChange?: EventHandler<DateInputChangeEvent>;
}

export interface DateInputProps
extends
Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'onChange'>,
Omit<FormElementProps, 'label' | 'labelProps'> {
export interface DateInputElementProps extends Omit<
ComponentPropsWithoutRef<'div'>,
'defaultValue' | 'onChange'
> {
value?: Partial<DateInputValue>;
defaultValue?: Partial<DateInputValue>;
onChange?: EventHandler<DateInputChangeEvent>;
}

export type DateInputProps = DateInputElementProps &
Omit<
FormElementProps<Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'onChange'>, 'div'>,
'label' | 'labelProps'
>;

export type DateInputType = 'day' | 'month' | 'year';

const DateInputComponent = forwardRef<HTMLDivElement, DateInputProps>(
Expand Down Expand Up @@ -78,7 +84,7 @@ const DateInputComponent = forwardRef<HTMLDivElement, DateInputProps>(
};

return (
<FormGroup<Omit<DateInputProps, 'value' | 'defaultValue'>>
<FormGroup<Omit<DateInputProps, 'value' | 'defaultValue'>, 'div'>
formGroupProps={{ ...formGroupProps, ref: moduleRef }}
fieldsetProps={{ role: 'group' }}
inputType="dateinput"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ import { DateInputContext, type IDateInputContext } from '../DateInputContext.js
import { Label } from '#components/form-elements/label/index.js';
import { type FormElementProps } from '#util/types/FormTypes.js';

export interface IndividualDateInputProps
extends ComponentPropsWithoutRef<'input'>, Pick<FormElementProps, 'label' | 'labelProps'> {
export interface IndividualDateInputElementProps extends ComponentPropsWithoutRef<'input'> {
error?: string | ReactElement | false;
inputType: 'day' | 'month' | 'year';
}

export type IndividualDateInputProps = IndividualDateInputElementProps &
Omit<
FormElementProps<IndividualDateInputElementProps, 'input'>,
'error' | 'fieldsetProps' | 'legend' | 'legendProps'
>;

const labels: Record<'day' | 'month' | 'year', string> = {
day: 'Day',
month: 'Month',
Expand Down
1 change: 1 addition & 0 deletions src/components/form-elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './form/index.js';
export * from './hint-text/index.js';
export * from './label/index.js';
export * from './legend/index.js';
export * from './password-input/index.js';
export * from './radios/index.js';
export * from './select/index.js';
export * from './text-input/index.js';
Expand Down
Loading