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
191 lines
5.1 KiB
TypeScript
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>
|
|
)
|
|
}
|