diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx index 31df3cc84..aaf2d7b83 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx @@ -1,9 +1,8 @@ "use client" -import { useState } from "react" import { useIntl } from "react-intl" -import { useScrollToTop } from "@scandic-hotels/common/hooks/useScrollToTop" +import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop" import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton" import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled" @@ -32,24 +31,8 @@ export function PreviousStaysSidePeek({ const intl = useIntl() const lang = useLang() - const [scrollContainer, setScrollContainer] = useState( - null - ) - - const scrollContainerRef = (node: HTMLDivElement | null) => { - const parent = node?.parentElement - // SidePeekSelfControlled renders children twice: in the modal & in an sr-only SEO wrapper. - // We filter out the SEO wrapper to get the actual scrollable container. - if (parent && !parent.classList.contains("sr-only")) { - setScrollContainer(parent) - } - } - - const { showBackToTop, scrollToTop } = useScrollToTop({ - threshold: 200, - element: scrollContainer, - refScrollable: true, - }) + const { scrollContainerRef, showBackToTop, scrollToTop } = + useSidePeekScrollToTop() const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = trpc.user.stays.previous.useInfiniteQuery( diff --git a/packages/common/hooks/useScrollToTop.ts b/packages/common/hooks/useScrollToTop.ts index 6a41cc5a8..f6f1d9528 100644 --- a/packages/common/hooks/useScrollToTop.ts +++ b/packages/common/hooks/useScrollToTop.ts @@ -29,11 +29,12 @@ export function useScrollToTop({ const scrollTarget = refScrollable && targetElement ? targetElement : window function handleScroll() { + const currentElement = element ?? elementRef?.current let position = window.scrollY - if (targetElement) { + if (currentElement) { position = refScrollable - ? targetElement.scrollTop - : targetElement.getBoundingClientRect().top * -1 + ? currentElement.scrollTop + : currentElement.getBoundingClientRect().top * -1 } setShowBackToTop(position > threshold) } @@ -47,8 +48,9 @@ export function useScrollToTop({ if (targetElement) { if (refScrollable) { targetElement.scrollTo({ top: 0, behavior: "smooth" }) + } else { + window.scrollTo({ top: targetElement.offsetTop, behavior: "smooth" }) } - window.scrollTo({ top: targetElement.offsetTop, behavior: "smooth" }) } else { window.scrollTo({ top: 0, behavior: "smooth" }) } diff --git a/packages/common/hooks/useSidePeekScrollToTop.ts b/packages/common/hooks/useSidePeekScrollToTop.ts new file mode 100644 index 000000000..c8b55d931 --- /dev/null +++ b/packages/common/hooks/useSidePeekScrollToTop.ts @@ -0,0 +1,53 @@ +import { useState } from "react" + +import { useScrollToTop } from "./useScrollToTop" + +interface UseSidePeekScrollToTopProps { + threshold?: number +} + +/** + * Hook for managing scroll-to-top functionality within a SidePeekSelfControlled component. + * Automatically finds the scrollable dialog container from the SidePeek DOM structure. + * + * @example + * ```tsx + * const { scrollContainerRef, showBackToTop, scrollToTop } = useSidePeekScrollToTop() + * + * return ( + * + *
+ * {content} + * {showBackToTop && } + *
+ *
+ * ) + * ``` + */ +export function useSidePeekScrollToTop({ + threshold = 200, +}: UseSidePeekScrollToTopProps = {}) { + const [scrollContainer, setScrollContainer] = useState( + null + ) + + const scrollContainerRef = (node: HTMLDivElement | null) => { + // SidePeekSelfControlled renders children twice: in the modal & in an sr-only SEO wrapper. + // We filter out the SEO wrapper to get the actual scrollable container. + // DOM structure: Dialog (scrollable) -> aside -> sidePeekContent -> our content div + const sidePeekContent = node?.parentElement + const aside = sidePeekContent?.parentElement + const dialog = aside?.parentElement + if (dialog && !sidePeekContent?.classList.contains("sr-only")) { + setScrollContainer(dialog) + } + } + + const { showBackToTop, scrollToTop } = useScrollToTop({ + threshold, + element: scrollContainer, + refScrollable: true, + }) + + return { scrollContainerRef, showBackToTop, scrollToTop } +}