implement View Transitions API for smoother page navigation and replace the current implementation
This commit is contained in:
@@ -1,58 +1,93 @@
|
|||||||
/* Page Transition Animations */
|
/* Page Wrapper */
|
||||||
.page-wrapper {
|
.page-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enter animations */
|
/* View Transitions API - Native browser page transitions */
|
||||||
.page-enter {
|
/* These run automatically when the browser supports view transitions */
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(20px);
|
/* Forward navigation (going deeper) */
|
||||||
|
::view-transition-old(root):only-child {
|
||||||
|
animation: slide-out-left 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-enter-active {
|
::view-transition-new(root):only-child {
|
||||||
opacity: 1;
|
animation: slide-in-right 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transform: translateX(0);
|
|
||||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Exit animations */
|
/* Back navigation (going up) */
|
||||||
.page-exit {
|
html[data-transition='back']::view-transition-old(root):only-child {
|
||||||
opacity: 1;
|
animation: slide-out-right 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transform: translateX(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-exit-active {
|
html[data-transition='back']::view-transition-new(root):only-child {
|
||||||
opacity: 0;
|
animation: slide-in-left 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transform: translateX(-20px);
|
|
||||||
transition: opacity 200ms ease-in, transform 200ms ease-in;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specific animation for back navigation */
|
/* Fade for auth pages */
|
||||||
.page-back-enter {
|
html[data-transition='fade']::view-transition-old(root):only-child,
|
||||||
opacity: 0;
|
html[data-transition='fade']::view-transition-new(root):only-child {
|
||||||
transform: translateX(-20px);
|
animation: fade 200ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-back-enter-active {
|
/* Animation keyframes */
|
||||||
opacity: 1;
|
@keyframes slide-out-left {
|
||||||
transform: translateX(0);
|
from {
|
||||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-30%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-back-exit {
|
@keyframes slide-in-right {
|
||||||
opacity: 1;
|
from {
|
||||||
transform: translateX(0);
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-back-exit-active {
|
@keyframes slide-out-right {
|
||||||
opacity: 0;
|
from {
|
||||||
transform: translateX(20px);
|
transform: translateX(0);
|
||||||
transition: opacity 200ms ease-in, transform 200ms ease-in;
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal/overlay animations */
|
@keyframes slide-in-left {
|
||||||
|
from {
|
||||||
|
transform: translateX(-30%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal/overlay animations (still useful for non-route transitions) */
|
||||||
.modal-enter {
|
.modal-enter {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95) translateY(10px);
|
transform: scale(0.95) translateY(10px);
|
||||||
@@ -61,7 +96,9 @@
|
|||||||
.modal-enter-active {
|
.modal-enter-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1) translateY(0);
|
transform: scale(1) translateY(0);
|
||||||
transition: opacity 250ms ease-out, transform 250ms ease-out;
|
transition:
|
||||||
|
opacity 250ms ease-out,
|
||||||
|
transform 250ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-exit {
|
.modal-exit {
|
||||||
@@ -72,26 +109,9 @@
|
|||||||
.modal-exit-active {
|
.modal-exit-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95) translateY(10px);
|
transform: scale(0.95) translateY(10px);
|
||||||
transition: opacity 200ms ease-in, transform 200ms ease-in;
|
transition:
|
||||||
}
|
opacity 200ms ease-in,
|
||||||
|
transform 200ms ease-in;
|
||||||
/* Fade animation for simple transitions */
|
|
||||||
.fade-enter {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 250ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-exit {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-exit-active {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 200ms ease-in;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slide up animation for bottom sheets/panels */
|
/* Slide up animation for bottom sheets/panels */
|
||||||
@@ -103,7 +123,9 @@
|
|||||||
.slide-up-enter-active {
|
.slide-up-enter-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
transition:
|
||||||
|
opacity 300ms ease-out,
|
||||||
|
transform 300ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-exit {
|
.slide-up-exit {
|
||||||
@@ -114,7 +136,9 @@
|
|||||||
.slide-up-exit-active {
|
.slide-up-exit-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
transition: opacity 250ms ease-in, transform 250ms ease-in;
|
transition:
|
||||||
|
opacity 250ms ease-in,
|
||||||
|
transform 250ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading spinner animation */
|
/* Loading spinner animation */
|
||||||
@@ -171,33 +195,49 @@
|
|||||||
.stagger-enter-active {
|
.stagger-enter-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
transition:
|
||||||
|
opacity 300ms ease-out,
|
||||||
|
transform 300ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add animation delays for staggered effects */
|
/* Add animation delays for staggered effects */
|
||||||
.stagger-enter-active:nth-child(1) { transition-delay: 0ms; }
|
.stagger-enter-active:nth-child(1) {
|
||||||
.stagger-enter-active:nth-child(2) { transition-delay: 50ms; }
|
transition-delay: 0ms;
|
||||||
.stagger-enter-active:nth-child(3) { transition-delay: 100ms; }
|
}
|
||||||
.stagger-enter-active:nth-child(4) { transition-delay: 150ms; }
|
.stagger-enter-active:nth-child(2) {
|
||||||
.stagger-enter-active:nth-child(5) { transition-delay: 200ms; }
|
transition-delay: 50ms;
|
||||||
.stagger-enter-active:nth-child(6) { transition-delay: 250ms; }
|
}
|
||||||
.stagger-enter-active:nth-child(7) { transition-delay: 300ms; }
|
.stagger-enter-active:nth-child(3) {
|
||||||
.stagger-enter-active:nth-child(8) { transition-delay: 350ms; }
|
transition-delay: 100ms;
|
||||||
|
}
|
||||||
|
.stagger-enter-active:nth-child(4) {
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
|
.stagger-enter-active:nth-child(5) {
|
||||||
|
transition-delay: 200ms;
|
||||||
|
}
|
||||||
|
.stagger-enter-active:nth-child(6) {
|
||||||
|
transition-delay: 250ms;
|
||||||
|
}
|
||||||
|
.stagger-enter-active:nth-child(7) {
|
||||||
|
transition-delay: 300ms;
|
||||||
|
}
|
||||||
|
.stagger-enter-active:nth-child(8) {
|
||||||
|
transition-delay: 350ms;
|
||||||
|
}
|
||||||
|
|
||||||
/* Reduced motion for accessibility */
|
/* Reduced motion for accessibility */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.page-enter,
|
/* Disable View Transitions for users who prefer reduced motion */
|
||||||
.page-enter-active,
|
::view-transition-old(root),
|
||||||
.page-exit,
|
::view-transition-new(root) {
|
||||||
.page-exit-active,
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-enter,
|
.modal-enter,
|
||||||
.modal-enter-active,
|
.modal-enter-active,
|
||||||
.modal-exit,
|
.modal-exit,
|
||||||
.modal-exit-active,
|
.modal-exit-active,
|
||||||
.fade-enter,
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-exit,
|
|
||||||
.fade-exit-active,
|
|
||||||
.interactive-element,
|
.interactive-element,
|
||||||
.stagger-enter,
|
.stagger-enter,
|
||||||
.stagger-enter-active {
|
.stagger-enter-active {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
import { flushSync } from 'react-dom'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
|
||||||
import './PageTransition.css'
|
import './PageTransition.css'
|
||||||
|
|
||||||
// Route hierarchy for determining navigation direction
|
// Route hierarchy for determining navigation direction
|
||||||
@@ -14,9 +14,11 @@ const routeHierarchy = {
|
|||||||
'/activities': 1,
|
'/activities': 1,
|
||||||
'/points': 1,
|
'/points': 1,
|
||||||
'/labels': 1,
|
'/labels': 1,
|
||||||
|
'/projects': 1,
|
||||||
'/login': 0,
|
'/login': 0,
|
||||||
'/signup': 1,
|
'/signup': 1,
|
||||||
'/landing': 0,
|
'/landing': 0,
|
||||||
|
'/archived': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRouteLevel = pathname => {
|
const getRouteLevel = pathname => {
|
||||||
@@ -32,16 +34,23 @@ const getRouteLevel = pathname => {
|
|||||||
if (pathname.includes('/chores/') && pathname.includes('/history')) {
|
if (pathname.includes('/chores/') && pathname.includes('/history')) {
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
|
if (pathname.includes('/chores/') && pathname.includes('/timer')) {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
pathname.includes('/chores/') &&
|
pathname.includes('/chores/') &&
|
||||||
!pathname.includes('/edit') &&
|
!pathname.includes('/edit') &&
|
||||||
!pathname.includes('/history')
|
!pathname.includes('/history') &&
|
||||||
|
!pathname.includes('/timer')
|
||||||
) {
|
) {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if (pathname.includes('/things/')) {
|
if (pathname.includes('/things/')) {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
if (pathname.includes('/settings/')) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
// Default level
|
// Default level
|
||||||
return 1
|
return 1
|
||||||
@@ -49,55 +58,72 @@ const getRouteLevel = pathname => {
|
|||||||
|
|
||||||
const PageTransition = ({ children }) => {
|
const PageTransition = ({ children }) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const prevLocation = useRef(location)
|
const prevLevel = useRef(0)
|
||||||
const isNavigatingBack = useRef(false)
|
const [displayLocation, setDisplayLocation] = useState(location)
|
||||||
const nodeRef = useRef(null)
|
const isFirstRender = useRef(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const currentLevel = getRouteLevel(location.pathname)
|
// Skip transition on first render
|
||||||
const previousLevel = getRouteLevel(prevLocation.current.pathname)
|
if (isFirstRender.current) {
|
||||||
|
isFirstRender.current = false
|
||||||
// Determine if we're navigating back (to a higher level in hierarchy)
|
setDisplayLocation(location)
|
||||||
// isNavigatingBack.current = currentLevel < previousLevel
|
prevLevel.current = getRouteLevel(location.pathname)
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
return
|
||||||
|
|
||||||
prevLocation.current = location
|
|
||||||
}, [location])
|
|
||||||
|
|
||||||
const getTransitionClasses = () => {
|
|
||||||
if (
|
|
||||||
location.pathname.includes('/login') ||
|
|
||||||
location.pathname.includes('/signup') ||
|
|
||||||
location.pathname.includes('/landing')
|
|
||||||
) {
|
|
||||||
return 'fade' // Use fade for auth pages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNavigatingBack.current ? 'page-back' : 'page'
|
// Don't transition if location hasn't actually changed
|
||||||
}
|
if (location.pathname === displayLocation.pathname) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLevel = getRouteLevel(location.pathname)
|
||||||
|
const previousLevel = prevLevel.current
|
||||||
|
|
||||||
|
// Determine navigation direction
|
||||||
|
const isBack = currentLevel < previousLevel
|
||||||
|
const isFade =
|
||||||
|
location.pathname.includes('/login') ||
|
||||||
|
location.pathname.includes('/signup') ||
|
||||||
|
location.pathname.includes('/landing') ||
|
||||||
|
location.pathname.includes('/auth/')
|
||||||
|
|
||||||
|
// Apply transition type as data attribute for CSS
|
||||||
|
document.documentElement.dataset.transition = isFade
|
||||||
|
? 'fade'
|
||||||
|
: isBack
|
||||||
|
? 'back'
|
||||||
|
: 'forward'
|
||||||
|
|
||||||
|
// Trigger View Transition if supported
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
// flushSync forces React to update the DOM synchronously
|
||||||
|
// This ensures the View Transition captures the actual DOM change
|
||||||
|
flushSync(() => {
|
||||||
|
setDisplayLocation(location)
|
||||||
|
})
|
||||||
|
// Scroll to top after DOM update
|
||||||
|
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Fallback for browsers without View Transitions API
|
||||||
|
setDisplayLocation(location)
|
||||||
|
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update refs
|
||||||
|
prevLevel.current = currentLevel
|
||||||
|
}, [location, displayLocation.pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionGroup component={null}>
|
<div
|
||||||
<CSSTransition
|
className='page-wrapper'
|
||||||
key={location.pathname}
|
style={{
|
||||||
classNames={getTransitionClasses()}
|
paddingBottom: `var(--safe-area-inset-bottom, 0px)`,
|
||||||
timeout={{
|
}}
|
||||||
enter: 50,
|
>
|
||||||
exit: 50,
|
{displayLocation.pathname === location.pathname ? children : null}
|
||||||
}}
|
</div>
|
||||||
nodeRef={nodeRef}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={nodeRef}
|
|
||||||
className='page-wrapper'
|
|
||||||
style={{
|
|
||||||
paddingBottom: `var(--safe-area-inset-bottom, 0px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
</TransitionGroup>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user