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