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 { 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>
|
||||||
)
|
)
|
||||||
|
|||||||
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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
|||||||
Reference in New Issue
Block a user