Files
web/apps/scandic-web/components/GuestsRoomsPicker/index.tsx
Tobias Johansson b641f8387e Merged in fix/SW-2534-booking-widget-fixes (pull request #2129)
fix(SW-2534): Added validation error and fixed initial month in date picker for BW

* Merged in revert-version (pull request #2128)

revert including version in layouts and api responses

* revert including version in layouts and api responses


Approved-by: Linus Flood

* feat: prevent users from selecting the same room when there is no vacancy for it

* Merged in fix/no-my-stay-for-external-bookings (pull request #2126)

fix: Only link web and app bookings to my stay

Approved-by: Joakim Jäderberg

* Merged in fix/bookingwidget-fixes (pull request #2135)

Fix: (#SW-2663) bookingwidget mobile - Space around, border-radius and correct color on date field

* fix: booking widget mobile - padding around booking widget and date color

* Fixed rounded corners

* Reduced minimum size of column


Approved-by: Joakim Jäderberg

* Merged in fix/LOY-105-signupform-error-messages (pull request #2121)

feat(LOY-105): update signup form validation messages

* feat(LOY-105): improve signup form validation messages


Approved-by: Erik Tiekstra

* fix(LOY-199): add missing benefits link

* Merged in feat/SW-2340-aa-tracking-my-stay-pageview- (pull request #2133)

feat: SW-2340 Implemented tracking on my-stay, webview my-stay and receipt page

* feat: SW-2340 Implemented tracking on my-stay, webview my-stay and receipt page

* feat: SW-2340 Updated webview tracking

* feat: SW-2340 Updated receipt tracking


Approved-by: Linus Flood

* Merged in fix/SW-2804-missing-meeting-rooms (pull request #2138)

fix: return [] when we get a 404 for meeting rooms for a hotel

* fix: return [] when we get a 404 for meeting rooms for a hotel


Approved-by: Linus Flood

* feat(auth): limit output in session endpoint

* fix(SW-2376): Vertically centered previous/next buttons inside carousel cards

Approved-by: Matilda Landström

* fix(SW-2055): Surrounded ul inside JsonToHtml with a typography component

Approved-by: Matilda Landström

* fix(SW-2621): Breaking too long words on heading inside destination city pages

Approved-by: Matilda Landström

* Merged in fix/sw-2763-external-scripts (pull request #2104)

fix: try/catch external scripts to avoid them breaking our page #sw-2763

* fix: try/catch external scripts to avoid them breaking our page #sw-2763


Approved-by: Joakim Jäderberg

* fix: handle non loaded surprises in case they're returned as null from server

* feat(SW-2806): booking widget should not be blocked by sitewide alert

* Merged in fix/remove-on-error (pull request #2142)

fix: revert onError on the Script component

* fix: revert onError on the Script component

* Merged in fix/alert-icon (pull request #2139)

fix(SW-2807): alert icons

* fix(SW-2807): fix incorrect icon color on sitewide alert

* fix(SW-2807): change error icon


Approved-by: Erik Tiekstra
Approved-by: Linus Flood

* Merged in feat/SW-2760-SW-552-wellness-openinghours (pull request #2112)

fix(SW-2760, SW-2552): fix opening hours wellness sidepeek

* fix(SW-2760, SW-2552): fix opening hours wellness sidepeek


Approved-by: Erik Tiekstra

* Merged in feat/SW-1749-sidepeek-hotel-cta (pull request #2123)

feat(SW-1749): add link to hotel page in sidepeek

* feat(SW-1749): add link to hotel page in sidepeek


Approved-by: Matilda Landström

* fix(SW-2811): suggest list should follow where-to-field

* fix(SW-2451): placement of suggest list

* Merged in fix/SW-2684-booking-widget-text-overflow (pull request #2048)

fix(SW-2684): truncate overflowing text in booking widget

* fix: truncate overflowing text in booking widget

* fix: change Body to Typography and css selector fix


Approved-by: Hrishikesh Vaipurkar

* Merged in feat/SW-2800-lightbox-history-state (pull request #2147)

feat(SW-2800): closing image gallery and lightbox on using browser navigation

* feat(SW-2800): closing image gallery and lightbox on using browser navigation


Approved-by: Linus Flood

* Merged in fix/enter-details-footer-margin (pull request #2150)

fix: margin to footer on enter details

* fix: margin to footer on enter details

* Merged in fix/SW-2822-missing-meetingroom-images (pull request #2151)

fix: meeting rooms with missing images

* fix: meeting rooms with missing images


Approved-by: Linus Flood


Approved-by: Bianca Widstam
2025-05-20 06:49:27 +00:00

191 lines
5.1 KiB
TypeScript

"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 { BookingWidgetSchema } from "@/types/components/bookingWidget"
import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
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>{parts.join(", ")}</span>
</Typography>
</Button>
)
}