Merged in fix/BOOK-323-enter-details-scroll-error (pull request #2986)
Fix/BOOK-323 enter details scroll error * fix(BOOK-323): scroll to invalid element on submit on enter details * fix(BOOK-323): update error message design * fix(BOOK-323): clean up * fix(BOOK-323): scroll to fields in room in right order * fix(BOOK-323): add id to translations * fix(BOOK-323): remove undefined * fix(BOOK-323): fix submitting state * fix(BOOK-323): use ref in multiroom for scrolling to right element, add membershipNo * fix(BOOK-323): fix invalid border country * fix(BOOK-323): use error message component * fix(BOOK-323): fix invalid focused styling on mobile * fix(BOOK-323): remove redundant dependency in callback Approved-by: Erik Tiekstra
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useCallback, useEffect, useMemo } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function Details() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const config = useBookingFlowConfig()
|
||||
const refs = useRef<Record<string, HTMLElement | null>>({})
|
||||
|
||||
const { addPreSubmitCallback, rooms } = useEnterDetailsStore((state) => ({
|
||||
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
||||
@@ -106,12 +107,32 @@ export default function Details() {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
function callback() {
|
||||
trigger()
|
||||
async function callback() {
|
||||
await trigger()
|
||||
trackFormSubmit()
|
||||
const fieldOrder = [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"countryCode",
|
||||
"email",
|
||||
"phoneNumber",
|
||||
"membershipNo",
|
||||
]
|
||||
for (const name of fieldOrder) {
|
||||
const fieldError =
|
||||
methods.formState.errors[
|
||||
name as keyof typeof methods.formState.errors
|
||||
]
|
||||
if (fieldError && refs.current[name]) {
|
||||
return refs.current[name] ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
addPreSubmitCallback(`${idx}-details`, callback)
|
||||
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit])
|
||||
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit, methods])
|
||||
|
||||
const updateDetailsStore = useCallback(() => {
|
||||
if (isValid) {
|
||||
@@ -188,88 +209,124 @@ export default function Details() {
|
||||
defaultMessage: "Guest information",
|
||||
})}
|
||||
</Footnote>
|
||||
<BookingFlowInput
|
||||
label={intl.formatMessage({
|
||||
id: "common.firstName",
|
||||
defaultMessage: "First name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="firstName"
|
||||
registerOptions={{
|
||||
required: true,
|
||||
deps: "lastName",
|
||||
onBlur: updateDetailsStore,
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.firstName = el
|
||||
}}
|
||||
/>
|
||||
<BookingFlowInput
|
||||
label={intl.formatMessage({
|
||||
id: "common.lastName",
|
||||
defaultMessage: "Last name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="lastName"
|
||||
registerOptions={{
|
||||
required: true,
|
||||
deps: "firstName",
|
||||
onBlur: updateDetailsStore,
|
||||
}}
|
||||
/>
|
||||
<CountrySelect
|
||||
className={styles.fullWidth}
|
||||
countries={getFormattedCountryList(intl)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
errors.countryCode?.message
|
||||
)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.country",
|
||||
defaultMessage: "Country",
|
||||
})}
|
||||
lang={lang}
|
||||
name="countryCode"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
<BookingFlowInput
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({
|
||||
id: "common.emailAddress",
|
||||
defaultMessage: "Email address",
|
||||
})}
|
||||
name="email"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
<Phone
|
||||
countryLabel={intl.formatMessage({
|
||||
id: "common.countryCode",
|
||||
defaultMessage: "Country code",
|
||||
})}
|
||||
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
||||
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
errors.phoneNumber?.message
|
||||
)}
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({
|
||||
id: "common.phoneNumber",
|
||||
defaultMessage: "Phone number",
|
||||
})}
|
||||
name="phoneNumber"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
{showMembershipIdInput ? (
|
||||
>
|
||||
<BookingFlowInput
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({
|
||||
id: "common.membershipId",
|
||||
defaultMessage: "Membership ID",
|
||||
id: "common.firstName",
|
||||
defaultMessage: "First name",
|
||||
})}
|
||||
name="membershipNo"
|
||||
type="tel"
|
||||
registerOptions={{ onBlur: updateDetailsStore }}
|
||||
maxLength={30}
|
||||
name="firstName"
|
||||
registerOptions={{
|
||||
required: true,
|
||||
deps: "lastName",
|
||||
onBlur: updateDetailsStore,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.lastName = el
|
||||
}}
|
||||
>
|
||||
<BookingFlowInput
|
||||
label={intl.formatMessage({
|
||||
id: "common.lastName",
|
||||
defaultMessage: "Last name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="lastName"
|
||||
registerOptions={{
|
||||
required: true,
|
||||
deps: "firstName",
|
||||
onBlur: updateDetailsStore,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.countryCode = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
>
|
||||
<CountrySelect
|
||||
countries={getFormattedCountryList(intl)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
errors.countryCode?.message
|
||||
)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.country",
|
||||
defaultMessage: "Country",
|
||||
})}
|
||||
lang={lang}
|
||||
name="countryCode"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.email = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
>
|
||||
<BookingFlowInput
|
||||
label={intl.formatMessage({
|
||||
id: "common.emailAddress",
|
||||
defaultMessage: "Email address",
|
||||
})}
|
||||
name="email"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.phoneNumber = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
>
|
||||
<Phone
|
||||
countryLabel={intl.formatMessage({
|
||||
id: "common.countryCode",
|
||||
defaultMessage: "Country code",
|
||||
})}
|
||||
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
||||
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
errors.phoneNumber?.message
|
||||
)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.phoneNumber",
|
||||
defaultMessage: "Phone number",
|
||||
})}
|
||||
name="phoneNumber"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
{showMembershipIdInput ? (
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.membershipNo = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
>
|
||||
<BookingFlowInput
|
||||
label={intl.formatMessage({
|
||||
id: "common.membershipId",
|
||||
defaultMessage: "Membership ID",
|
||||
})}
|
||||
name="membershipNo"
|
||||
type="tel"
|
||||
registerOptions={{ onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} />
|
||||
</div>
|
||||
|
||||
@@ -23,10 +23,12 @@ export default function Signup({
|
||||
errors,
|
||||
name,
|
||||
registerOptions,
|
||||
refs,
|
||||
}: {
|
||||
errors: FieldErrors
|
||||
name: string
|
||||
registerOptions?: RegisterOptions
|
||||
refs: React.RefObject<Record<string, HTMLElement | null>>
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
@@ -45,15 +47,26 @@ export default function Signup({
|
||||
if (isJoinChecked)
|
||||
return (
|
||||
<div className={styles.additionalFormData}>
|
||||
<BookingFlowInput
|
||||
name="zipCode"
|
||||
label={intl.formatMessage({
|
||||
id: "common.zipCode",
|
||||
defaultMessage: "Zip code",
|
||||
})}
|
||||
registerOptions={{ required: true, ...registerOptions }}
|
||||
/>
|
||||
<div className={styles.dateField}>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.zipCode = el
|
||||
}}
|
||||
>
|
||||
<BookingFlowInput
|
||||
name="zipCode"
|
||||
label={intl.formatMessage({
|
||||
id: "common.zipCode",
|
||||
defaultMessage: "Zip code",
|
||||
})}
|
||||
registerOptions={{ required: true, ...registerOptions }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.dateField}
|
||||
ref={(el) => {
|
||||
refs.current.dateOfBirth = el
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<Caption type="bold">
|
||||
<span className={styles.required}>
|
||||
@@ -94,5 +107,13 @@ export default function Signup({
|
||||
|
||||
if (config.enterDetailsMembershipIdInputLocation === "join-card") return null
|
||||
|
||||
return <MembershipNumberInput registerOptions={registerOptions} />
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.membershipNo = el
|
||||
}}
|
||||
>
|
||||
<MembershipNumberInput registerOptions={registerOptions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -40,6 +40,8 @@ type DetailsProps = {
|
||||
|
||||
const formID = "enter-details"
|
||||
export default function Details({ user }: DetailsProps) {
|
||||
const refs = useRef<Record<string, HTMLElement | null>>({})
|
||||
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const config = useBookingFlowConfig()
|
||||
@@ -107,12 +109,36 @@ export default function Details({ user }: DetailsProps) {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
function callback() {
|
||||
trigger()
|
||||
async function callback() {
|
||||
await trigger()
|
||||
trackFormSubmit()
|
||||
const baseFieldOrder = [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"countryCode",
|
||||
"email",
|
||||
"phoneNumber",
|
||||
"membershipNo",
|
||||
]
|
||||
const joinChecked = methods.watch("join")
|
||||
const fieldOrder = joinChecked
|
||||
? [...baseFieldOrder, "zipCode", "dateOfBirth"]
|
||||
: baseFieldOrder
|
||||
for (const name of fieldOrder) {
|
||||
const fieldError =
|
||||
methods.formState.errors[
|
||||
name as keyof typeof methods.formState.errors
|
||||
]
|
||||
if (fieldError && refs.current[name]) {
|
||||
return refs.current[name] ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
addPreSubmitCallback(`${idx}-details`, callback)
|
||||
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit])
|
||||
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit, methods])
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: GuestDetailsSchema) => {
|
||||
@@ -133,12 +159,12 @@ export default function Details({ user }: DetailsProps) {
|
||||
setIncomplete()
|
||||
}
|
||||
}, [
|
||||
handleSubmit,
|
||||
formState.isValid,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
setIncomplete,
|
||||
updatePartialGuestData,
|
||||
getValues,
|
||||
setIncomplete,
|
||||
])
|
||||
|
||||
useEffect(updateDetailsStore, [updateDetailsStore])
|
||||
@@ -174,83 +200,114 @@ export default function Details({ user }: DetailsProps) {
|
||||
defaultMessage: "Guest information",
|
||||
})}
|
||||
</Footnote>
|
||||
<BookingFlowInput
|
||||
autoComplete="given-name"
|
||||
label={intl.formatMessage({
|
||||
id: "common.firstName",
|
||||
defaultMessage: "First name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="firstName"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
<BookingFlowInput
|
||||
autoComplete="family-name"
|
||||
label={intl.formatMessage({
|
||||
id: "common.lastName",
|
||||
defaultMessage: "Last name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="lastName"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
<CountrySelect
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.firstName = el
|
||||
}}
|
||||
>
|
||||
<BookingFlowInput
|
||||
autoComplete="given-name"
|
||||
label={intl.formatMessage({
|
||||
id: "common.firstName",
|
||||
defaultMessage: "First name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="firstName"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.lastName = el
|
||||
}}
|
||||
>
|
||||
<BookingFlowInput
|
||||
autoComplete="family-name"
|
||||
label={intl.formatMessage({
|
||||
id: "common.lastName",
|
||||
defaultMessage: "Last name",
|
||||
})}
|
||||
maxLength={30}
|
||||
name="lastName"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.countryCode = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({
|
||||
id: "common.country",
|
||||
defaultMessage: "Country",
|
||||
})}
|
||||
lang={lang}
|
||||
countries={getFormattedCountryList(intl)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
formState.errors.countryCode?.message
|
||||
)}
|
||||
name="countryCode"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
disabled={!!user}
|
||||
/>
|
||||
<BookingFlowInput
|
||||
autoComplete="email"
|
||||
>
|
||||
<CountrySelect
|
||||
label={intl.formatMessage({
|
||||
id: "common.country",
|
||||
defaultMessage: "Country",
|
||||
})}
|
||||
lang={lang}
|
||||
countries={getFormattedCountryList(intl)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
formState.errors.countryCode?.message
|
||||
)}
|
||||
name="countryCode"
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
disabled={!!user}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.email = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({
|
||||
id: "common.emailAddress",
|
||||
defaultMessage: "Email address",
|
||||
})}
|
||||
name="email"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
<Phone
|
||||
>
|
||||
<BookingFlowInput
|
||||
autoComplete="email"
|
||||
label={intl.formatMessage({
|
||||
id: "common.emailAddress",
|
||||
defaultMessage: "Email address",
|
||||
})}
|
||||
name="email"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={(el) => {
|
||||
refs.current.phoneNumber = el
|
||||
}}
|
||||
className={styles.fullWidth}
|
||||
countryLabel={intl.formatMessage({
|
||||
id: "common.countryCode",
|
||||
defaultMessage: "Country code",
|
||||
})}
|
||||
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
||||
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
formState.errors.phoneNumber?.message
|
||||
)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.phoneNumber",
|
||||
defaultMessage: "Phone number",
|
||||
})}
|
||||
name="phoneNumber"
|
||||
disabled={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
>
|
||||
<Phone
|
||||
countryLabel={intl.formatMessage({
|
||||
id: "common.countryCode",
|
||||
defaultMessage: "Country code",
|
||||
})}
|
||||
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
||||
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
||||
errorMessage={getErrorMessage(
|
||||
intl,
|
||||
config.variant,
|
||||
formState.errors.phoneNumber?.message
|
||||
)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.phoneNumber",
|
||||
defaultMessage: "Phone number",
|
||||
})}
|
||||
name="phoneNumber"
|
||||
disabled={!!user}
|
||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||
/>
|
||||
</div>
|
||||
{user ? null : (
|
||||
<div className={styles.fullWidth}>
|
||||
<Signup
|
||||
errors={formState.errors}
|
||||
name="join"
|
||||
registerOptions={{ onBlur: updateDetailsStore }}
|
||||
refs={refs}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user