Merged in fix/book-500-disable-booking-widget-overlay (pull request #3149)

fix(BOOK-500): disable scrolling of backdrop on mobile booking modal

* BOOK-500: fixed scrolling issue behind open booking widget

* fix(BOOK-500): added customized hook for scrollLock

* BOOK-500: gave hook functions more descriptive names


Approved-by: Erik Tiekstra
This commit is contained in:
Matilda Haneling
2025-11-20 07:22:17 +00:00
parent fc02c957d2
commit 5fd379892e
3 changed files with 168 additions and 11 deletions

View File

@@ -6,6 +6,7 @@ import { use, useEffect, useRef, useState } from "react"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { useScrollLock } from "@scandic-hotels/common/hooks/useScrollLock"
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition" import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position" import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
import { debounce } from "@scandic-hotels/common/utils/debounce" import { debounce } from "@scandic-hotels/common/utils/debounce"
@@ -51,12 +52,11 @@ export default function BookingWidgetClient({
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const bookingWidgetRef = useRef(null) const bookingWidgetRef = useRef(null)
const lang = useLang() const lang = useLang()
const [originalOverflowY, setOriginalOverflowY] = useState<string | null>(
null
)
const bookingFlowConfig = useBookingFlowConfig() const bookingFlowConfig = useBookingFlowConfig()
const storedBookingWidgetState = useBookingWidgetState() const storedBookingWidgetState = useBookingWidgetState()
const { lockScroll, unlockScroll } = useScrollLock({
autoLock: false,
})
const shouldFetchAutoComplete = !!data.hotelId || !!data.city const shouldFetchAutoComplete = !!data.hotelId || !!data.city
const { data: destinationsData, isPending } = const { data: destinationsData, isPending } =
@@ -165,15 +165,13 @@ export default function BookingWidgetClient({
}, [selectedLocation, methods]) }, [selectedLocation, methods])
function closeMobileSearch() { function closeMobileSearch() {
unlockScroll()
setIsOpen(false) setIsOpen(false)
const overflowY = originalOverflowY ?? "visible"
document.body.style.overflowY = overflowY
} }
function openMobileSearch() { function openMobileSearch() {
lockScroll()
setIsOpen(true) setIsOpen(true)
setOriginalOverflowY(document.body.style.overflowY)
document.body.style.overflowY = "hidden"
} }
useEffect(() => { useEffect(() => {
@@ -181,7 +179,7 @@ export default function BookingWidgetClient({
debounce(([entry]) => { debounce(([entry]) => {
if (entry.contentRect.width > 768) { if (entry.contentRect.width > 768) {
setIsOpen(false) setIsOpen(false)
document.body.style.removeProperty("overflow-y") unlockScroll()
} }
}) })
) )
@@ -191,7 +189,7 @@ export default function BookingWidgetClient({
return () => { return () => {
observer.unobserve(document.body) observer.unobserve(document.body)
} }
}, []) }, [unlockScroll])
useEffect(() => { useEffect(() => {
if (!window?.sessionStorage || !window?.localStorage) return if (!window?.sessionStorage || !window?.localStorage) return
@@ -247,6 +245,7 @@ export default function BookingWidgetClient({
data-booking-widget-open={isOpen} data-booking-widget-open={isOpen}
> >
<MobileToggleButton openMobileSearch={openMobileSearch} /> <MobileToggleButton openMobileSearch={openMobileSearch} />
<div className={styles.backdrop} onClick={closeMobileSearch} />
<div className={formContainerClassNames}> <div className={formContainerClassNames}>
<button <button
className={styles.close} className={styles.close}
@@ -257,7 +256,6 @@ export default function BookingWidgetClient({
</button> </button>
<Form type={type} onClose={closeMobileSearch} /> <Form type={type} onClose={closeMobileSearch} />
</div> </div>
<div className={styles.backdrop} onClick={closeMobileSearch} />
</section> </section>
</FormProvider> </FormProvider>
) )

View File

@@ -0,0 +1,158 @@
import { useRef, useState } from "react"
import { useIsomorphicLayoutEffect } from "usehooks-ts"
// Modified hook useScrollLock originally from usehooks-ts
/** Hook options. */
type UseScrollLockOptions = {
/**
* Whether to lock the scroll initially.
* @default true
*/
autoLock?: boolean
/**
* The target element to lock the scroll (default is the body element).
* @default document.body
*/
lockTarget?: HTMLElement | string
/**
* Whether to prevent width reflow when locking the scroll.
* @default true
*/
widthReflow?: boolean
}
/** Hook return type. */
type UseScrollLockReturn = {
/** Whether the scroll is locked. */
isLocked: boolean
/** Lock the scroll. */
lockScroll: () => void
/** Unlock the scroll. */
unlockScroll: () => void
}
type OriginalStyle = {
overflow: CSSStyleDeclaration["overflow"]
paddingRight: CSSStyleDeclaration["paddingRight"]
position: CSSStyleDeclaration["position"]
top: CSSStyleDeclaration["top"]
scrollPosition: number
}
const IS_SERVER = typeof window === "undefined"
/**
* A custom hook that locks and unlocks scroll.
* @param {UseScrollLockOptions} [options] - Options to configure the hook, by default it will lock the scroll automatically.
* @returns {UseScrollLockReturn} - An object containing the lockScroll and unlockScroll functions.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-scroll-lock)
* @example
* ```tsx
* // Lock the scroll when the modal is mounted, and unlock it when it's unmounted
* useScrollLock()
* ```
* @example
* ```tsx
* // Manually lock and unlock the scroll
* const { lockScroll, unlockScroll } = useScrollLock({ autoLock: false })
*
* return (
* <div>
* <button onClick={lockScroll}>Lock</button>
* <button onClick={unlockScroll}>Unlock</button>
* </div>
* )
* ```
*/
export function useScrollLock(
options: UseScrollLockOptions = {}
): UseScrollLockReturn {
const { autoLock = true, lockTarget, widthReflow = true } = options
const [isLocked, setIsLocked] = useState(false)
const target = useRef<HTMLElement | null>(null)
const originalStyle = useRef<OriginalStyle | null>(null)
let scrollPosition = 0
const lockScroll = () => {
if (target.current) {
const { overflow, paddingRight, position, top } = target.current.style
// Save the original styles
originalStyle.current = {
overflow,
paddingRight,
position,
top,
scrollPosition,
}
originalStyle.current.scrollPosition = window.scrollY
// Prevent width reflow
if (widthReflow) {
// Use window inner width if body is the target as global scrollbar isn't part of the document
const offsetWidth =
target.current === document.body
? window.innerWidth
: target.current.offsetWidth
// Get current computed padding right in pixels
const currentPaddingRight =
parseInt(window.getComputedStyle(target.current).paddingRight, 10) ||
0
const scrollbarWidth = offsetWidth - target.current.scrollWidth
target.current.style.paddingRight = `${scrollbarWidth + currentPaddingRight}px`
}
// Lock the scroll
target.current.style.overflow = "hidden"
target.current.style.position = "fixed"
document.body.style.top = `-${originalStyle.current.scrollPosition}px`
setIsLocked(true)
}
}
const unlockScroll = () => {
if (target.current && originalStyle.current) {
target.current.style.overflow = originalStyle.current.overflow
target.current.style.top = originalStyle.current.top
target.current.style.position = originalStyle.current.position
// Only reset padding right if we changed it
if (widthReflow) {
target.current.style.paddingRight = originalStyle.current.paddingRight
}
//Scroll to original scroll position
window.scrollTo({
left: 0,
top: originalStyle.current.scrollPosition,
behavior: "instant",
})
}
setIsLocked(false)
}
useIsomorphicLayoutEffect(() => {
if (IS_SERVER) return
if (lockTarget) {
target.current =
typeof lockTarget === "string"
? document.querySelector(lockTarget)
: lockTarget
}
if (!target.current) {
target.current = document.body
}
if (autoLock) {
lockScroll()
}
return () => {
unlockScroll()
}
}, [autoLock, lockTarget, widthReflow])
return { isLocked, lockScroll, unlockScroll }
}

View File

@@ -7,6 +7,7 @@ html,
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100vw;
} }
.root { .root {