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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -78,7 +78,6 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
isDisabled={isDisabled}
|
||||
isReadOnly={readOnly}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions.required}
|
||||
>
|
||||
<Input
|
||||
{...props}
|
||||
|
||||
@@ -59,7 +59,6 @@ export const FormTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
|
||||
isDisabled={isDisabled}
|
||||
isReadOnly={readOnly}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions.required}
|
||||
>
|
||||
<TextArea
|
||||
{...props}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user