Merged in fix/loy-514-fix-validation-tracking-in-signup-form (pull request #3445)

fix(LOY-514): Fix validation error tracking in SignupForm

* Fix issue with form submit handling

* Fix

* Remove browser validation

* Add automatic tracking of validatione rrors


Approved-by: Rasmus Langvad
Approved-by: Matilda Landström
This commit is contained in:
Anton Gunnarsson
2026-01-19 08:34:24 +00:00
parent 0c5670823b
commit 66b7af877a
11 changed files with 43 additions and 47 deletions

View File

@@ -249,8 +249,8 @@ export default function SignupForm({
defaultMessage: "Email address",
})}
name="email"
registerOptions={{ required: true }}
type="email"
registerOptions={{ required: true }}
/>
<Phone
countryLabel={intl.formatMessage({
@@ -307,9 +307,7 @@ export default function SignupForm({
</header>
<Checkbox
name="profilingConsent"
registerOptions={{
required: false,
}}
registerOptions={{ required: true }}
>
{intl.formatMessage({
id: "signup.yesConsent",
@@ -344,9 +342,7 @@ export default function SignupForm({
</header>
<Checkbox
name="termsAccepted"
registerOptions={{
required: true,
}}
registerOptions={{ required: true }}
errorCodeMessages={{
[signupErrors.TERMS_REQUIRED]: intl.formatMessage({
id: "common.mustAcceptTermsError",
@@ -394,23 +390,6 @@ export default function SignupForm({
</Typography>
</section>
{/*
This is a manual validation trigger workaround:
- The Controller component (which Input uses) doesn't re-render on submit,
which prevents automatic error display.
- Future fix requires Input component refactoring (out of scope for now).
*/}
{!methods.formState.isValid ? (
<Button
className={styles.signUpButton}
type="submit"
variant="Primary"
onPress={() => methods.trigger()}
data-testid="trigger-validation"
>
{signupButtonText}
</Button>
) : (
<Button
className={styles.signUpButton}
type="submit"
@@ -420,7 +399,6 @@ export default function SignupForm({
>
{signupButtonText}
</Button>
)}
</form>
</FormProvider>
</div>

View File

@@ -146,7 +146,6 @@ export default function ChildInfoSelector({
<div key={index} className={styles.childInfoContainer}>
<div ref={ageSelectRef}>
<Select
isRequired
items={ageList}
name={ageFieldName}
label={ageLabel}
@@ -161,7 +160,6 @@ export default function ChildInfoSelector({
<div ref={bedPrefSelectRef}>
{child.age >= 0 ? (
<Select
isRequired
items={getAvailableBeds(child.age)}
name={bedFieldName}
label={bedLabel}

View File

@@ -85,7 +85,6 @@ export default function CountryCombobox({
className={styles.select}
data-testid={name}
isDisabled={disabled}
isRequired={Boolean(registerOptions?.required)}
isInvalid={fieldState.invalid}
name={name}
onBlur={field.onBlur}

View File

@@ -54,7 +54,6 @@ export default function CountrySelect({
items={items}
label={label}
isDisabled={disabled}
isRequired={Boolean(registerOptions?.required)}
isInvalid={fieldState.invalid}
name={name}
onBlur={field.onBlur}

View File

@@ -127,7 +127,6 @@ export default function DateSelect({
label={labels.day}
name={DateName.day}
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
isRequired
enableFiltering={isDesktop}
isInvalid={fieldState.invalid}
onBlur={field.onBlur}
@@ -142,7 +141,6 @@ export default function DateSelect({
label={labels.month}
name={DateName.month}
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
isRequired
enableFiltering={isDesktop}
isInvalid={fieldState.invalid}
onBlur={field.onBlur}
@@ -156,7 +154,6 @@ export default function DateSelect({
label={labels.year}
name={DateName.year}
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
isRequired
enableFiltering={isDesktop}
isInvalid={fieldState.invalid}
onBlur={field.onBlur}

View File

@@ -78,7 +78,6 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
isDisabled={isDisabled}
isReadOnly={readOnly}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
>
<Input
{...props}

View File

@@ -59,7 +59,6 @@ export const FormTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
isDisabled={isDisabled}
isReadOnly={readOnly}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
>
<TextArea
{...props}

View File

@@ -131,7 +131,6 @@ export default function Phone({
aria-label={ariaLabel}
isDisabled={disabled || registerOptions.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions?.required}
name={name}
type="tel"
value={phoneNumber}

View File

@@ -89,7 +89,6 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
<AriaInput
{...props}
id={inputId}
required={required}
// Avoid duplicating label text in placeholder when label is positioned above
// Screen readers would announce the label twice (once as label, once as placeholder)
// Only use placeholder if explicitly provided, otherwise use empty string
@@ -140,7 +139,6 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
<AriaInput
{...props}
id={id}
required={required}
// For floating labels, only set placeholder if explicitly provided
// The label itself acts as the placeholder, so we don't want to duplicate it
// This ensures the label only floats when focused or has value

View File

@@ -84,7 +84,6 @@ export const PasswordInput = ({
aria-describedby={describedBy}
isDisabled={field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}

View File

@@ -13,8 +13,11 @@ import {
trackFormAbandonment,
trackFormCompletion,
trackFormInputStarted,
trackFormValidationError,
} from "./form"
import type { FieldErrors } from "react-hook-form"
export function useFormTracking<T extends FieldValues>(
formType: FormType,
subscribe: UseFormSubscribe<T>,
@@ -66,6 +69,13 @@ export function useFormTracking<T extends FieldValues>(
}
}, [formStarted, formType, nameSuffix, formState.isValid])
useEffect(() => {
if (formState.submitCount === 0) return
if (formState.isValid) return
trackErrors(formState.errors, formType, nameSuffix)
}, [formState.submitCount, formState.errors, formType, nameSuffix])
const trackFormSubmit = useCallback(() => {
if (formState.isValid) {
trackFormCompletion(formType, nameSuffix)
@@ -76,3 +86,24 @@ export function useFormTracking<T extends FieldValues>(
trackFormSubmit,
}
}
function trackErrors<T extends FieldValues>(
errors: FieldErrors<T>,
formType: FormType,
nameSuffix: string
) {
const errorKeys = Object.getOwnPropertyNames(errors)
errorKeys.forEach((key) => {
const msg = errors[key]?.message
if (!msg) {
// Handle nested errors
trackErrors(errors[key] as FieldErrors<T>, formType, nameSuffix)
return
}
if (!msg || typeof msg !== "string") return
trackFormValidationError(formType, nameSuffix, msg)
})
}