Fix/book 149 ui fixes * fixed text-overflow issue in datepicker trigger * fixed X missing in booking code text field * fixed toDate not setting properly * fixed spacing issues and placeholder text not fitting * added error message to child age if none is added * spacing fixes * Revert "map link alignment fix" This reverts commit d38cc5b007bc05a1d48ce6661b1052fe714961c3. * fixed EB points padding issue on SAS tablet * maxWidth on BookingCode/voucher * spacing fixes * fixed icons in error message * spacing fixes * scroll to child age picker updates * feat(SW-3706): fix heatmap issue for langswitcher and booking widget * fixed tablet lineup issue Approved-by: Linus Flood
249 lines
7.1 KiB
TypeScript
249 lines
7.1 KiB
TypeScript
"use client"
|
||
import { useCallback, useEffect, useRef, useState } from "react"
|
||
import { FocusScope, useOverlay } from "react-aria"
|
||
import { Button as ButtonRAC } from "react-aria-components"
|
||
import { useFormContext, useWatch } from "react-hook-form"
|
||
import { useIntl } from "react-intl"
|
||
import { useMediaQuery } from "usehooks-ts"
|
||
|
||
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||
import { dt } from "@scandic-hotels/common/dt"
|
||
import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock"
|
||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||
|
||
import useLang from "../../../hooks/useLang"
|
||
import DatePickerRangeDesktop from "./Range/Desktop"
|
||
import DatePickerRangeMobile from "./Range/Mobile"
|
||
|
||
import styles from "./date-picker.module.css"
|
||
|
||
import type { DateRange } from "react-day-picker"
|
||
|
||
type DatePickerFormProps = {
|
||
ariaLabelledBy?: string
|
||
name?: string
|
||
}
|
||
export default function DatePickerForm({
|
||
ariaLabelledBy,
|
||
name = "date",
|
||
}: DatePickerFormProps) {
|
||
const lang = useLang()
|
||
|
||
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
|
||
const [isDesktop, setIsDesktop] = useState(true)
|
||
const { lockScroll, unlockScroll } = useScrollLock({
|
||
autoLock: false,
|
||
})
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
const selectedDate = useWatch({ name })
|
||
const { setValue } = useFormContext()
|
||
const ref = useRef<HTMLDivElement | null>(null)
|
||
const close = useCallback(() => {
|
||
if (!selectedDate.toDate) {
|
||
setValue(
|
||
name,
|
||
{
|
||
fromDate: selectedDate.fromDate,
|
||
toDate: dt(selectedDate.fromDate).add(1, "day").format("YYYY-MM-DD"),
|
||
},
|
||
{ shouldDirty: true }
|
||
)
|
||
}
|
||
setIsOpen(false)
|
||
unlockScroll()
|
||
}, [name, setValue, selectedDate, unlockScroll])
|
||
|
||
const { overlayProps, underlayProps } = useOverlay(
|
||
{
|
||
isOpen,
|
||
onClose: () => {
|
||
close()
|
||
},
|
||
isDismissable: true,
|
||
},
|
||
ref
|
||
)
|
||
|
||
function handleSelectDate(
|
||
_nextRange: DateRange | undefined,
|
||
selectedDay: Date
|
||
) {
|
||
const now = dt()
|
||
const dateClicked = dt(selectedDay)
|
||
const dateClickedFormatted = dateClicked.format("YYYY-MM-DD")
|
||
/* check if selected date is not before todays date,
|
||
which happens when "Enter" key is pressed in any other input field of the form */
|
||
if (!dateClicked.isBefore(now, "day")) {
|
||
// Handle form value updates based on the requirements
|
||
if (selectedDate.fromDate && selectedDate.toDate) {
|
||
// Both dates were previously selected, starting fresh with new date
|
||
setValue(
|
||
name,
|
||
{
|
||
fromDate: dateClickedFormatted,
|
||
toDate: undefined,
|
||
},
|
||
{ shouldDirty: true }
|
||
)
|
||
} else if (selectedDate.fromDate && !selectedDate.toDate) {
|
||
// If the selected day is the same as the first date, we don't need to update the form value
|
||
if (dateClicked.isSame(selectedDate.fromDate)) {
|
||
return
|
||
}
|
||
// We're selecting the second date
|
||
if (dateClicked.isBefore(selectedDate.fromDate)) {
|
||
// If second selected date is before first date, swap them
|
||
setValue(
|
||
name,
|
||
{
|
||
fromDate: dateClickedFormatted,
|
||
toDate: selectedDate.fromDate,
|
||
},
|
||
{ shouldDirty: true }
|
||
)
|
||
} else {
|
||
// If second selected date is after first date, keep order
|
||
setValue(
|
||
name,
|
||
{
|
||
fromDate: selectedDate.fromDate,
|
||
toDate: dateClickedFormatted,
|
||
},
|
||
{ shouldDirty: true }
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
setIsDesktop(checkIsDesktop)
|
||
}, [checkIsDesktop])
|
||
|
||
const selectedFromDate = dt(selectedDate.fromDate)
|
||
.locale(lang)
|
||
.format(longDateFormat[lang])
|
||
const selectedToDate = !!selectedDate.toDate
|
||
? dt(selectedDate.toDate).locale(lang).format(longDateFormat[lang])
|
||
: ""
|
||
|
||
return isDesktop ? (
|
||
<div className={styles.datePicker}>
|
||
<Trigger
|
||
ariaLabelledBy={ariaLabelledBy}
|
||
onPress={() => {
|
||
setIsOpen((prev) => !prev)
|
||
}}
|
||
selectedFromDate={selectedFromDate}
|
||
selectedToDate={selectedToDate}
|
||
/>
|
||
{isOpen && (
|
||
<div {...underlayProps}>
|
||
<FocusScope contain restoreFocus autoFocus>
|
||
<div
|
||
{...overlayProps}
|
||
ref={ref}
|
||
className={styles.pickerContainer}
|
||
data-datepicker-open={isOpen}
|
||
>
|
||
<DatePickerRangeDesktop
|
||
close={close}
|
||
handleOnSelect={handleSelectDate}
|
||
// DayPicker lib needs Daterange in form as below to show appropriate UI
|
||
selectedRange={{
|
||
from: dt(selectedDate.fromDate).toDate(),
|
||
to: selectedDate.toDate
|
||
? dt(selectedDate.toDate).toDate()
|
||
: undefined,
|
||
}}
|
||
/>
|
||
</div>
|
||
</FocusScope>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className={styles.datePicker}>
|
||
<Trigger
|
||
ariaLabelledBy={ariaLabelledBy}
|
||
onPress={() => {
|
||
setIsOpen((prev) => !prev)
|
||
if (!isOpen) {
|
||
lockScroll()
|
||
} else {
|
||
unlockScroll()
|
||
}
|
||
}}
|
||
selectedFromDate={selectedFromDate}
|
||
selectedToDate={selectedToDate}
|
||
/>
|
||
{isOpen && (
|
||
<div {...underlayProps}>
|
||
<FocusScope contain restoreFocus autoFocus>
|
||
<div
|
||
{...overlayProps}
|
||
ref={ref}
|
||
className={styles.pickerContainer}
|
||
data-datepicker-open={isOpen}
|
||
>
|
||
<DatePickerRangeMobile
|
||
close={close}
|
||
handleOnSelect={handleSelectDate}
|
||
// DayPicker lib needs Daterange in form as below to show appropriate UI
|
||
selectedRange={{
|
||
from: dt(selectedDate.fromDate).toDate(),
|
||
to: selectedDate.toDate
|
||
? dt(selectedDate.toDate).toDate()
|
||
: undefined,
|
||
}}
|
||
/>
|
||
</div>
|
||
</FocusScope>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Trigger({
|
||
onPress,
|
||
selectedFromDate,
|
||
selectedToDate,
|
||
ariaLabelledBy,
|
||
}: {
|
||
onPress?: () => void
|
||
selectedFromDate: string
|
||
selectedToDate: string
|
||
ariaLabelledBy?: string
|
||
}) {
|
||
const intl = useIntl()
|
||
const { register } = useFormContext()
|
||
|
||
const triggerText = intl.formatMessage(
|
||
{
|
||
id: "booking.selectedDateRange",
|
||
defaultMessage: "{selectedFromDate} – {selectedToDate}",
|
||
},
|
||
{
|
||
selectedFromDate,
|
||
selectedToDate,
|
||
}
|
||
)
|
||
return (
|
||
<>
|
||
<Typography variant="Body/Paragraph/mdRegular">
|
||
<ButtonRAC
|
||
className={styles.triggerButton}
|
||
onPress={onPress}
|
||
type="button"
|
||
aria-labelledby={ariaLabelledBy}
|
||
>
|
||
{triggerText}
|
||
</ButtonRAC>
|
||
</Typography>
|
||
<input {...register("date.fromDate")} type="hidden" />
|
||
<input {...register("date.toDate")} type="hidden" />
|
||
</>
|
||
)
|
||
}
|