import { z } from 'zod' import { BlockDefinition, ResolvableArray, ResolvableBoolean, ResolvableString, EvaluatedBlock, FieldBlockDefinition, } from '@ministryofjustice/hmpps-forge/core/components' import { nunjucksComponent } from '../../utils/nunjucksComponent' import { normaliseGovukErrorMessage, normaliseGovukFieldset, normaliseGovukTextParam, renderGovukBlocksToHtml, type GovukRenderedBlockContent, } from '../../utils/govukParamNormalisers' /** * GOV.UK Checkbox Input component. * Allows users to select multiple options from a list of choices. * * @see https://design-system.service.gov.uk/components/checkboxes/ * @example * ```typescript * GovUKCheckboxInput({ * code: 'contact_methods', * label: 'How would you like to be contacted?', * hint: 'Select all that apply', * items: [ * { value: 'email', text: 'Email' }, * { value: 'phone', text: 'Phone' }, * { value: 'text', text: 'Text message' }, * ], * }) * ``` */ export interface GovUKCheckboxInput extends FieldBlockDefinition { /** * The label for the checkbox group. * When using fieldset, this becomes the legend text if no fieldset legend is specified. * * @example 'Which countries have you visited?' */ label?: ResolvableString /** Can be used to add a fieldset to the checkboxes component. */ fieldset?: { /** * Legend for the fieldset - describes the group of checkbox options. * If not provided, falls back to the `label` property. */ legend?: { /** Text content of the legend */ text?: ResolvableString /** HTML content of the legend (takes precedence over text) */ html?: ResolvableString /** Additional CSS classes for the legend */ classes?: ResolvableString /** Whether to render the legend as a page heading (wrapped in h1) */ isPageHeading?: ResolvableBoolean } /** Additional CSS classes for the fieldset wrapper */ classes?: ResolvableString /** HTML attributes to add to the fieldset */ attributes?: Record /** Element IDs to add to the fieldset's aria-describedby attribute */ describedBy?: ResolvableString } /** * Can be used to add a hint to the checkboxes component. * Provides additional context or instructions for the checkbox group. * * @example 'Select all that apply' // Simple hint * @example { html: 'Choose all relevant options' } // Rich HTML hint */ hint?: | ResolvableString | { /** Unique ID for the hint (auto-generated if not provided) */ id?: ResolvableString /** Text content of the hint */ text?: ResolvableString /** HTML content of the hint (takes precedence over text) */ html?: ResolvableString /** Additional CSS classes for the hint */ classes?: ResolvableString /** Additional HTML attributes for the hint */ attributes?: Record } /** Additional options for the form group containing the checkboxes component. */ formGroup?: { /** Classes to add to the form group wrapper. */ classes?: ResolvableString /** HTML attributes to add to the form group wrapper */ attributes?: Record /** Content to add before all checkbox items within the checkboxes component. */ beforeInputs?: { /** Text content to add before all checkbox items */ text?: ResolvableString /** HTML content to add before all checkbox items (takes precedence over text) */ html?: ResolvableString /** Additional CSS classes for the before inputs content */ classes?: ResolvableString } /** Content to add after all checkbox items within the checkboxes component. */ afterInputs?: { /** Text content to add after all checkbox items */ text?: ResolvableString /** HTML content to add after all checkbox items (takes precedence over text) */ html?: ResolvableString /** Additional CSS classes for the after inputs content */ classes?: ResolvableString } } /** * Optional prefix. This is used to prefix the `id` attribute for each checkbox item input, * hint and error message, separated by `-`. Defaults to the `code` value. * * @example 'contact-methods' // Creates IDs like 'contact-methods-email', 'contact-methods-phone' */ idPrefix?: ResolvableString /** * Name attribute for all checkbox items. * * @example 'contact_preferences' // Form submission key */ name?: ResolvableString /** * One or more element IDs to add to the input `aria-describedby` attribute without a fieldset. * Used to provide additional descriptive information for screenreader users. * * @example 'contact-methods-guidance' */ describedBy?: ResolvableString /** * Additional CSS classes to add to the checkboxes container. * * @example 'govuk-checkboxes--small' // Smaller checkboxes */ classes?: ResolvableString /** Additional HTML attributes (such as data attributes) to add to the anchor tag. */ attributes?: Record /** * The checkbox items within the checkboxes component. * Can include both checkbox options and dividers for visual separation. * Can also be an expression for dynamic items using the Iterator pattern. * * @example [ * { value: 'email', text: 'Email' }, * { value: 'phone', text: 'Phone' }, * { divider: 'or' }, * { value: 'none', text: 'None of the above', behaviour: 'exclusive' } * ] * * @example * // Dynamic items using Iterator * Data('areas').each(Iterator.Map({ value: Item().path('value'), text: Item().path('text') })) */ items: ResolvableArray } /** * Individual checkbox option within a checkbox group. * Represents a single selectable choice with optional conditional reveals and behaviors. */ export interface GovUKCheckboxInputItem { /** * Value for the checkbox input. This is submitted with the form data when selected. * * @example 'Dog' */ value: ResolvableString /** * Text to use within the checkbox item label. * If `html` is provided, this will be ignored. * * @example 'Email' */ text?: ResolvableString /** * HTML to use within the checkbox item label. * Takes precedence over `text` if both are provided. * * @example 'Email Fastest response' */ html?: ResolvableString /** * Specific ID attribute for the checkbox item. * If omitted, then component global `idPrefix` option will be applied. * * @example 'contact-email' */ id?: ResolvableString /** * Can be used to add a hint to each checkbox item within the checkboxes component. * Provides additional context for individual options. * * @example 'We'll send updates to this email address' */ hint?: | ResolvableString | { /** Unique ID for the hint (auto-generated if not provided) */ id?: ResolvableString /** Text content of the hint */ text?: ResolvableString /** HTML content of the hint (takes precedence over text) */ html?: ResolvableString /** Additional CSS classes for the hint */ classes?: ResolvableString /** Additional HTML attributes for the hint */ attributes?: Record } /** * Whether the checkbox should be checked when the page loads. * Takes precedence over the top-level `values` option. * * @example true // Pre-select this option */ checked?: ResolvableBoolean /** * If `true`, checkbox will be disabled and cannot be selected. * * @example true // Disable this option */ disabled?: ResolvableBoolean /** * If set to "exclusive", implements a 'None of these' type behavior via JavaScript. * When this checkbox is selected, all other checkboxes in the group are unchecked. * When any other checkbox is selected, this exclusive checkbox is unchecked. * * @example 'exclusive' // Typical for "None of the above" options */ behaviour?: 'exclusive' /** * Additional HTML attributes (such as data attributes) to add to the checkbox input tag. */ attributes?: Record /** * Subset of options for the label used by each checkbox item. */ label?: { /** Additional CSS classes for the label tag */ classes?: ResolvableString /** HTML attributes to add to the label tag */ attributes?: Record } /** * Provide additional content to reveal when the checkbox is checked. * Useful for collecting additional information when specific options are selected. * * @example someConditionalField // A field definition that appears when this checkbox is selected */ block?: BlockDefinition | BlockDefinition[] /** Conditional visibility for this checkbox item */ visibleWhen?: ResolvableBoolean } /** * Divider element to separate checkbox options visually. */ export interface GovUKCheckboxInputDivider { /** * Divider text to separate checkbox items. * * @example 'or' */ divider: ResolvableString /** Conditional visibility for this divider */ visibleWhen?: ResolvableBoolean } /** * GOV.UK Checkbox Input component. * Allows users to select multiple options from a list of choices. * * @see https://design-system.service.gov.uk/components/checkboxes/ * @example * ```typescript * GovUKCheckboxInput({ * code: 'contact_methods', * label: 'How would you like to be contacted?', * hint: 'Select all that apply', * items: [ * { value: 'email', text: 'Email' }, * { value: 'phone', text: 'Phone' }, * { value: 'text', text: 'Text message' }, * ], * }) * ``` */ export const GovUKCheckboxInput = nunjucksComponent('govukCheckboxInput', { field: true, inputSchema: z.array(z.string()), multiple: true, // The first rendered checkbox's id is the idPrefix, so error summary links land there. errorAnchor: props => props.idPrefix || props.code, render: (props, nunjucksEnv) => { // At render time, items has been evaluated (Collection expressions resolved to arrays) const evaluatedItems = props.items as EvaluatedBlock[] const items = evaluatedItems .filter(option => option.visibleWhen !== false) .map(option => makeOption(option, props.value)) const params = { fieldset: normaliseGovukFieldset(props.fieldset, props.label), idPrefix: props.idPrefix || props.code, name: props.name || props.code, describedBy: props.describedBy, formGroup: props.formGroup, hint: normaliseGovukTextParam(props.hint), items, classes: props.classes, attributes: props.attributes, errorMessage: normaliseGovukErrorMessage(props.errors), } return nunjucksEnv.render('govuk/components/checkboxes/template.njk', { params, }) }, }) const getConditionalContent = (block: GovukRenderedBlockContent) => { const html = renderGovukBlocksToHtml(block) if (html === undefined) { return undefined } return { html } } const makeOption = (option: EvaluatedBlock, blockValue?: any) => { if (isCheckboxDivider(option)) { return { divider: option.divider, } } // For checkboxes, check if the option value is in the array of values let isChecked = false if (option.checked !== undefined) { isChecked = Boolean(option.checked) } else if (Array.isArray(blockValue)) { isChecked = blockValue.includes(option.value) } return { value: option.value, text: option.text, html: option.html, id: option.id, hint: normaliseGovukTextParam(option.hint), checked: isChecked, conditional: getConditionalContent(option.block), disabled: option.disabled, behaviour: option.behaviour, attributes: option.attributes, label: option.label, } } // Narrow to Divider function isCheckboxDivider( option: EvaluatedBlock, ): option is EvaluatedBlock function isCheckboxDivider(option: any): option is GovUKCheckboxInputDivider { return option != null && typeof option === 'object' && 'divider' in option && !('value' in option) // prefer Divider if both accidentally exist }