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
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
"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"
|
||||
@@ -16,17 +20,24 @@ import styles from "./date-picker.module.css"
|
||||
import type { DateRange } from "react-day-picker"
|
||||
|
||||
type DatePickerFormProps = {
|
||||
ariaLabelledBy?: string
|
||||
name?: string
|
||||
}
|
||||
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
export default function DatePickerForm({
|
||||
ariaLabelledBy,
|
||||
name = "date",
|
||||
}: DatePickerFormProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
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 { register, setValue } = useFormContext()
|
||||
const { setValue } = useFormContext()
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (!selectedDate.toDate) {
|
||||
setValue(
|
||||
@@ -38,13 +49,21 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
|
||||
setIsOpen(false)
|
||||
}, [name, setValue, selectedDate])
|
||||
unlockScroll()
|
||||
}, [name, setValue, selectedDate, unlockScroll])
|
||||
|
||||
function showOnFocus() {
|
||||
setIsOpen(true)
|
||||
}
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{
|
||||
isOpen,
|
||||
onClose: () => {
|
||||
setIsOpen(false)
|
||||
unlockScroll()
|
||||
},
|
||||
isDismissable: true,
|
||||
},
|
||||
ref
|
||||
)
|
||||
|
||||
function handleSelectDate(
|
||||
_nextRange: DateRange | undefined,
|
||||
@@ -98,34 +117,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const closeIfOutside = useCallback(
|
||||
(target: HTMLElement) => {
|
||||
if (ref.current && target && !ref.current.contains(target)) {
|
||||
close()
|
||||
}
|
||||
},
|
||||
[close, ref]
|
||||
)
|
||||
|
||||
function closeOnBlur(evt: FocusEvent) {
|
||||
if (isOpen) {
|
||||
const target = evt.relatedTarget as HTMLElement
|
||||
closeIfOutside(target)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(evt: Event) {
|
||||
if (isOpen) {
|
||||
const target = evt.target as HTMLElement
|
||||
closeIfOutside(target)
|
||||
}
|
||||
}
|
||||
document.body.addEventListener("click", handleClickOutside)
|
||||
return () => {
|
||||
document.body.removeEventListener("click", handleClickOutside)
|
||||
}
|
||||
}, [closeIfOutside, isOpen])
|
||||
setIsDesktop(checkIsDesktop)
|
||||
}, [checkIsDesktop])
|
||||
|
||||
const selectedFromDate = dt(selectedDate.fromDate)
|
||||
.locale(lang)
|
||||
@@ -134,64 +128,122 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
? dt(selectedDate.toDate).locale(lang).format(longDateFormat[lang])
|
||||
: ""
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
onBlur={(e) => {
|
||||
closeOnBlur(e.nativeEvent)
|
||||
}}
|
||||
data-datepicker-open={isOpen}
|
||||
ref={ref}
|
||||
>
|
||||
<button
|
||||
className={styles.btn}
|
||||
onFocus={showOnFocus}
|
||||
onClick={() => setIsOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular" className={styles.body}>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "booking.selectedDateRange",
|
||||
defaultMessage: "{selectedFromDate} – {selectedToDate}",
|
||||
},
|
||||
{
|
||||
selectedFromDate,
|
||||
selectedToDate,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Typography>
|
||||
</button>
|
||||
<input {...register("date.fromDate")} type="hidden" />
|
||||
<input {...register("date.toDate")} type="hidden" />
|
||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||
<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,
|
||||
}}
|
||||
/>
|
||||
{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>
|
||||
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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user