Skip to content

Commit

Permalink
Merge pull request #553 from UKHSA-Internal/feat/CDD-2298-feedback-ad…
Browse files Browse the repository at this point in the history
…ditional-field-type

ADD FEEDBACK ADDITIONAL FIELDS
  • Loading branch information
rhys-burendo authored Dec 20, 2024
2 parents 3c8201a + 20b5444 commit c565765
Show file tree
Hide file tree
Showing 20 changed files with 915 additions and 113 deletions.
199 changes: 86 additions & 113 deletions src/app/components/cms/Feedback/Feedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,59 @@ import { z } from 'zod'
import { FormField } from '@/api/models/cms/Page/FormFields'

import { handler } from '../utils/handler'
import CheckboxField from './Fields/Checkbox/CheckboxField'
import CheckboxesField from './Fields/Checkboxes/CheckboxesField'
import DropdownField from './Fields/Dropdown/DropdownField'
import EmailField from './Fields/Email/EmailField'
import MultilineField from './Fields/Multiline/MultilineField'
import NumberField from './Fields/Number/NumberField'
import RadioField from './Fields/Radio/RadioField'
import SinglelineField from './Fields/Singleline/SinglelineField'
import UrlField from './Fields/Url/UrlField'

const initialState = {
message: '',
errors: [],
}

export interface Fieldtype {
label: string
helpText: string
cleanName: string
choicesList?: string[]
defaultValue?: string
defaultValuesList?: string[]
fieldHasError?: boolean
}

interface FieldError {
clean_name: string
label: string
}

export const renderErrorSummary = (errors: FieldError[]) => {
return (
<div className="govuk-error-summary" data-module="govuk-error-summary">
<div role="alert">
<h2 className="govuk-error-summary__title">
<span className="govuk-visually-hidden">Error:</span>The following form fields have errors:{' '}
</h2>
<div className="govuk-error-summary__body">
<ul className="govuk-list govuk-error-summary__list">
{errors.map((item) => {
return (
<li key={item.clean_name}>
<a href={'#' + item.clean_name}>{item.label}</a>
</li>
)
})}
</ul>
</div>
</div>
</div>
)
}

