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",
|
defaultMessage: "Email address",
|
||||||
})}
|
})}
|
||||||
name="email"
|
name="email"
|
||||||
registerOptions={{ required: true }}
|
|
||||||
type="email"
|
type="email"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
<Phone
|
<Phone
|
||||||
countryLabel={intl.formatMessage({
|
countryLabel={intl.formatMessage({
|
||||||
@@ -307,9 +307,7 @@ export default function SignupForm({
|
|||||||
</header>
|
</header>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="profilingConsent"
|
name="profilingConsent"
|
||||||
registerOptions={{
|
registerOptions={{ required: true }}
|
||||||
required: false,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "signup.yesConsent",
|
id: "signup.yesConsent",
|
||||||
@@ -344,9 +342,7 @@ export default function SignupForm({
|
|||||||
</header>
|
</header>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="termsAccepted"
|
name="termsAccepted"
|
||||||
registerOptions={{
|
registerOptions={{ required: true }}
|
||||||
required: true,
|
|
||||||
}}
|
|
||||||
errorCodeMessages={{
|
errorCodeMessages={{
|
||||||
[signupErrors.TERMS_REQUIRED]: intl.formatMessage({
|
[signupErrors.TERMS_REQUIRED]: intl.formatMessage({
|
||||||
id: "common.mustAcceptTermsError",
|
id: "common.mustAcceptTermsError",
|
||||||
@@ -394,33 +390,15 @@ export default function SignupForm({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/*
|
<Button
|
||||||
This is a manual validation trigger workaround:
|
className={styles.signUpButton}
|
||||||
- The Controller component (which Input uses) doesn't re-render on submit,
|
type="submit"
|
||||||
which prevents automatic error display.
|
variant="Primary"
|
||||||
- Future fix requires Input component refactoring (out of scope for now).
|
isDisabled={methods.formState.isSubmitting || signup.isPending}
|
||||||
*/}
|
data-testid="submit"
|
||||||
{!methods.formState.isValid ? (
|
>
|
||||||
<Button
|
{signupButtonText}
|
||||||
className={styles.signUpButton}
|
</Button>
|
||||||
type="submit"
|
|
||||||
variant="Primary"
|
|
||||||
onPress={() => methods.trigger()}
|
|
||||||
data-testid="trigger-validation"
|
|
||||||
>
|
|
||||||
{signupButtonText}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
className={styles.signUpButton}
|
|
||||||
type="submit"
|
|
||||||
variant="Primary"
|
|
||||||
isDisabled={methods.formState.isSubmitting || signup.isPending}
|
|
||||||
data-testid="submit"
|
|
||||||
>
|
|
||||||
{signupButtonText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ export default function ChildInfoSelector({
|
|||||||
<div key={index} className={styles.childInfoContainer}>
|
<div key={index} className={styles.childInfoContainer}>
|
||||||
<div ref={ageSelectRef}>
|
<div ref={ageSelectRef}>
|
||||||
<Select
|
<Select
|
||||||
isRequired
|
|
||||||
items={ageList}
|
items={ageList}
|
||||||
name={ageFieldName}
|
name={ageFieldName}
|
||||||
label={ageLabel}
|
label={ageLabel}
|
||||||
@@ -161,7 +160,6 @@ export default function ChildInfoSelector({
|
|||||||
<div ref={bedPrefSelectRef}>
|
<div ref={bedPrefSelectRef}>
|
||||||
{child.age >= 0 ? (
|
{child.age >= 0 ? (
|
||||||
<Select
|
<Select
|
||||||
isRequired
|
|
||||||
items={getAvailableBeds(child.age)}
|
items={getAvailableBeds(child.age)}
|
||||||
name={bedFieldName}
|
name={bedFieldName}
|
||||||
label={bedLabel}
|
label={bedLabel}
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ export default function CountryCombobox({
|
|||||||
className={styles.select}
|
className={styles.select}
|
||||||
data-testid={name}
|
data-testid={name}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
isRequired={Boolean(registerOptions?.required)}
|
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
name={name}
|
name={name}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export default function CountrySelect({
|
|||||||
items={items}
|
items={items}
|
||||||
label={label}
|
label={label}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
isRequired={Boolean(registerOptions?.required)}
|
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
name={name}
|
name={name}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export default function DateSelect({
|
|||||||
label={labels.day}
|
label={labels.day}
|
||||||
name={DateName.day}
|
name={DateName.day}
|
||||||
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
|
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
|
||||||
isRequired
|
|
||||||
enableFiltering={isDesktop}
|
enableFiltering={isDesktop}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
@@ -142,7 +141,6 @@ export default function DateSelect({
|
|||||||
label={labels.month}
|
label={labels.month}
|
||||||
name={DateName.month}
|
name={DateName.month}
|
||||||
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
|
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
|
||||||
isRequired
|
|
||||||
enableFiltering={isDesktop}
|
enableFiltering={isDesktop}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
@@ -156,7 +154,6 @@ export default function DateSelect({
|
|||||||
label={labels.year}
|
label={labels.year}
|
||||||
name={DateName.year}
|
name={DateName.year}
|
||||||
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
|
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
|
||||||
isRequired
|
|
||||||
enableFiltering={isDesktop}
|
enableFiltering={isDesktop}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isReadOnly={readOnly}
|
isReadOnly={readOnly}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
isRequired={!!registerOptions.required}
|
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export const FormTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isReadOnly={readOnly}
|
isReadOnly={readOnly}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
isRequired={!!registerOptions.required}
|
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ export default function Phone({
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
isDisabled={disabled || registerOptions.disabled}
|
isDisabled={disabled || registerOptions.disabled}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
isRequired={!!registerOptions?.required}
|
|
||||||
name={name}
|
name={name}
|
||||||
type="tel"
|
type="tel"
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
|||||||
<AriaInput
|
<AriaInput
|
||||||
{...props}
|
{...props}
|
||||||
id={inputId}
|
id={inputId}
|
||||||
required={required}
|
|
||||||
// Avoid duplicating label text in placeholder when label is positioned above
|
// Avoid duplicating label text in placeholder when label is positioned above
|
||||||
// Screen readers would announce the label twice (once as label, once as placeholder)
|
// Screen readers would announce the label twice (once as label, once as placeholder)
|
||||||
// Only use placeholder if explicitly provided, otherwise use empty string
|
// Only use placeholder if explicitly provided, otherwise use empty string
|
||||||
@@ -140,7 +139,6 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
|||||||
<AriaInput
|
<AriaInput
|
||||||
{...props}
|
{...props}
|
||||||
id={id}
|
id={id}
|
||||||
required={required}
|
|
||||||
// For floating labels, only set placeholder if explicitly provided
|
// For floating labels, only set placeholder if explicitly provided
|
||||||
// The label itself acts as the placeholder, so we don't want to duplicate it
|
// 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
|
// This ensures the label only floats when focused or has value
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export const PasswordInput = ({
|
|||||||
aria-describedby={describedBy}
|
aria-describedby={describedBy}
|
||||||
isDisabled={field.disabled}
|
isDisabled={field.disabled}
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
isRequired={!!registerOptions.required}
|
|
||||||
name={field.name}
|
name={field.name}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import {
|
|||||||
trackFormAbandonment,
|
trackFormAbandonment,
|
||||||
trackFormCompletion,
|
trackFormCompletion,
|
||||||
trackFormInputStarted,
|
trackFormInputStarted,
|
||||||
|
trackFormValidationError,
|
||||||
} from "./form"
|
} from "./form"
|
||||||
|
|
||||||
|
import type { FieldErrors } from "react-hook-form"
|
||||||
|
|
||||||
export function useFormTracking<T extends FieldValues>(
|
export function useFormTracking<T extends FieldValues>(
|
||||||
formType: FormType,
|
formType: FormType,
|
||||||
subscribe: UseFormSubscribe<T>,
|
subscribe: UseFormSubscribe<T>,
|
||||||
@@ -66,6 +69,13 @@ export function useFormTracking<T extends FieldValues>(
|
|||||||
}
|
}
|
||||||
}, [formStarted, formType, nameSuffix, formState.isValid])
|
}, [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(() => {
|
const trackFormSubmit = useCallback(() => {
|
||||||
if (formState.isValid) {
|
if (formState.isValid) {
|
||||||
trackFormCompletion(formType, nameSuffix)
|
trackFormCompletion(formType, nameSuffix)
|
||||||
@@ -76,3 +86,24 @@ export function useFormTracking<T extends FieldValues>(
|
|||||||
trackFormSubmit,
|
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