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 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Enter animations */
|
||||
.page-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
/* View Transitions API - Native browser page transitions */
|
||||
/* These run automatically when the browser supports view transitions */
|
||||
|
||||
/* 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 {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
||||
::view-transition-new(root):only-child {
|
||||
animation: slide-in-right 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Exit animations */
|
||||
.page-exit {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
/* Back navigation (going up) */
|
||||
html[data-transition='back']::view-transition-old(root):only-child {
|
||||
animation: slide-out-right 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.page-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
transition: opacity 200ms ease-in, transform 200ms ease-in;
|
||||
html[data-transition='back']::view-transition-new(root):only-child {
|
||||
animation: slide-in-left 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Specific animation for back navigation */
|
||||
.page-back-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
/* Fade for auth pages */
|
||||
html[data-transition='fade']::view-transition-old(root):only-child,
|
||||
html[data-transition='fade']::view-transition-new(root):only-child {
|
||||
animation: fade 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.page-back-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
||||
/* Animation keyframes */
|
||||
@keyframes slide-out-left {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(-30%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-back-exit {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-back-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
transition: opacity 200ms ease-in, transform 200ms ease-in;
|
||||
@keyframes slide-out-right {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
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 {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
@@ -61,7 +96,9 @@
|
||||
.modal-enter-active {
|
||||
opacity: 1;
|
||||
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 {
|
||||
@@ -72,26 +109,9 @@
|
||||
.modal-exit-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
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;
|
||||
transition:
|
||||
opacity 200ms ease-in,
|
||||
transform 200ms ease-in;
|
||||
}
|
||||
|
||||
/* Slide up animation for bottom sheets/panels */
|
||||
@@ -103,7 +123,9 @@
|
||||
.slide-up-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 300ms ease-out, transform 300ms ease-out;
|
||||
transition:
|
||||
opacity 300ms ease-out,
|
||||
transform 300ms ease-out;
|
||||
}
|
||||
|
||||
.slide-up-exit {
|
||||
@@ -114,7 +136,9 @@
|
||||
.slide-up-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition: opacity 250ms ease-in, transform 250ms ease-in;
|
||||
transition:
|
||||
opacity 250ms ease-in,
|
||||
transform 250ms ease-in;
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@@ -171,33 +195,49 @@
|
||||
.stagger-enter-active {
|
||||
opacity: 1;
|
||||
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 */
|
||||
.stagger-enter-active:nth-child(1) { transition-delay: 0ms; }
|
||||
.stagger-enter-active:nth-child(2) { transition-delay: 50ms; }
|
||||
.stagger-enter-active:nth-child(3) { 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; }
|
||||
.stagger-enter-active:nth-child(1) {
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(2) {
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(3) {
|
||||
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 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.page-enter,
|
||||
.page-enter-active,
|
||||
.page-exit,
|
||||
.page-exit-active,
|
||||
/* Disable View Transitions for users who prefer reduced motion */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.modal-enter,
|
||||
.modal-enter-active,
|
||||
.modal-exit,
|
||||
.modal-exit-active,
|
||||
.fade-enter,
|
||||
.fade-enter-active,
|
||||
.fade-exit,
|
||||
.fade-exit-active,
|
||||
.interactive-element,
|
||||
.stagger-enter,
|
||||
.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 { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import './PageTransition.css'
|
||||
|
||||
// Route hierarchy for determining navigation direction
|
||||
@@ -14,9 +14,11 @@ const routeHierarchy = {
|
||||
'/activities': 1,
|
||||
'/points': 1,
|
||||
'/labels': 1,
|
||||
'/projects': 1,
|
||||
'/login': 0,
|
||||
'/signup': 1,
|
||||
'/landing': 0,
|
||||
'/archived': 1,
|
||||
}
|
||||
|
||||
const getRouteLevel = pathname => {
|
||||
@@ -32,16 +34,23 @@ const getRouteLevel = pathname => {
|
||||
if (pathname.includes('/chores/') && pathname.includes('/history')) {
|
||||
return 3
|
||||
}
|
||||
if (pathname.includes('/chores/') && pathname.includes('/timer')) {
|
||||
return 3
|
||||
}
|
||||
if (
|
||||
pathname.includes('/chores/') &&
|
||||
!pathname.includes('/edit') &&
|
||||
!pathname.includes('/history')
|
||||
!pathname.includes('/history') &&
|
||||
!pathname.includes('/timer')
|
||||
) {
|
||||
return 2
|
||||
}
|
||||
if (pathname.includes('/things/')) {
|
||||
return 2
|
||||
}
|
||||
if (pathname.includes('/settings/')) {
|
||||
return 2
|
||||
}
|
||||
|
||||
// Default level
|
||||
return 1
|
||||
@@ -49,55 +58,72 @@ const getRouteLevel = pathname => {
|
||||
|
||||
const PageTransition = ({ children }) => {
|
||||
const location = useLocation()
|
||||
const prevLocation = useRef(location)
|
||||
const isNavigatingBack = useRef(false)
|
||||
const nodeRef = useRef(null)
|
||||
const prevLevel = useRef(0)
|
||||
const [displayLocation, setDisplayLocation] = useState(location)
|
||||
const isFirstRender = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
const currentLevel = getRouteLevel(location.pathname)
|
||||
const previousLevel = getRouteLevel(prevLocation.current.pathname)
|
||||
|
||||
// Determine if we're navigating back (to a higher level in hierarchy)
|
||||
// isNavigatingBack.current = currentLevel < previousLevel
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
||||
|
||||
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
|
||||
useLayoutEffect(() => {
|
||||
// Skip transition on first render
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false
|
||||
setDisplayLocation(location)
|
||||
prevLevel.current = getRouteLevel(location.pathname)
|
||||
return
|
||||
}
|
||||
|
||||
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 (
|
||||
<TransitionGroup component={null}>
|
||||
<CSSTransition
|
||||
key={location.pathname}
|
||||
classNames={getTransitionClasses()}
|
||||
timeout={{
|
||||
enter: 50,
|
||||
exit: 50,
|
||||
}}
|
||||
nodeRef={nodeRef}
|
||||
>
|
||||
<div
|
||||
ref={nodeRef}
|
||||
className='page-wrapper'
|
||||
style={{
|
||||
paddingBottom: `var(--safe-area-inset-bottom, 0px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
className='page-wrapper'
|
||||
style={{
|
||||
paddingBottom: `var(--safe-area-inset-bottom, 0px)`,
|
||||
}}
|
||||
>
|
||||
{displayLocation.pathname === location.pathname ? children : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user