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
This commit is contained in:
Niclas Edenvin
2025-02-21 09:13:29 +00:00
parent a0286603db
commit 9cd648fd65
2 changed files with 60 additions and 17 deletions

View File

@@ -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<HTMLDivElement>(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}
>
<div className={styles.iconWrapper}>
<div className={styles.circle} data-checked={isComplete}>

View File

@@ -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<StickyElement[]>((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,
}
}