interface FeedbackProps {
formFields: {
id: number
Expand All @@ -28,11 +75,6 @@ interface FeedbackProps {
}[]
}

interface FieldError {
clean_name: string
label: string
}

export default function Feedback({ formFields }: FeedbackProps) {
const [state, formAction] = useFormState(handler.bind(null, formFields), initialState)

Expand Down Expand Up @@ -64,29 +106,6 @@ export default function Feedback({ formFields }: FeedbackProps) {
)
}

export const renderErrorSummary = (errors: FieldError[]) => {
return (
<div className="govuk-error-summary" data-module="govuk-error-summary">
<div role="alert">
<h2 className="govuk-error-summary__title">
<span className="govuk-visually-hidden">Error:</span>The following form fields have errors:{' '}
</h2>
<div className="govuk-error-summary__body">
<ul className="govuk-list govuk-error-summary__list">
{errors.map((item) => {
return (
<li key={item.clean_name}>
<a href={'#' + item.clean_name}>{item.label}</a>
</li>
)
})}
</ul>
</div>
</div>
</div>
)
}

export const renderFormFields = (
errors: FieldError[],
{
Expand All @@ -95,108 +114,62 @@ export const renderFormFields = (
label,
field_type: fieldType,
help_text: helpText,
// TODO: Required validation added in ticket CDD-2300
// required,
choices,
// default_value: defaultValue,
default_value: defaultValue,
}: z.infer<typeof FormField>
) => {
const choicesList = choices.includes('\r\n') ? choices.split('\r\n') : choices.split(',')

// TODO: Implement default values only for checkboxes
// const defaultValuesList = defaultValue.includes('\r\n') ? defaultValue.split('\r\n') : defaultValue.split(',')
const defaultValuesList = defaultValue.includes('\r\n') ? defaultValue.split('\r\n') : defaultValue.split(',')

//does field have errors

const fieldHasError = errors.find(({ clean_name }) => (clean_name === cleanName ? true : false))
// Checks if any errors are present, type conversion to boolean
const fieldHasError = !!errors.find(({ clean_name }) => clean_name === cleanName)

return (
<Fragment key={id}>
{fieldType === 'singleline' && (
<div className="govuk-form-group govuk-!-margin-bottom-9">
<h2 className="govuk-label-wrapper">
<label
className={'govuk-label govuk-label--m' + (fieldHasError ? 'govuk-error-message' : null)}
htmlFor={cleanName}
>
{label}
</label>
</h2>

{helpText.length > 0 ? <div className="govuk-hint">{helpText}</div> : null}

{fieldHasError ? (
<p id="multiline-error" className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span> Please enter a value as this field is required
</p>
) : null}

<textarea
className="govuk-textarea govuk-textarea--error"
name={cleanName}
id={cleanName}
rows={1}
></textarea>
</div>
<SinglelineField label={label} helpText={helpText} cleanName={cleanName} fieldHasError={fieldHasError} />
)}

{fieldType === 'multiline' && (
<div className={'govuk-form-group' + (fieldHasError ? '--error ' : null) + 'govuk-!-margin-bottom-9'}>
<h2 className="govuk-label-wrapper">
<label className="govuk-label govuk-label--m" htmlFor={cleanName}>
{label}
</label>
</h2>

{helpText.length > 0 ? <div className="govuk-hint">{helpText}</div> : null}

{fieldHasError ? (
<p id="multiline-error" className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span> Please enter a value as this field is required
</p>
) : null}

<textarea
className={'govuk-textarea ' + (fieldHasError ? 'govuk-textarea--error' : null)}
name={cleanName}
id={cleanName}
rows={5}
/>
</div>
<MultilineField label={label} helpText={helpText} cleanName={cleanName} fieldHasError={fieldHasError} />
)}

{fieldType === 'radio' && (
<div className={'govuk-form-group govuk-form-group' + (fieldHasError ? '--error ' : null)}>
<fieldset className="govuk-fieldset govuk-!-margin-bottom-9">
<legend className="govuk-fieldset__legend govuk-fieldset__legend--m">
<h2 className="govuk-fieldset__heading">{label}</h2>
</legend>

{helpText.length > 0 ? <div className="govuk-hint">{helpText}</div> : null}

{fieldHasError ? (
<p id="multiline-error" className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span> Please enter a value as this field is required
</p>
) : null}
<div className="govuk-radios" data-module="govuk-radios">
{choicesList.map((choice, key) => {
return (
<div key={key} className="govuk-radios__item">
<input
className="govuk-radios__input"
id={cleanName}
name={cleanName}
type="radio"
value={choice}
/>
<label className="govuk-label govuk-radios__label" htmlFor={cleanName}>
{choice}
</label>
</div>
)
})}
</div>
</fieldset>
</div>
<RadioField
label={label}
helpText={helpText}
cleanName={cleanName}
choicesList={choicesList}
fieldHasError={fieldHasError}
/>
)}

{fieldType === 'email' && <EmailField label={label} helpText={helpText} cleanName={cleanName} />}

{fieldType === 'checkbox' && (
<CheckboxField label={label} helpText={helpText} cleanName={cleanName} defaultValue={defaultValue} />
)}

{fieldType === 'checkboxes' && (
<CheckboxesField
label={label}
helpText={helpText}
cleanName={cleanName}
choicesList={choicesList}
defaultValuesList={defaultValuesList}
/>
)}

{fieldType === 'number' && <NumberField label={label} helpText={helpText} cleanName={cleanName} />}

{fieldType === 'url' && <UrlField label={label} helpText={helpText} cleanName={cleanName} />}

{fieldType === 'dropdown' && (
<DropdownField label={label} helpText={helpText} cleanName={cleanName} choicesList={choicesList} />
)}
</Fragment>
)
Expand Down
21 changes: 21 additions & 0 deletions src/app/components/cms/Feedback/Fields/Checkbox/CheckboxField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Fieldtype } from '../../Feedback'

export default function CheckboxField({ label, helpText, cleanName, defaultValue }: Fieldtype) {
return (
<div className="govuk-form-group govuk-!-margin-bottom-9">
{helpText.length > 0 ? <div className="govuk-hint">{helpText}</div> : null}
<div className="govuk-checkboxes__item">
<input
className="govuk-checkboxes__input"
name={cleanName}
value={defaultValue}
type="checkbox"
id={cleanName}
/>
<label className="govuk-label govuk-checkboxes__label" htmlFor={cleanName}>
{label}
</label>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { render, screen } from '@/config/test-utils'

import CheckboxField from './CheckboxField'

describe('CheckboxField', () => {
const mockProps = {
label: 'Accept terms and conditions',
helpText: 'Please read the terms carefully.',
cleanName: 'terms_and_conditions',
defaultValue: 'true', // Testing with a string value
}

test('renders checkbox field with label and help text when provided', () => {
render(<CheckboxField {...mockProps} />)

// Check if the label is rendered correctly
const label = screen.getByLabelText(mockProps.label)
expect(label).toBeInTheDocument()

// Check if the help text is rendered correctly
const helpText = screen.getByText(mockProps.helpText)
expect(helpText).toBeInTheDocument()
})

test('does not render help text when not provided', () => {
render(<CheckboxField {...{ ...mockProps, helpText: '' }} />)

// Ensure help text is not rendered if it's an empty string
const helpText = screen.queryByText(mockProps.helpText)
expect(helpText).toBeNull()
})

test('checkbox is unchecked by default when defaultValue is an empty string', () => {
render(<CheckboxField {...{ ...mockProps, defaultValue: '' }} />)

// Check if the checkbox is unchecked when defaultValue is an empty string
const checkbox = screen.getByRole('checkbox')
expect(checkbox).not.toBeChecked()
})

test('checkbox is unchecked by default when defaultValue is a falsy string ("false")', () => {
render(<CheckboxField {...{ ...mockProps, defaultValue: 'false' }} />)

// Check if the checkbox is unchecked when defaultValue is "false"
const checkbox = screen.getByRole('checkbox')
expect(checkbox).not.toBeChecked()
})

test('checkbox input has the correct name and id', () => {
render(<CheckboxField {...mockProps} />)

const checkbox = screen.getByRole('checkbox')

// Check if the checkbox has the correct name and id
expect(checkbox).toHaveAttribute('name', mockProps.cleanName)
expect(checkbox).toHaveAttribute('id', mockProps.cleanName)
})

test('checkbox input value matches the defaultValue prop', () => {
render(<CheckboxField {...mockProps} />)

const checkbox = screen.getByRole('checkbox')

// Ensure the checkbox input value matches defaultValue prop
expect(checkbox).toHaveAttribute('value', mockProps.defaultValue)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Fieldtype } from '../../Feedback'

export default function CheckboxesField({
label,
helpText,
cleanName,
choicesList = [],
defaultValuesList = [],
}: Fieldtype) {
return (
<div className="govuk-form-group govuk-!-margin-bottom-9">
<fieldset className="govuk-fieldset govuk-!-margin-bottom-9">
<legend className="govuk-fieldset__legend govuk-fieldset__legend--m">
<h2 className="govuk-fieldset__heading">
<label className="govuk-label govuk-label--m" htmlFor={cleanName}>
{label}
</label>
</h2>
</legend>

{helpText.length > 0 ? <div className="govuk-hint">{helpText}</div> : null}

<div className="govuk-checkboxes" data-module="govuk-checkboxes">
{choicesList.map((choiceVal, key) => {
const uniqueId = `${cleanName}-${key}` // Generate a unique ID for each checkbox
return (
<div key={key} className="govuk-checkboxes__item">
<input
className="govuk-checkboxes__input"
id={uniqueId}
name={cleanName}
type="checkbox"
value={choiceVal}
defaultChecked={defaultValuesList.includes(choiceVal)}
/>
<label className="govuk-label govuk-checkboxes__label" htmlFor={uniqueId}>
{choiceVal}
</label>
</div>
)
})}
</div>
</fieldset>
</div>
)
}
Loading

1 comment on commit c565765

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests coverage

Lines Statements Branches Functions
Coverage: 93%
91.97% (1787/1943) 80.93% (433/535) 89.28% (275/308)
Tests Skipped Failures Errors Time
519 0 💤 0 ❌ 0 🔥 16.723s ⏱️

Please sign in to comment.