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:
@@ -6,10 +6,9 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import useLang from "../../../../hooks/useLang"
|
||||
@@ -112,20 +111,11 @@ export default function DatePickerRangeDesktop({
|
||||
color="Border/Divider/Subtle"
|
||||
/>
|
||||
<footer className={props.className}>
|
||||
<Button
|
||||
intent="tertiary"
|
||||
onPress={close}
|
||||
size="small"
|
||||
theme="base"
|
||||
>
|
||||
<Caption color="white" type="bold" asChild>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "datePicker.selectDates",
|
||||
defaultMessage: "Select dates",
|
||||
})}
|
||||
</span>
|
||||
</Caption>
|
||||
<Button variant="Tertiary" onPress={close} size="sm">
|
||||
{intl.formatMessage({
|
||||
id: "datePicker.selectDates",
|
||||
defaultMessage: "Select dates",
|
||||
})}
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import useLang from "../../../../hooks/useLang"
|
||||
@@ -131,19 +131,15 @@ export default function DatePickerRangeMobile({
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
intent="tertiary"
|
||||
variant="Primary"
|
||||
color="Primary"
|
||||
size="md"
|
||||
onPress={close}
|
||||
size="large"
|
||||
theme="base"
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "datePicker.selectDates",
|
||||
defaultMessage: "Select dates",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
{intl.formatMessage({
|
||||
id: "datePicker.selectDates",
|
||||
defaultMessage: "Select dates",
|
||||
})}
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ div.months {
|
||||
|
||||
.captionLabel {
|
||||
text-transform: capitalize;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
td.day,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
align-self: flex-end;
|
||||
background-color: var(--Main-Grey-White);
|
||||
grid-area: header;
|
||||
padding: var(--Space-x3) var(--Space-x2);
|
||||
padding: 0 var(--Space-x2) 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.btn {
|
||||
.triggerButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--Text-Default);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
@@ -12,55 +13,63 @@
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 20px var(--Space-x15) 0;
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
}
|
||||
|
||||
.body {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.hideWrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container[data-datepicker-open="true"] .hideWrapper {
|
||||
.datePicker[data-datepicker-open="true"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pickerContainer {
|
||||
--header-height: 72px;
|
||||
--sticky-button-height: 140px;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
|
||||
transition: top 300ms ease;
|
||||
overflow: scroll;
|
||||
z-index: var(--booking-widget-z-index);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.container {
|
||||
z-index: 10001;
|
||||
.datePicker {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.hideWrapper {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
transition: top 300ms ease;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.container[data-datepicker-open="true"] .hideWrapper {
|
||||
.datePicker[data-datepicker-open="true"] {
|
||||
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
|
||||
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.hideWrapper {
|
||||
.datePicker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pickerContainer {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
box-shadow: var(--popup-box-shadow);
|
||||
padding: var(--Space-x2) var(--Space-x3);
|
||||
position: absolute;
|
||||
/**
|
||||
BookingWidget padding +
|
||||
border-width +
|
||||
wanted space below booking widget
|
||||
*/
|
||||
top: calc(100% + var(--Space-x1) + 1px + var(--Space-x4));
|
||||
max-width: calc(100vw - 20px);
|
||||
max-height: 440px;
|
||||
top: calc(100% + 36px);
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.triggerButton {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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