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:
Matilda Haneling
2026-01-12 14:18:51 +00:00
parent 0c6a4cf186
commit 6a008ba342
38 changed files with 1117 additions and 743 deletions

View File

@@ -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>
</>

View File

@@ -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>

View File

@@ -14,6 +14,7 @@ div.months {
.captionLabel {
text-transform: capitalize;
color: var(--Text-Default);
}
td.day,

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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" />
</>
)
}