From 9cd648fd651a943e1119804e33314c36949460a7 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Fri, 21 Feb 2025 09:13:29 +0000 Subject: [PATCH] Merged in feat/sw-1513-anchoring-on-enter-details (pull request #1379) feat(SW-1513): scroll to new section on enter details page * feat(SW-1513): scroll to new section on enter details page Approved-by: Simon.Emanuelsson --- .../EnterDetails/SectionAccordion/index.tsx | 27 +++++++++- hooks/useStickyPosition.ts | 50 +++++++++++++------ 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index 2979d6018..f0dafe06e 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -1,5 +1,5 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" @@ -12,6 +12,7 @@ import { import { CheckIcon, ChevronDownIcon } from "@/components/Icons" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useStickyPosition from "@/hooks/useStickyPosition" import styles from "./sectionAccordion.module.css" @@ -30,6 +31,7 @@ export default function SectionAccordion({ selectRoomStatus(state, roomIndex) ) + const stickyPosition = useStickyPosition({}) const setStep = useEnterDetailsStore((state) => state.actions.setStep) const { bedType, breakfast } = useEnterDetailsStore((state) => selectRoom(state, roomIndex) @@ -67,8 +69,28 @@ export default function SectionAccordion({ setIsComplete(isValid) }, [isValid, setIsComplete]) + const accordionRef = useRef(null) + useEffect(() => { - setIsOpen(roomStatus.currentStep === step && currentRoomIndex === roomIndex) + const shouldBeOpen = + roomStatus.currentStep === step && currentRoomIndex === roomIndex + + setIsOpen(shouldBeOpen) + + // Scroll to this section when it is opened, but wait for the accordion animations to + // finish, else the height calculations will not be correct and the scroll position + // will be off. + if (shouldBeOpen) { + setTimeout(() => { + if (accordionRef.current) { + window.scrollTo({ + top: accordionRef.current.offsetTop - stickyPosition.getTopOffset(), + behavior: "smooth", + }) + } + }, 250) + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentRoomIndex, roomIndex, roomStatus.currentStep, setIsOpen, step]) function onModify() { @@ -98,6 +120,7 @@ export default function SectionAccordion({ className={styles.accordion} data-section-open={isOpen} data-step={step} + ref={accordionRef} >
diff --git a/hooks/useStickyPosition.ts b/hooks/useStickyPosition.ts index 08fce174b..8cc9a4eef 100644 --- a/hooks/useStickyPosition.ts +++ b/hooks/useStickyPosition.ts @@ -1,9 +1,10 @@ "use client" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { env } from "@/env/client" import useStickyPositionStore, { + type StickyElement, type StickyElementNameEnum, } from "@/stores/sticky-position" @@ -71,6 +72,36 @@ export default function useStickyPosition({ } }, [ref, name, group, registerSticky, unregisterSticky, updateHeights]) + /** + * Get the top position of element at index `index` + * + * Calculates the total height of all sticky elements _before_ the element + * at position `index`. If `index` is not provided all elements are included + * in the calculation. Takes grouping into consideration (only counts one + * element per group) + */ + const getTopOffset = useCallback( + (index?: number) => { + // Get the group name of the current sticky element. + // This will be used to only count one element per group. + const elementGroup = index ? stickyElements[index].group : undefined + + return stickyElements + .slice(0, index) + .reduce((acc, curr) => { + if ( + (elementGroup && curr.group === elementGroup) || + acc.some((elem: StickyElement) => elem.group === curr.group) + ) { + return acc + } + return [...acc, curr] + }, []) + .reduce((acc, el) => acc + el.height, baseTopOffset) + }, + [baseTopOffset, stickyElements] + ) + useEffect(() => { if (ref) { // Find the index of the current sticky element in the array of stickyElements. @@ -78,25 +109,13 @@ export default function useStickyPosition({ const index = stickyElements.findIndex((el) => el.ref === ref) if (index !== -1 && ref.current) { - // Get the group name of the current sticky element. - // This will be used to filter out other elements in the same group. - const currentGroup = stickyElements[index].group - - // Calculate the top offset for the current element. - // We sum the heights of all elements that appear before the current element - // in the stickyElements array, but only if they belong to a different group. - // This ensures that elements in the same group don't stack on top of each other. - const topOffset = stickyElements - .slice(0, index) - .filter((el) => el.group !== currentGroup) - .reduce((acc, el) => acc + el.height, baseTopOffset) - + const topOffset = getTopOffset(index) // Apply the calculated top offset to the current element's style. // This positions the element at the correct location within the document. ref.current.style.top = `${topOffset}px` } } - }, [baseTopOffset, stickyElements, ref]) + }, [baseTopOffset, stickyElements, ref, getTopOffset]) useEffect(() => { if (!resizeObserver) { @@ -128,5 +147,6 @@ export default function useStickyPosition({ return { currentHeight: ref?.current?.offsetHeight || null, allElements: getAllElements(), + getTopOffset, } }