Files
web/packages/booking-flow/lib/components/BookingWidget/DatePicker/index.tsx
Matilda Haneling 6a008ba342 Merged in fix/book-149-incorrect-onfocus-behaviour-booking-widget (pull request #3320)
Fix/book 149 incorrect onfocus behaviour booking widget

* fix(BOOK-149): fixed labels shifting

* fix(BOOK-149): reintroduced sticky position

* fix(BOOK-149): added missing border to "where" text field

* added overflow to datepicker

* comment fixes

* removed separate typography declaration

* changed to onPress

* fix(BOOK-149): moved components to separate files

* fix(BOOK-149): removed desktop & mobile specific css classes

* fix(BOOK-149): new implementation of date and room modals

* dependencies update

* fix(BOOK-149): fixed child age dropdown issue, related error message, and Rooms & Guests container height

* updated info button to new variant

* fix(BOOK-149): prevent scrolling of background when modals are open in Tablet mode

* fixed overlay issue and added focus indicator on mobile

* fixed missing space in css

* rebase and fixed icon buttons after update

* simplified to use explicit boolean

* PR comments fixes

* more PR comment fixes

* PR comment fixes

* fixed setIsOpen((prev) => !prev)

* fixed issues with room error not showing properly on mobile

* fixing pr comments

* fixed flickering on GuestRoomModal


Approved-by: Erik Tiekstra
2026-01-12 14:18:51 +00:00

250 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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: () => {
setIsOpen(false)
unlockScroll()
},
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" />
</>
)
}