diff --git a/components/elements/fields/decimalField.js b/components/elements/fields/decimalField.js
new file mode 100644
index 00000000..772e9a5a
--- /dev/null
+++ b/components/elements/fields/decimalField.js
@@ -0,0 +1,106 @@
+import React from "react";
+import _ from "lodash";
+import useTranslation from "next-translate/useTranslation";
+import { Field } from "redux-form";
+import { RenderTextfield } from "./renderTextfield";
+import { RenderDecimalField } from "./renderDecimalField";
+import { validateField } from "../validationUtils";
+
+const isVisibleByInclusion = ({
+ parentField,
+ form,
+ formProps,
+ parentFieldShowOnValue
+}) =>
+ parentField != false &&
+ !_.isEmpty(formProps[form]) &&
+ _.has(formProps[form].values, parentField) &&
+ parentFieldShowOnValue.indexOf(
+ formProps[form].values[parentField].toString()
+ ) > -1;
+
+export function DecimalField(props) {
+ const {
+ name,
+ validation,
+ form,
+ formProps,
+ parentFieldShowOnValue,
+ parentField
+ } = props;
+
+ let { t } = useTranslation();
+
+ const showIfHasParentShowValue = isVisibleByInclusion({
+ parentField,
+ form,
+ formProps,
+ parentFieldShowOnValue
+ });
+
+ const validateDecimal = (value) => {
+ if (!value) return t("newappeal:is-required-label");
+
+ const regex = /^\d{1,6}(\.\d{1,2})?$/;
+
+ if (!regex.test(value)) {
+ return t("newappeal:invalid-decimal-label");
+ }
+
+ return undefined;
+ };
+
+ const requiredMessage = t("newappeal:is-required-label");
+ const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
+ const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
+
+ return (
+ <>
+ {parentField != false ? (
+ showIfHasParentShowValue && (
+
+
+ validateField(
+ value,
+ validation,
+ requiredMessage,
+ emojiNotAllowedMessage,
+ invalidPostcodeMessage
+ )
+ }
+ component={RenderTextfield}
+ label={props.label}
+ maxFieldLength={props.maxFieldLength}
+ />
+
+ )
+ ) : (
+
+
+
+ )}
+ >
+ );
+}
diff --git a/components/elements/fields/numericField.js b/components/elements/fields/numericField.js
new file mode 100644
index 00000000..6bd17454
--- /dev/null
+++ b/components/elements/fields/numericField.js
@@ -0,0 +1,114 @@
+import React from "react";
+import _ from "lodash";
+import useTranslation from "next-translate/useTranslation";
+import { Field } from "redux-form";
+import { RenderTextfield } from "./renderTextfield";
+import { validateField } from "../validationUtils";
+
+const isVisibleByInclusion = ({
+ parentField,
+ form,
+ formProps,
+ parentFieldShowOnValue
+}) =>
+ parentField != false &&
+ !_.isEmpty(formProps[form]) &&
+ _.has(formProps[form].values, parentField) &&
+ parentFieldShowOnValue.indexOf(
+ formProps[form].values[parentField].toString()
+ ) > -1;
+
+export function NumericField(props) {
+ const {
+ name,
+ label,
+ validation,
+ form,
+ formProps,
+ parentFieldShowOnValue,
+ parentField
+ } = props;
+
+ let { t } = useTranslation();
+
+ const required = (value) => {
+ return value || value == 0
+ ? undefined
+ : t("newappeal:is-required-label");
+ };
+ const isNumber = (value) => {
+ const regex = /^\d+$/;
+ return regex.test(value)
+ ? undefined
+ : t("newappeal:invalid-number-label");
+ };
+
+ const maxLength = (max) => (value) =>
+ value && value.length > max
+ ? `Must be ${max} characters or less`
+ : undefined;
+
+ const showIfHasParentShowValue = isVisibleByInclusion({
+ parentField,
+ form,
+ formProps,
+ parentFieldShowOnValue
+ });
+
+ const requiredMessage = t("newappeal:is-required-label");
+ const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
+ const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
+
+ return (
+ <>
+ {parentField != false ? (
+ showIfHasParentShowValue && (
+
+
+ validateField(
+ value,
+ validation,
+ requiredMessage,
+ emojiNotAllowedMessage,
+ invalidPostcodeMessage
+ )
+ }
+ component={RenderTextfield}
+ label={label}
+ maxFieldLength={props.maxFieldLength}
+ />
+
+ )
+ ) : (
+
+
+
+ )}
+ >
+ );
+}
diff --git a/components/elements/fields/radioField.js b/components/elements/fields/radioField.js
new file mode 100644
index 00000000..80eae158
--- /dev/null
+++ b/components/elements/fields/radioField.js
@@ -0,0 +1,107 @@
+import React from "react";
+import _ from "lodash";
+import useTranslation from "next-translate/useTranslation";
+import { Field } from "redux-form";
+import { RenderRadio } from "./renderRadio";
+import { validateField } from "../validationUtils";
+
+const isVisibleByInclusion = ({
+ parentField,
+ form,
+ formProps,
+ parentFieldShowOnValue
+}) =>
+ parentField != false &&
+ !_.isEmpty(formProps[form]) &&
+ _.has(formProps[form].values, parentField) &&
+ parentFieldShowOnValue.indexOf(
+ formProps[form].values[parentField].toString()
+ ) > -1;
+
+export function Radiofield(props) {
+ const {
+ name,
+ label,
+ datafieldname,
+ form,
+ formProps,
+ parentFieldShowOnValue,
+ parentField,
+ validation
+ } = props;
+
+ let { t } = useTranslation();
+
+ const fieldOptions = props.options
+ .slice(1, props.options.length - 1)
+ .split(",");
+
+ const showIfHasParentShowValue = isVisibleByInclusion({
+ parentField,
+ form,
+ formProps,
+ parentFieldShowOnValue
+ });
+
+ const requiredMessage = t("newappeal:is-required-label");
+ const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
+ const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
+
+ return (
+ <>
+ {parentField != false ? (
+ showIfHasParentShowValue && (
+
+ validateField(
+ value,
+ validation,
+ requiredMessage,
+ emojiNotAllowedMessage,
+ invalidPostcodeMessage
+ )
+ }
+ requiredDocumentLabel={props.requiredDocumentLabel}
+ requiredDocumentValue={props.requiredDocumentValue}
+ setDocumentsList={props.setDocumentsList}
+ documentList={props.documentList}
+ />
+ )
+ ) : (
+
+ validateField(
+ value,
+ validation,
+ requiredMessage,
+ emojiNotAllowedMessage,
+ invalidPostcodeMessage
+ )
+ }
+ requiredDocumentLabel={props.requiredDocumentLabel}
+ requiredDocumentValue={props.requiredDocumentValue}
+ setDocumentsList={props.setDocumentsList}
+ documentList={props.documentList}
+ />
+ )}
+ >
+ );
+}
diff --git a/components/elements/index.js b/components/elements/index.js
index da43b0a3..550f8d17 100644
--- a/components/elements/index.js
+++ b/components/elements/index.js
@@ -13,12 +13,13 @@ import { getFieldTranslation } from "./helpers/translationHelpers";
import { RenderRichMultiline } from "./fields/renderRichMultiline";
import { RenderTextfield } from "./fields/renderTextfield";
import { RenderMultiline } from "./fields/renderMultiline";
-import { RenderRadio } from "./fields/renderRadio";
import { PickList } from "./fields/pickListField";
import { CheckBoxfield } from "./fields/checkBoxField";
import { DateFieldPicker } from "./fields/dateFieldPicker";
import { YesNofield } from "./fields/yesNoField";
-import { RenderDecimalField } from "./fields/renderDecimalField";
+import { Radiofield } from "./fields/radioField";
+import { NumericField } from "./fields/numericField";
+import { DecimalField } from "./fields/decimalField";
import { RenderFileUpload } from "./fields/renderFileUpload";
export { RenderSubFields } from "./fields/renderSubFields";
export { FieldArrayForm } from "./fields/fieldArrayForm";
@@ -26,6 +27,9 @@ export { PickList };
export { CheckBoxfield };
export { DateFieldPicker };
export { YesNofield };
+export { Radiofield };
+export { NumericField };
+export { DecimalField };
const getValidationMessages = (t) => ({
requiredMessage: t("newappeal:is-required-label"),
@@ -489,318 +493,6 @@ export function DateField(props) {
);
}
-export function Radiofield(props) {
- const {
- name,
- label,
- datafieldname,
- form,
- formProps,
- parentFieldShowOnValue,
- parentField,
- validation
- } = props;
-
- const router = useRouter();
- // console.log(props.options);
- let { t } = useTranslation();
-
- const fieldOptions = props.options
- .slice(1, props.options.length - 1)
- .split(",");
-
- //parentFieldShowOnValue
- var showIfHasParentShowValue = isVisibleByInclusion({
- parentField,
- form,
- formProps,
- parentFieldShowOnValue
- });
-
- (showIfHasParentShowValue == parentField) != false &&
- showIfHasParentShowValue;
-
- const { requiredMessage, emojiNotAllowedMessage, invalidPostcodeMessage } =
- getValidationMessages(t);
- return (
- <>
- {parentField != false ? (
- showIfHasParentShowValue && (
-
- validateField(
- value,
- validation,
- requiredMessage,
- emojiNotAllowedMessage,
- invalidPostcodeMessage
- )
- } // Use the external validate function
- requiredDocumentLabel={props.requiredDocumentLabel}
- requiredDocumentValue={props.requiredDocumentValue}
- setDocumentsList={props.setDocumentsList}
- documentList={props.documentList}
- />
- )
- ) : (
-
- validateField(
- value,
- validation,
- requiredMessage,
- emojiNotAllowedMessage,
- invalidPostcodeMessage
- )
- } // Use the external validate function
- requiredDocumentLabel={props.requiredDocumentLabel}
- requiredDocumentValue={props.requiredDocumentValue}
- setDocumentsList={props.setDocumentsList}
- documentList={props.documentList}
- />
- )}
- >
- );
-}
-
-export function NumericField(props) {
- const {
- name,
- label,
- validation,
- form,
- formProps,
- parentFieldShowOnValue,
- parentField,
- maxFieldLength
- } = props;
-
- let { t } = useTranslation();
-
- const required = (value) => {
- return value || value == 0
- ? undefined
- : t("newappeal:is-required-label");
- };
- const isNumber = (value) => {
- const regex = /^\d+$/;
- return regex.test(value)
- ? undefined
- : t("newappeal:invalid-number-label");
- };
-
- const maxLength = (max) => (value) =>
- value && value.length > max
- ? `Must be ${max} characters or less`
- : undefined;
-
- //parentFieldShowOnValue
- var showIfHasParentShowValue = isVisibleByInclusion({
- parentField,
- form,
- formProps,
- parentFieldShowOnValue
- });
-
- (showIfHasParentShowValue == parentField) != false &&
- showIfHasParentShowValue;
-
- //console.log("parentField:", parentField, showIfHasParentShowValue);
- const { requiredMessage, emojiNotAllowedMessage, invalidPostcodeMessage } =
- getValidationMessages(t);
- return (
- <>
- {parentField != false ? (
- showIfHasParentShowValue && (
-
-
- validateField(
- value,
- validation,
- requiredMessage,
- emojiNotAllowedMessage,
- invalidPostcodeMessage
- )
- } // Use the external validate function
- component={RenderTextfield}
- label={label}
- maxFieldLength={props.maxFieldLength}
- />
-
- )
- ) : (
-
- parseInt(value)}
- // maxFieldLength={props.maxFieldLength}
-
- name={props.name}
- id={props.name}
- type="text"
- className="govuk-input govuk-input--width-10"
- aria-describedby={name}
- //pattern="^\d*\.?\d+$"
- pattern="[0-9]*"
- validate={[
- required,
- isNumber,
- maxLength(props.maxFieldLength)
- ]}
- component={RenderTextfield}
- label={props.label}
- maxFieldLength={props.maxFieldLength}
- />
-
- )}
- >
- );
-}
-
-export function DecimalField(props) {
- const {
- name,
- label,
- validation,
- form,
- formProps,
- parentFieldShowOnValue,
- parentField,
- maxFieldLength
- } = props;
-
- let { t } = useTranslation();
-
- //parentFieldShowOnValue
- var showIfHasParentShowValue = isVisibleByInclusion({
- parentField,
- form,
- formProps,
- parentFieldShowOnValue
- });
-
- (showIfHasParentShowValue == parentField) != false &&
- showIfHasParentShowValue;
-
- //console.log("parentField:", parentField, showIfHasParentShowValue);
-
- const validateDecimal = (value) => {
- if (!value) return t("newappeal:is-required-label");
-
- // Regex to match whole numbers or decimal numbers (up to 7 characters including decimal)
- const regex = /^\d{1,6}(\.\d{1,2})?$/; // Up to 6 digits before the decimal, 2 after
-
- // Check if the value matches the regex pattern
- if (!regex.test(value)) {
- return t("newappeal:invalid-decimal-label");
- }
-
- return undefined;
- };
-
- const { requiredMessage, emojiNotAllowedMessage, invalidPostcodeMessage } =
- getValidationMessages(t);
- return (
- <>
- {parentField != false ? (
- showIfHasParentShowValue && (
-
-
- validateField(
- value,
- validation,
- requiredMessage,
- emojiNotAllowedMessage,
- invalidPostcodeMessage
- )
- } // Use the external validate function
- component={RenderTextfield}
- label={props.label}
- maxFieldLength={props.maxFieldLength}
- />
-
- )
- ) : (
-
-
-
- )}
- >
- );
-}
-
export function ReadOnlyfield(props) {
const { name, label, value } = props;
//console.log(props.value.refno);
diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md
index 7449ea75..28f12758 100644
--- a/memory-bank/change-log.md
+++ b/memory-bank/change-log.md
@@ -3670,3 +3670,31 @@ Validation:
Follow-ups:
- Remaining wrappers can continue as bounded slices (`Radiofield`, `NumericField`, `DecimalField`) if required.
+
+---
+
+### CL-104: 22500 wrapper extraction bundle (`Radiofield`, `NumericField`, `DecimalField`)
+
+date: 2026-04-08
+author: Cline
+scope: `components/elements/index.js`, `components/elements/fields/{radioField,numericField,decimalField}.js`
+type: change
+rationale: Continue Phase 2 with the next bounded wrapper bundle by extracting wrappers 4/5/6 from `components/elements/index.js` into dedicated field modules without behavior change.
+impact: No intended behavior change; preserves EN/CY behavior, validation wiring, and accessibility semantics while reducing monolith size.
+status: completed
+
+Summary:
+
+- Added `components/elements/fields/radioField.js` for `Radiofield`.
+- Added `components/elements/fields/numericField.js` for `NumericField`.
+- Added `components/elements/fields/decimalField.js` for `DecimalField`.
+- Updated `components/elements/index.js` to import/export these wrappers from field modules.
+- Removed inline `Radiofield`, `NumericField`, and `DecimalField` implementations from `index.js`.
+
+Validation:
+
+- `npx eslint components/elements/index.js components/elements/fields/radioField.js components/elements/fields/numericField.js components/elements/fields/decimalField.js` -> pass
+
+Follow-ups:
+
+- Next bounded wrappers (if needed): `ReadOnlyfield`/remaining small wrappers or additional dead-code cleanup slices.