This commit is contained in:
Tobias Johansson
2024-11-27 16:11:10 +01:00
parent 2c7842f79a
commit d3dad4c73d
2 changed files with 64 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { useEffect, useState } from "react" import { animate, motion, scroll, useAnimate } from "framer-motion"
import { useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details" import { useEnterDetailsStore } from "@/stores/enter-details"
@@ -33,6 +34,10 @@ export default function SectionAccordion({
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" }) const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" }) const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" })
const accordionRef = useRef<HTMLDivElement>(null)
const [scope, animate] = useAnimate()
useEffect(() => { useEffect(() => {
if (step === StepEnum.selectBed && bedType) { if (step === StepEnum.selectBed && bedType) {
setTitle(bedType.description) setTitle(bedType.description)
@@ -55,6 +60,49 @@ export default function SectionAccordion({
setIsOpen(currentStep === step) setIsOpen(currentStep === step)
}, [currentStep, setIsOpen, step]) }, [currentStep, setIsOpen, step])
useEffect(() => {
if (!accordionRef.current) return
const animateAccordion = async () => {
// Measure the initial position of the accordion
const initialOffset = accordionRef.current?.offsetTop || 0
// Start the accordion expansion animation
const accordionAnimation = animate(
scope.current,
{
gridTemplateRows: isOpen
? "var(--header-height) 1fr"
: "var(--header-height) 0fr",
},
{ duration: 0.3, ease: "easeInOut" }
)
if (isOpen) {
// Start scrolling immediately
const scrollAnimation = animate(
window.scrollY,
initialOffset - 100, // 100px offset from top
{
duration: 0.3,
ease: "easeInOut",
onUpdate: (latest) => {
window.scrollTo(0, latest)
},
}
)
// Wait for both animations to complete
await Promise.all([accordionAnimation, scrollAnimation])
} else {
// If closing, just wait for the accordion animation
await accordionAnimation
}
}
animateAccordion()
}, [isOpen, scope, animate])
function onModify() { function onModify() {
navigate(step) navigate(step)
} }
@@ -62,7 +110,13 @@ export default function SectionAccordion({
const textColor = const textColor =
isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled" isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
return ( return (
<div className={styles.accordion} data-open={isOpen} data-step={step}> <motion.div
className={styles.accordion}
data-open={isOpen}
data-step={step}
ref={scope}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className={styles.iconWrapper}> <div className={styles.iconWrapper}>
<div className={styles.circle} data-checked={isComplete}> <div className={styles.circle} data-checked={isComplete}>
{isComplete ? ( {isComplete ? (
@@ -70,7 +124,7 @@ export default function SectionAccordion({
) : null} ) : null}
</div> </div>
</div> </div>
<header className={styles.header}> <header className={styles.header} ref={accordionRef}>
<button <button
onClick={onModify} onClick={onModify}
disabled={!isComplete} disabled={!isComplete}
@@ -97,6 +151,6 @@ export default function SectionAccordion({
<div className={styles.content}> <div className={styles.content}>
<div className={styles.contentWrapper}>{children}</div> <div className={styles.contentWrapper}>{children}</div>
</div> </div>
</div> </motion.div>
) )
} }

View File

@@ -5,7 +5,7 @@
gap: var(--Spacing-x3); gap: var(--Spacing-x3);
width: 100%; width: 100%;
padding-top: var(--Spacing-x3); padding-top: var(--Spacing-x3);
transition: 0.4s ease-out; /* transition: 0.4s ease-out; */
display: grid; display: grid;
grid-template-areas: "circle header" "content content"; grid-template-areas: "circle header" "content content";
@@ -13,6 +13,8 @@
grid-template-rows: var(--header-height) 0fr; grid-template-rows: var(--header-height) 0fr;
column-gap: var(--Spacing-x-one-and-half); column-gap: var(--Spacing-x-one-and-half);
transform-origin: top;
} }
.accordion:last-child { .accordion:last-child {
@@ -79,9 +81,9 @@
background-color: var(--Base-Surface-Subtle-Hover); background-color: var(--Base-Surface-Subtle-Hover);
} }
.accordion[data-open="true"] { /* .accordion[data-open="true"] {
grid-template-rows: var(--header-height) 1fr; grid-template-rows: var(--header-height) 1fr;
} } */
.contentWrapper { .contentWrapper {
padding-bottom: var(--Spacing-x3); padding-bottom: var(--Spacing-x3);
@@ -91,6 +93,7 @@
overflow: hidden; overflow: hidden;
grid-area: content; grid-area: content;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
transform-origin: top;
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {