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:
@@ -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 />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user