Merged in feat/sw-2879-booking-widget-to-booking-flow-package (pull request #2532)

feat(SW-2879): Move BookingWidget to booking-flow package

* Fix lockfile

* Fix styling

* a tiny little booking widget test

* Tiny fixes

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Remove unused scripts

* lint:fix

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Tiny lint fixes

* update test

* Update Input in booking-flow

* Clean up comments etc

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Setup tracking context for booking-flow

* Add missing use client

* Fix temp tracking function

* Pass booking to booking-widget

* Remove comment

* Add use client to booking widget tracking provider

* Add use client to tracking functions

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Move debug page

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package

* Merge branch 'master' into feat/sw-2879-booking-widget-to-booking-flow-package


Approved-by: Bianca Widstam
This commit is contained in:
Anton Gunnarsson
2025-08-05 09:20:20 +00:00
parent 03c9244fdf
commit 1bd8fe6821
206 changed files with 1936 additions and 796 deletions

View File

@@ -0,0 +1,5 @@
.container {
display: flex;
justify-content: space-between;
align-items: center;
}

View File

@@ -0,0 +1,53 @@
"use client"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Caption from "@scandic-hotels/design-system/Caption"
import Counter from "../Counter"
import styles from "./adult-selector.module.css"
type AdultSelectorProps = {
roomIndex: number
currentAdults: number
}
export default function AdultSelector({
roomIndex = 0,
currentAdults,
}: AdultSelectorProps) {
const name = `rooms.${roomIndex}.adults`
const intl = useIntl()
const adultsLabel = intl.formatMessage({
defaultMessage: "Adults",
})
const { setValue } = useFormContext()
function increaseAdultsCount() {
if (currentAdults < 6) {
setValue(name, currentAdults + 1, { shouldDirty: true })
}
}
function decreaseAdultsCount() {
if (currentAdults > 1) {
setValue(name, currentAdults - 1, { shouldDirty: true })
}
}
return (
<section className={styles.container}>
<Caption color="uiTextHighContrast" type="bold">
{adultsLabel}
</Caption>
<Counter
count={currentAdults}
handleOnDecrease={decreaseAdultsCount}
handleOnIncrease={increaseAdultsCount}
disableDecrease={currentAdults == 1}
disableIncrease={currentAdults == 6}
/>
</section>
)
}

View File

@@ -0,0 +1,157 @@
"use client"
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 { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import styles from "./child-selector.module.css"
import type { Child } from "@scandic-hotels/trpc/types/child"
const ageList = [...Array(13)].map((_, i) => ({
label: i.toString(),
value: i,
}))
const childDefaultValues = { age: -1, bed: -1 }
type ChildBed = {
label: string
value: number
}
type ChildInfoSelectorProps = {
child: Child
adults: number
index: number
roomIndex: number
childrenInAdultsBed: number
}
export default function ChildInfoSelector({
child,
childrenInAdultsBed,
adults,
index = 0,
roomIndex = 0,
}: ChildInfoSelectorProps) {
const ageFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.age`
const bedFieldName = `rooms.${roomIndex}.childrenInRoom.${index}.bed`
const intl = useIntl()
const ageLabel = intl.formatMessage({
defaultMessage: "Age",
})
const bedLabel = intl.formatMessage({
defaultMessage: "Bed preference",
})
const errorMessage = intl.formatMessage({
defaultMessage: "Child age is required",
})
const { setValue, formState } = useFormContext()
function updateSelectedBed(bed: number) {
setValue(`rooms.${roomIndex}.childrenInRoom.${index}.bed`, bed)
}
function updateSelectedAge(age: number) {
setValue(`rooms.${roomIndex}.childrenInRoom.${index}.age`, age)
const availableBedTypes = getAvailableBeds(age)
updateSelectedBed(availableBedTypes[0].value)
}
const allBedTypes: ChildBed[] = [
{
label: intl.formatMessage({
defaultMessage: "In adult's bed",
}),
value: ChildBedMapEnum.IN_ADULTS_BED,
},
{
label: intl.formatMessage({
defaultMessage: "In crib",
}),
value: ChildBedMapEnum.IN_CRIB,
},
{
label: intl.formatMessage({
defaultMessage: "In extra bed",
}),
value: ChildBedMapEnum.IN_EXTRA_BED,
},
]
function getAvailableBeds(age: number) {
let availableBedTypes: ChildBed[] = []
if (age <= 5 && (adults > childrenInAdultsBed || child.bed === 0)) {
availableBedTypes.push(allBedTypes[0])
}
if (age < 3) {
availableBedTypes.push(allBedTypes[1])
}
if (age > 2) {
availableBedTypes.push(allBedTypes[2])
}
return availableBedTypes
}
const roomErrors =
//@ts-expect-error: formState is typed with FormValues
formState.errors.rooms?.[roomIndex]?.childrenInRoom?.[index]
const ageError = roomErrors?.age
const bedError = roomErrors?.bed
return (
<>
<div key={index} className={styles.childInfoContainer}>
<div>
<DeprecatedSelect
required={true}
items={ageList}
label={ageLabel}
aria-label={ageLabel}
value={child.age ?? childDefaultValues.age}
onSelect={(key) => {
updateSelectedAge(key as number)
}}
maxHeight={180}
name={ageFieldName}
isNestedInModal={true}
/>
</div>
<div>
{child.age >= 0 ? (
<DeprecatedSelect
items={getAvailableBeds(child.age)}
label={bedLabel}
aria-label={bedLabel}
value={child.bed ?? childDefaultValues.bed}
onSelect={(key) => {
updateSelectedBed(key as number)
}}
name={bedFieldName}
isNestedInModal={true}
/>
) : null}
</div>
</div>
{roomErrors && roomErrors.message ? (
<Caption color="red" className={styles.error}>
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
{roomErrors.message}
</Caption>
) : null}
{ageError || bedError ? (
<Caption color="red" className={styles.error}>
<MaterialIcon icon="error" color="Icon/Interactive/Accent" />
{errorMessage}
</Caption>
) : null}
</>
)
}

View File

@@ -0,0 +1,21 @@
.container {
display: flex;
justify-content: space-between;
align-items: center;
}
.captionBold {
font-weight: 600;
}
.childInfoContainer {
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: 1fr 2fr;
}
.error {
display: flex;
align-items: center;
gap: var(--Spacing-x1);
}

View File

@@ -0,0 +1,84 @@
"use client"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Caption from "@scandic-hotels/design-system/Caption"
import Counter from "../Counter"
import ChildInfoSelector from "./ChildInfoSelector"
import styles from "./child-selector.module.css"
import type { Child } from "@scandic-hotels/trpc/types/child"
type ChildSelectorProps = {
roomIndex: number
currentAdults: number
currentChildren: Child[]
childrenInAdultsBed: number
}
export default function ChildSelector({
roomIndex = 0,
currentAdults,
childrenInAdultsBed,
currentChildren,
}: ChildSelectorProps) {
const intl = useIntl()
const childrenLabel = intl.formatMessage({
defaultMessage: "Children",
})
const { setValue } = useFormContext()
function increaseChildrenCount(roomIndex: number) {
if (currentChildren.length < 5) {
setValue(
`rooms.${roomIndex}.childrenInRoom.${currentChildren.length}`,
{
age: undefined,
bed: undefined,
},
{ shouldDirty: true }
)
}
}
function decreaseChildrenCount(roomIndex: number) {
if (currentChildren.length > 0) {
currentChildren.pop()
setValue(`rooms.${roomIndex}.childrenInRoom`, currentChildren, {
shouldDirty: true,
})
}
}
return (
<>
<section className={styles.container}>
<Caption color="uiTextHighContrast" type="bold">
{childrenLabel}
</Caption>
<Counter
count={currentChildren.length}
handleOnDecrease={() => {
decreaseChildrenCount(roomIndex)
}}
handleOnIncrease={() => {
increaseChildrenCount(roomIndex)
}}
disableDecrease={currentChildren.length == 0}
disableIncrease={currentChildren.length == 5}
/>
</section>
{currentChildren.map((child, index) => (
<ChildInfoSelector
roomIndex={roomIndex}
index={index}
child={child}
adults={currentAdults}
key={"child_" + index}
childrenInAdultsBed={childrenInAdultsBed}
/>
))}
</>
)
}

View File

@@ -0,0 +1,13 @@
.counterContainer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20px;
}
.counterBtn {
width: 40px;
height: 40px;
}
.counterBtn:not([disabled]) {
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,55 @@
"use client"
import Body from "@scandic-hotels/design-system/Body"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import styles from "./counter.module.css"
type CounterProps = {
count: number
handleOnIncrease: () => void
handleOnDecrease: () => void
disableIncrease: boolean
disableDecrease: boolean
}
export default function Counter({
count,
handleOnIncrease,
handleOnDecrease,
disableIncrease,
disableDecrease,
}: CounterProps) {
return (
<div className={styles.counterContainer}>
<Button
className={styles.counterBtn}
intent="inverted"
onClick={handleOnDecrease}
size="small"
theme="base"
variant="icon"
wrapping={true}
disabled={disableDecrease}
>
<MaterialIcon icon="remove" color="CurrentColor" />
</Button>
<Body color="baseTextHighContrast" textAlign="center">
{count}
</Body>
<Button
className={styles.counterBtn}
onClick={handleOnIncrease}
intent="inverted"
variant="icon"
theme="base"
wrapping={true}
size="small"
disabled={disableIncrease}
>
<MaterialIcon icon="add" color="CurrentColor" />
</Button>
</div>
)
}

View File

@@ -0,0 +1,226 @@
"use client"
import { useCallback, useEffect } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
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 { GuestsRoom } from "./GuestsRoom"
import styles from "./guests-rooms-picker.module.css"
import type { GuestsRoom as TGuestsRoom } from ".."
import type { BookingWidgetSchema } from "../Client"
const MAX_ROOMS = 4
interface GuestsRoomsPickerDialogProps {
rooms: TGuestsRoom[]
onClose: () => void
}
export default function GuestsRoomsPickerDialog({
rooms,
onClose,
}: GuestsRoomsPickerDialogProps) {
const intl = useIntl()
const { getFieldState, trigger, setValue, getValues } =
useFormContext<BookingWidgetSchema>()
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const addRoomLabel = intl.formatMessage({
defaultMessage: "Add room",
})
const doneLabel = intl.formatMessage({
defaultMessage: "Done",
})
// Disable add room if booking code is either voucher or corporate cheque, or reward night is enabled
const addRoomDisabledTextForSpecialRate = getValues(SEARCH_TYPE_REDEMPTION)
? intl.formatMessage({
defaultMessage:
"Multi-room booking is not available with reward night.",
})
: getValues("bookingCode.value")?.toLowerCase().startsWith("vo") &&
intl.formatMessage({
defaultMessage:
"Multi-room booking is not available with this booking code.",
})
const handleClose = useCallback(async () => {
const isValid = await trigger("rooms")
if (isValid) onClose()
}, [trigger, onClose])
const handleAddRoom = useCallback(() => {
setValue("rooms", [...roomsValue, { adults: 1, childrenInRoom: [] }], {
shouldValidate: true,
shouldDirty: true,
})
}, [roomsValue, setValue])
const handleRemoveRoom = useCallback(
(index: number) => {
const updatedRooms = roomsValue.filter((_, i) => i !== index)
setValue("rooms", updatedRooms, {
shouldValidate: true,
shouldDirty: true,
})
if (updatedRooms.length === 1) {
trigger("bookingCode.value")
trigger(SEARCH_TYPE_REDEMPTION)
}
},
[roomsValue, trigger, setValue]
)
// Validate rooms when they change
useEffect(() => {
const fieldState = getFieldState("rooms")
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
return (
<>
<section className={styles.contentWrapper}>
<header className={styles.header}>
<button type="button" className={styles.close} onClick={onClose}>
<MaterialIcon icon="close" />
</button>
</header>
<div className={styles.contentContainer}>
{rooms.map((room, index) => (
<GuestsRoom
key={index}
room={room}
index={index}
onRemove={handleRemoveRoom}
/>
))}
{addRoomDisabledTextForSpecialRate ? (
<div className={styles.addRoomMobileContainer}>
<Button
intent="text"
variant="icon"
wrapping
theme="base"
fullWidth
onPress={handleAddRoom}
disabled
>
<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}>
<Button
className={styles.addRoomBtn}
intent="text"
variant="icon"
wrapping
theme="base"
fullWidth
onPress={handleAddRoom}
>
<MaterialIcon icon="add" color="CurrentColor" />
{addRoomLabel}
</Button>
</div>
)
)}
</div>
</section>
<footer className={styles.footer}>
{addRoomDisabledTextForSpecialRate ? (
<div className={styles.hideOnMobile}>
<Tooltip
text={addRoomDisabledTextForSpecialRate}
position="bottom"
arrow="left"
>
<Button
intent="text"
variant="icon"
wrapping
theme="base"
disabled
onPress={handleAddRoom}
>
<MaterialIcon icon="add_circle" color="CurrentColor" />
{addRoomLabel}
</Button>
</Tooltip>
</div>
) : (
canAddRooms && (
<div className={styles.hideOnMobile}>
<Button
className={styles.addRoomBtn}
intent="text"
variant="icon"
wrapping
theme="base"
onPress={handleAddRoom}
>
<MaterialIcon icon="add_circle" color="CurrentColor" />
{addRoomLabel}
</Button>
</div>
)
)}
<Button
onPress={handleClose}
disabled={isInvalid}
className={styles.hideOnDesktop}
intent="tertiary"
theme="base"
size="large"
>
{doneLabel}
</Button>
<Button
onPress={handleClose}
disabled={isInvalid}
className={styles.hideOnMobile}
intent="tertiary"
theme="base"
size="small"
>
{doneLabel}
</Button>
</footer>
</>
)
}

View File

@@ -0,0 +1,72 @@
import { useIntl } from "react-intl"
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 Subtitle from "@scandic-hotels/design-system/Subtitle"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import AdultSelector from "../AdultSelector"
import ChildSelector from "../ChildSelector"
import styles from "../guests-rooms-picker.module.css"
import type { GuestsRoom } from "../.."
export function GuestsRoom({
room,
index,
onRemove,
}: {
room: GuestsRoom
index: number
onRemove: (index: number) => void
}) {
const intl = useIntl()
const roomLabel = intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{
roomIndex: index + 1,
}
)
const childrenInAdultsBed = room.childrenInRoom.filter(
(child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED
).length
return (
<div className={styles.roomContainer}>
<section className={styles.roomDetailsContainer}>
<Subtitle type="two" className={styles.roomHeading}>
{roomLabel}
</Subtitle>
<AdultSelector roomIndex={index} currentAdults={room.adults} />
<ChildSelector
roomIndex={index}
currentAdults={room.adults}
currentChildren={room.childrenInRoom}
childrenInAdultsBed={childrenInAdultsBed}
/>
{index !== 0 && (
<Button
intent="text"
variant="icon"
wrapping
theme="secondaryLight"
onPress={() => onRemove(index)}
size="small"
className={styles.roomActionsButton}
>
<MaterialIcon icon="delete" color="CurrentColor" />
{intl.formatMessage({
defaultMessage: "Remove room",
})}
</Button>
)}
</section>
<Divider color="Border/Divider/Subtle" />
</div>
)
}

View File

@@ -0,0 +1,246 @@
.triggerDesktop {
display: none;
}
.errorContainer {
display: flex;
padding: var(--Space-x2);
}
.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: 20px;
transition: top 300ms ease;
z-index: 100;
}
.contentWrapper {
display: grid;
grid-template-areas:
"header"
"content";
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
}
.pickerContainerDesktop {
display: none;
}
.roomContainer {
display: grid;
gap: var(--Spacing-x2);
}
.roomDetailsContainer {
display: grid;
gap: var(--Spacing-x2);
padding-bottom: var(--Spacing-x1);
}
.roomHeading {
margin-bottom: var(--Spacing-x1);
}
.btn {
background: none;
border: none;
color: var(--Text-Default);
cursor: pointer;
outline: none;
padding: 0;
width: 100%;
text-align: left;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 20px var(--Spacing-x-one-and-half) 0;
}
.guestsAndRooms {
color: var(--Text-Default);
}
.footer {
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
}
.roomContainer {
padding: var(--Spacing-x2);
}
.roomContainer:last-of-type {
padding-bottom: calc(var(--sticky-button-height) + 20px);
}
.roomActionsButton {
margin-left: auto;
color: var(--Base-Text-Accent);
}
.footer button {
width: 100%;
}
.contentWrapper
.addRoomMobileContainer
.addRoomBtn:is(:focus, :focus-visible, :focus-within),
.footer .hideOnMobile .addRoomBtn:is(:focus, :focus-visible, :focus-within),
.roomActionsButton:is(:focus, :focus-visible, :focus-within) {
outline: -webkit-focus-ring-color auto 1px;
text-decoration: none;
}
@media screen and (max-width: 1366px) {
.contentContainer {
grid-area: content;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.header {
display: grid;
grid-area: header;
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
}
.close {
background: none;
border: none;
cursor: pointer;
display: flex;
justify-self: flex-end;
padding: 0;
}
.footer {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 7.5%,
#ffffff 82.5%
);
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x7);
position: sticky;
bottom: 0;
width: 100%;
}
.footer .hideOnMobile {
display: none;
}
.addRoomMobileContainer {
display: grid;
padding-bottom: calc(var(--sticky-button-height) + 20px);
}
.addRoomMobileContainer button {
width: 150px;
margin: 0 auto;
}
.addRoomMobileContainer .addRoomMobileDisabledText {
padding: var(--Spacing-x1) var(--Spacing-x2);
background-color: var(--Background-Primary);
margin: 0 var(--Spacing-x2);
border-radius: var(--Corner-radius-md);
display: flex;
gap: var(--Spacing-x1);
}
}
@media screen and (min-width: 1367px) {
.container {
height: 24px;
}
.pickerContainerMobile {
display: none;
}
.contentWrapper {
grid-template-rows: auto;
}
.roomContainer {
padding: var(--Spacing-x2) 0 0 0;
}
.roomContainer:first-of-type {
padding-top: 0;
}
.roomContainer:last-of-type {
padding-bottom: 0;
}
.contentContainer {
overflow-y: visible;
}
.triggerMobile {
display: none;
}
.triggerDesktop {
display: block;
}
.triggerDesktop > 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(--Spacing-x2) var(--Spacing-x3);
width: 360px;
}
.pickerContainerDesktop:focus-visible {
outline: none;
}
.header {
display: none;
}
.footer {
grid-template-columns: auto auto;
padding-top: var(--Spacing-x2);
}
.footer button {
margin-left: auto;
width: 125px;
}
.footer .hideOnDesktop,
.addRoomMobileContainer {
display: none;
}
}

View File

@@ -0,0 +1,190 @@
"use client"
import { useCallback, useEffect, useId, useState } from "react"
import {
Button,
Dialog,
DialogTrigger,
Modal,
Popover,
} from "react-aria-components"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { Typography } from "@scandic-hotels/design-system/Typography"
import PickerForm from "./Form"
import styles from "./guests-rooms-picker.module.css"
import type { GuestsRoom } from ".."
import type { BookingWidgetSchema } from "../Client"
export default function GuestsRoomsPickerForm({
ariaLabelledBy,
}: {
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
//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"
}
}
if (!isOpen) {
const state = await trigger("rooms")
if (state) {
setIsOpen(isOpen)
}
}
}
useEffect(() => {
setIsDesktop(checkIsDesktop)
}, [checkIsDesktop])
const updateHeight = useCallback(() => {
// Get available space for picker to show without going beyond screen
const bookingWidget = document.getElementById("booking-widget")
let maxHeight =
window.innerHeight -
(bookingWidget?.getBoundingClientRect().bottom ?? 0) -
50
const innerContainerHeight = document
.getElementsByClassName(popoverId)[0]
?.getBoundingClientRect().height
if (
maxHeight != containerHeight &&
innerContainerHeight &&
maxHeight <= innerContainerHeight
) {
setContainerHeight(maxHeight)
} else if (
containerHeight &&
innerContainerHeight &&
maxHeight > innerContainerHeight
) {
setContainerHeight(0)
}
}, [containerHeight, popoverId])
useEffect(() => {
if (isDesktop && rooms.length > 0) {
updateHeight()
}
}, [childCount, isDesktop, updateHeight, 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>
)
}
function Trigger({
rooms,
className,
triggerFn,
ariaLabelledBy,
}: {
rooms: GuestsRoom[]
className: string
triggerFn?: () => void
ariaLabelledBy?: string
}) {
const intl = useIntl()
const parts = [
intl.formatMessage(
{
defaultMessage: "{totalRooms, plural, one {# room} other {# rooms}}",
},
{ totalRooms: rooms.length }
),
intl.formatMessage(
{
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) }
),
]
if (rooms.some((room) => room.childrenInRoom.length > 0)) {
parts.push(
intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{
totalChildren: rooms.reduce(
(acc, room) => acc + room.childrenInRoom.length,
0
),
}
)
)
}
return (
<Button
className={`${className} ${styles.btn}`}
type="button"
onPress={triggerFn}
aria-labelledby={ariaLabelledBy}
>
<Typography variant="Body/Paragraph/mdRegular">
<span className={styles.guestsAndRooms}>{parts.join(", ")}</span>
</Typography>
</Button>
)
}