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

@@ -1,11 +1,12 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Caption from "@scandic-hotels/design-system/Caption"
import DeprecatedSelect from "@scandic-hotels/design-system/DeprecatedSelect"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Select } from "@scandic-hotels/design-system/Select"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import styles from "./child-selector.module.css"
@@ -38,6 +39,34 @@ export default function ChildInfoSelector({
index = 0,
roomIndex = 0,
}: ChildInfoSelectorProps) {
const ageSelectRef = useRef<HTMLDivElement>(null)
const bedPrefSelectRef = useRef<HTMLDivElement>(null)
const [ageWidth, setAgeWidth] = useState<number | undefined>(undefined)
const [bedWidth, setBedWidth] = useState<number | undefined>(undefined)
//Match width of the dropdown with width of parent select
useEffect(() => {
if (!ageSelectRef.current) return
const observer = new ResizeObserver(() => {
setAgeWidth(ageSelectRef.current!.offsetWidth)
})
observer.observe(ageSelectRef.current)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!bedPrefSelectRef.current) return
const observer = new ResizeObserver(() => {
setBedWidth(bedPrefSelectRef.current!.offsetWidth)
})
observer.observe(bedPrefSelectRef.current)
return () => observer.disconnect()
}, [])
const ageFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.age`
const bedFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.bed`
const intl = useIntl()
@@ -49,10 +78,12 @@ export default function ChildInfoSelector({
id: "booking.bedPreference",
defaultMessage: "Bed preference",
})
const errorMessage = intl.formatMessage({
id: "bookingWidget.child.ageRequiredError",
defaultMessage: "Child age is required",
})
const { setValue, formState } = useFormContext()
function updateSelectedBed(bed: number) {
@@ -113,50 +144,62 @@ export default function ChildInfoSelector({
return (
<>
<div key={index} className={styles.childInfoContainer}>
<div>
<DeprecatedSelect
required={true}
<div ref={ageSelectRef}>
<Select
isRequired
items={ageList}
name={ageFieldName}
label={ageLabel}
aria-label={ageLabel}
value={child.age ?? childDefaultValues.age}
onSelect={(key) => {
onSelectionChange={(key) => {
updateSelectedAge(key as number)
}}
maxHeight={180}
name={ageFieldName}
isNestedInModal={true}
popoverWidth={`${ageWidth}px`}
value={child.age ?? childDefaultValues.age}
isInvalid={!!ageError}
/>
</div>
<div>
<div ref={bedPrefSelectRef}>
{child.age >= 0 ? (
<DeprecatedSelect
<Select
isRequired
items={getAvailableBeds(child.age)}
name={bedFieldName}
label={bedLabel}
aria-label={bedLabel}
value={child.bed ?? childDefaultValues.bed}
onSelect={(key) => {
onSelectionChange={(key) => {
updateSelectedBed(key as number)
}}
name={bedFieldName}
isNestedInModal={true}
popoverWidth={`${bedWidth}px`}
/>
) : null}
</div>
</div>
{roomErrors && roomErrors.message ? (
<Caption color="red" className={styles.error}>
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
{roomErrors.message}
</Caption>
<Typography
variant="Body/Supporting text (caption)/smRegular"
className={styles.error}
>
<span>
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
{roomErrors.message}
</span>
</Typography>
) : null}
{ageError || bedError ? (
<Caption color="red" className={styles.error}>
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
{errorMessage}
</Caption>
<>
<Typography
variant="Body/Supporting text (caption)/smRegular"
className={styles.error}
>
<span>
<MaterialIcon icon="error" color="Icon/Feedback/Error" />
{errorMessage}
</span>
</Typography>
</>
) : null}
</>
)

View File

@@ -15,6 +15,7 @@
}
.error {
color: var(--Text-Interactive-Error);
display: flex;
align-items: center;
gap: var(--Space-x1);

View File

@@ -6,11 +6,11 @@ import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Tooltip } from "@scandic-hotels/design-system/Tooltip"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
import { useIsDesktop } from "../../../hooks/useBreakpoint"
import { GuestsRoom } from "./GuestsRoom"
import styles from "./guests-rooms-picker.module.css"
@@ -30,6 +30,7 @@ export default function GuestsRoomsPickerDialog({
onClose,
}: GuestsRoomsPickerDialogProps) {
const intl = useIntl()
const isDesktop = useIsDesktop()
const config = useBookingFlowConfig()
const { getFieldState, trigger, setValue, getValues } =
useFormContext<BookingWidgetSchema>()
@@ -61,6 +62,11 @@ export default function GuestsRoomsPickerDialog({
defaultMessage:
"Multi-room booking is not available with this booking code.",
})
const isInvalid =
getFieldState("rooms").invalid ||
roomsValue.some((room) =>
room.childrenInRoom.some((child) => child.age === undefined)
)
const handleClose = useCallback(async () => {
const isValid = await trigger("rooms")
@@ -97,13 +103,11 @@ export default function GuestsRoomsPickerDialog({
if (fieldState.invalid) trigger("rooms")
}, [roomsValue, getFieldState, trigger])
const isInvalid =
getFieldState("rooms").invalid ||
roomsValue.some((room) =>
room.childrenInRoom.some((child) => child.age === undefined)
)
const canAddRooms = rooms.length < MAX_ROOMS
const addRoomButtonDisabled =
!!addRoomDisabledTextForSpecialRate || !canAddRooms
return (
<>
<section className={styles.contentWrapper}>
@@ -122,114 +126,89 @@ export default function GuestsRoomsPickerDialog({
onRemove={handleRemoveRoom}
/>
))}
{addRoomDisabledTextForSpecialRate ? (
<div className={styles.addRoomMobileContainer}>
<Button
variant="Text"
color="Primary"
wrapping
onPress={handleAddRoom}
isDisabled
size="sm"
>
<MaterialIcon icon="add" color="CurrentColor" />
{addRoomLabel}
</Button>
<div className={styles.errorContainer}>
<Typography
className={styles.error}
variant="Body/Supporting text (caption)/smRegular"
>
<span>
<MaterialIcon
icon="error"
size={20}
color="Icon/Feedback/Error"
isFilled
/>
{addRoomDisabledTextForSpecialRate}
</span>
</Typography>
</div>
</div>
) : (
canAddRooms && (
<div className={styles.addRoomMobileContainer}>
{!isDesktop && (
<>
<div className={styles.addRoomBtnContainer}>
<Button
className={styles.addRoomBtn}
variant="Text"
wrapping
color="Primary"
onPress={handleAddRoom}
isDisabled={addRoomButtonDisabled}
size="sm"
>
<MaterialIcon icon="add" color="CurrentColor" />
{addRoomLabel}
</Button>
</div>
)
{addRoomDisabledTextForSpecialRate && (
<div className={styles.errorContainer}>
<Typography
className={styles.error}
variant="Body/Supporting text (caption)/smRegular"
>
<span>
<MaterialIcon
icon="error"
size={20}
color="Icon/Feedback/Error"
isFilled
/>
{addRoomDisabledTextForSpecialRate}
</span>
</Typography>
</div>
)}
</>
)}
</div>
</section>
<footer className={styles.footer}>
{addRoomDisabledTextForSpecialRate ? (
<div className={styles.hideOnMobile}>
<Tooltip
text={addRoomDisabledTextForSpecialRate}
position="bottom"
arrow="left"
<div className={styles.footerButtons}>
{isDesktop && (
<Button
variant="Text"
wrapping
color="Primary"
isDisabled={addRoomButtonDisabled}
size="sm"
onPress={handleAddRoom}
>
<Button
variant="Text"
wrapping
color="Primary"
isDisabled
size="sm"
onPress={handleAddRoom}
>
<MaterialIcon icon="add_circle" color="CurrentColor" />
{addRoomLabel}
</Button>
</Tooltip>
</div>
) : (
canAddRooms && (
<div className={styles.hideOnMobile}>
<Button
className={styles.addRoomBtn}
variant="Text"
wrapping
color="Primary"
size="sm"
onPress={handleAddRoom}
>
<MaterialIcon icon="add_circle" color="CurrentColor" />
{addRoomLabel}
</Button>
<MaterialIcon icon="add_circle" color="CurrentColor" />
{addRoomLabel}
</Button>
)}
<Button
onPress={handleClose}
isDisabled={isInvalid}
className={styles.doneButton}
variant={isDesktop ? "Tertiary" : "Primary"}
color="Primary"
size={isDesktop ? "sm" : "md"}
>
{doneLabel}
</Button>
</div>
{/* DESKTOP INLINE ERROR MESSAGE */}
{addRoomDisabledTextForSpecialRate && isDesktop && (
<Typography
className={styles.error}
variant="Body/Supporting text (caption)/smRegular"
>
<div className={styles.errorContainer}>
<MaterialIcon
icon="error"
size={20}
color="Icon/Feedback/Error"
isFilled
/>
{addRoomDisabledTextForSpecialRate}
</div>
)
</Typography>
)}
<Button
onPress={handleClose}
isDisabled={isInvalid}
className={styles.hideOnDesktop}
variant="Tertiary"
color="Primary"
size="sm"
>
{doneLabel}
</Button>
<Button
onPress={handleClose}
isDisabled={isInvalid}
className={styles.hideOnMobile}
variant="Tertiary"
color="Primary"
size="sm"
>
{doneLabel}
</Button>
</footer>
</>
)

View File

@@ -0,0 +1,33 @@
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./validationError.module.css"
export default function ValidationError() {
const intl = useIntl()
const errorMessage = intl.formatMessage({
id: "bookingWidget.child.ageRequiredError",
defaultMessage: "Child age is required",
})
return (
<div className={styles.container}>
<Typography
className={styles.title}
variant="Body/Supporting text (caption)/smBold"
>
<span>
<MaterialIcon
icon="error_circle_rounded"
color="Icon/Feedback/Error"
size={20}
/>
{errorMessage}
</span>
</Typography>
</div>
)
}

View File

@@ -0,0 +1,38 @@
.container {
position: absolute;
top: calc(100% + var(--Space-x2));
background: var(--Surface-Primary-Default);
border-radius: var(--Corner-radius-lg);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
padding: var(--Space-x15);
max-width: min(100vw, calc(360px - var(--Space-x2)));
width: 100%;
left: 0;
display: flex;
flex-direction: column;
padding: var(--Space-x15);
align-items: flex-start;
gap: var(--Space-x05);
z-index: var(--dialog-z-index);
}
.title {
display: flex;
align-items: center;
gap: var(--Space-x1);
color: var(--UI-Text-Error);
}
.message {
text-wrap: auto;
}
@media screen and (min-width: 1367px) {
.container {
top: calc(100% + var(--Space-x1) + var(--Space-x2));
left: calc(var(--Space-x1) * -1);
padding: var(--Space-x2);
max-width: 360px;
width: fit-content;
}
}

View File

@@ -1,30 +1,17 @@
.triggerDesktop {
display: none;
}
.errorContainer {
display: flex;
padding: var(--Space-x2);
padding: var(--Space-x15);
border: 1px solid var(--Border-Default);
border-radius: var(--Corner-radius-md);
justify-content: center;
}
.error {
display: flex;
gap: var(--Space-x1);
color: var(--UI-Text-Error);
}
.pickerContainerMobile {
--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;
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
transition: top 300ms ease;
z-index: 100;
color: var(--Text-Feedback-Error);
text-wrap: wrap;
align-items: center;
}
.contentWrapper {
@@ -35,10 +22,6 @@
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
}
.pickerContainerDesktop {
display: none;
}
.roomContainer {
display: grid;
gap: var(--Space-x2);
@@ -69,6 +52,7 @@
bottom: 0;
right: 0;
padding: 20px var(--Space-x15) 0;
border-radius: var(--Corner-radius-lg);
}
.guestsAndRooms {
@@ -77,10 +61,18 @@
.footer {
display: flex;
flex-direction: row;
gap: var(--Space-x1);
flex-direction: column;
gap: var(--Space-x2);
}
.addRoomBtnContainer {
display: flex;
justify-content: center;
}
.footerButtons {
display: flex;
justify-content: space-between;
}
.roomContainer {
padding: var(--Space-x2);
}
@@ -97,15 +89,27 @@
width: 100%;
}
.contentWrapper
.addRoomMobileContainer
.addRoomBtn:is(:focus, :focus-visible, :focus-within),
.footer .hideOnMobile .addRoomBtn:is(:focus, :focus-visible, :focus-within),
.contentWrapper .addRoomBtn:is(:focus, :focus-visible, :focus-within),
.footer .addRoomBtn:is(:focus, :focus-visible, :focus-within),
.roomActionsButton:is(:focus, :focus-visible, :focus-within) {
outline: var(--Border-Interactive-Focus) auto 1px;
text-decoration: none;
}
.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;
top: calc(max(var(--sitewide-alert-sticky-height), 20px));
right: 0;
bottom: 0;
left: 0;
transition: top 300ms ease;
z-index: var(--booking-widget-z-index);
overflow: scroll;
}
@media screen and (max-width: 1366px) {
.contentContainer {
grid-area: content;
@@ -140,43 +144,52 @@
width: 100%;
}
.footer .hideOnMobile {
display: none;
}
.addRoomMobileContainer {
display: grid;
padding-bottom: calc(var(--sticky-button-height) + 20px);
padding-bottom: var(--Space-x3);
}
.errorContainer {
margin: var(--Space-x2);
}
.addRoomMobileContainer button {
width: 150px;
margin: 0 auto;
}
.addRoomMobileContainer .addRoomMobileDisabledText {
padding: var(--Space-x1) var(--Space-x2);
background-color: var(--Background-Primary);
margin: 0 var(--Space-x2);
border-radius: var(--Corner-radius-md);
display: flex;
gap: var(--Space-x1);
}
}
@media screen and (min-width: 1367px) {
.container {
height: 24px;
}
.pickerContainerMobile {
display: none;
}
.contentWrapper {
grid-template-rows: auto;
}
.footerButtons {
max-height: 40px;
}
.doneButton {
min-width: 125px;
}
.pickerContainer {
position: absolute;
display: grid;
bottom: auto;
left: auto;
right: auto;
border-radius: var(--Corner-radius-lg);
box-shadow: var(--popup-box-shadow);
min-width: 360px;
max-width: calc(100vw - 20px);
padding: var(--Space-x2) var(--Space-x3);
top: calc(100% + 36px);
max-height: none;
overflow-y: auto;
overflow-x: hidden;
}
.roomContainer {
padding: var(--Space-x2) 0 0 0;
}
@@ -193,34 +206,13 @@
overflow-y: visible;
}
.triggerMobile {
display: none;
}
.triggerDesktop {
display: block;
}
.triggerDesktop > span {
.trigger > span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.pickerContainerDesktop {
--header-height: 72px;
--sticky-button-height: 140px;
background-color: var(--Main-Grey-White);
display: grid;
border-radius: var(--Corner-radius-lg);
box-shadow: var(--popup-box-shadow);
max-width: calc(100vw - 20px);
padding: var(--Space-x2) var(--Space-x3);
width: 360px;
}
.pickerContainerDesktop:focus-visible {
.pickerContainer:focus-visible {
outline: none;
}
@@ -229,18 +221,13 @@
}
.footer {
grid-template-columns: auto auto;
display: flex;
justify-content: space-between;
padding-top: var(--Space-x2);
height: fit-content;
}
.footer button {
margin-left: auto;
width: auto;
min-width: 125px;
}
.footer .hideOnDesktop,
.addRoomMobileContainer {
display: none;
}
}

View File

@@ -1,19 +1,16 @@
"use client"
import { useCallback, useEffect, useId, useState } from "react"
import {
Button,
Dialog,
DialogTrigger,
Modal,
Popover,
} from "react-aria-components"
import { useCallback, useEffect, useRef, useState } from "react"
import { FocusScope, useOverlay } from "react-aria"
import { Button } from "react-aria-components"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ValidationError from "./ValidationError/index"
import PickerForm from "./Form"
import styles from "./guests-rooms-picker.module.css"
@@ -26,107 +23,175 @@ export default function GuestsRoomsPickerForm({
}: {
ariaLabelledBy?: string
}) {
const { trigger } = useFormContext<BookingWidgetSchema>()
const rooms = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const popoverId = useId()
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
const [isDesktop, setIsDesktop] = useState(true)
const [isOpen, setIsOpen] = useState(false)
const [containerHeight, setContainerHeight] = useState(0)
const childCount = rooms[0]?.childrenInRoom.length ?? 0 // ToDo Update for multiroom later
const [containerConstraint, setContainerConstraint] = useState(0)
const [showErrorModal, setShowErrorModal] = useState(false)
//isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed".
async function setOverflowClip(isOpen: boolean) {
const bodyElement = document.body
if (bodyElement) {
if (isOpen) {
bodyElement.style.overflow = "visible"
} else {
// !important needed to override 'overflow: hidden' set by react-aria.
// 'overflow: hidden' does not work in combination with other sticky positioned elements, which clip does.
bodyElement.style.overflow = "clip !important"
}
const ref = useRef<HTMLDivElement | null>(null)
// const childCount = rooms[0]?.childrenInRoom.length ?? 0 // ToDo Update for multiroom later
const {
clearErrors,
formState: { errors },
} = useFormContext()
const [scrollPosition, setScrollPosition] = useState(0)
const roomError = errors["rooms"]
const { lockScroll, unlockScroll } = useScrollLock({
autoLock: false,
})
useEffect(() => {
if (roomError) {
setShowErrorModal(true)
}
if (!isOpen) {
const state = await trigger("rooms")
if (state) {
setIsOpen(isOpen)
}
}, [roomError])
useEffect(() => {
clearErrors("rooms")
}, [clearErrors])
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (!roomError) return
if (timeoutRef.current) return
if (roomError) {
timeoutRef.current = setTimeout(() => {
setShowErrorModal(false)
// magic number originates from animation
// 5000ms delay + 120ms exectuion
timeoutRef.current = null
}, 5120)
}
}
return () => {}
}, [clearErrors, roomError])
useEffect(() => {
setIsDesktop(checkIsDesktop)
}, [checkIsDesktop])
const updateHeight = useCallback(() => {
const updateHeight = useCallback((containerConstraint: number) => {
// Get available space for picker to show without going beyond screen
const bookingWidget = document.getElementById("booking-widget")
const popoverElement = document.getElementById("guestsPopover")
const maxHeight =
window.innerHeight -
(bookingWidget?.getBoundingClientRect().bottom ?? 0) -
50
const innerContainerHeight = document
.getElementsByClassName(popoverId)[0]
?.getBoundingClientRect().height
if (
maxHeight != containerHeight &&
const innerContainerHeight = popoverElement?.getBoundingClientRect().height
const shouldAdjustHeight = Boolean(
// height should be constrained
maxHeight != containerConstraint &&
innerContainerHeight &&
maxHeight <= innerContainerHeight
) {
setContainerHeight(maxHeight)
} else if (
containerHeight &&
)
const hasExcessVerticalSpace = Boolean(
// no need to constrain height
containerConstraint &&
innerContainerHeight &&
maxHeight > innerContainerHeight
) {
setContainerHeight(0)
Math.floor(maxHeight) > Math.floor(innerContainerHeight)
)
if (shouldAdjustHeight) {
// avoid clipping if there's only one room
setContainerConstraint(Math.max(200, maxHeight))
} else if (hasExcessVerticalSpace) {
setContainerConstraint(0)
}
}, [containerHeight, popoverId])
}, [])
useEffect(() => {
if (isDesktop && rooms.length > 0) {
updateHeight()
if (isDesktop && rooms.length > 1) {
updateHeight(containerConstraint)
}
}, [childCount, isDesktop, updateHeight, rooms])
}, [
isOpen,
scrollPosition,
isDesktop,
updateHeight,
containerConstraint,
rooms,
])
return isDesktop ? (
<DialogTrigger onOpenChange={setOverflowClip} isOpen={isOpen}>
<Trigger
rooms={rooms}
className={styles.triggerDesktop}
triggerFn={() => {
setIsOpen(true)
}}
/>
<Popover
className={popoverId}
placement="bottom start"
offset={36}
style={containerHeight ? { overflow: "auto" } : undefined}
>
<Dialog className={styles.pickerContainerDesktop}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
</Dialog>
</Popover>
</DialogTrigger>
) : (
<DialogTrigger onOpenChange={setOverflowClip} isOpen={isOpen}>
<Trigger
rooms={rooms}
className={styles.triggerMobile}
triggerFn={() => {
setIsOpen(true)
}}
ariaLabelledBy={ariaLabelledBy}
/>
<Modal>
<Dialog className={styles.pickerContainerMobile}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
useEffect(() => {
if (isOpen && isDesktop) {
const handleScroll = () => {
setScrollPosition(window.scrollY)
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}
}, [isOpen, isDesktop, rooms])
const { overlayProps, underlayProps } = useOverlay(
{
isOpen,
onClose: () => {
setIsOpen(false)
unlockScroll()
},
isDismissable: !errors.rooms,
},
ref
)
return (
<>
<div className={styles.container}>
<Trigger
rooms={rooms}
className={styles.trigger}
triggerFn={() => {
setIsOpen((prev) => !prev)
if (!isDesktop && !isOpen) {
lockScroll()
} else {
unlockScroll()
}
}}
ariaLabelledBy={ariaLabelledBy}
/>
{isOpen && (
<div {...underlayProps}>
<FocusScope contain restoreFocus autoFocus>
<div
id="guestsPopover"
{...overlayProps}
ref={ref}
className={styles.pickerContainer}
data-pressed={isOpen}
data-rooms-open={isOpen}
style={
isDesktop
? containerConstraint > 0
? { maxHeight: containerConstraint }
: { maxHeight: "none" }
: undefined
}
>
<PickerForm
rooms={rooms}
onClose={() => {
setIsOpen((prev) => !prev)
unlockScroll()
}}
/>
</div>
</FocusScope>
</div>
)}
</div>
{showErrorModal && !isOpen && errors.rooms && <ValidationError />}
</>
)
}