fix: sidebar nav jumping around when loading page (#7574)
Fixes this: https://github.com/user-attachments/assets/1c637bca-0c13-43f6-bcd7-6ca58da9ae77
This commit is contained in:
@@ -9,7 +9,10 @@
|
||||
width: var(--nav-width);
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
opacity: 0;
|
||||
|
||||
&--nav-animate {
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
}
|
||||
|
||||
&--nav-open {
|
||||
opacity: 1;
|
||||
|
||||
@@ -10,10 +10,19 @@ export const NavWrapper: React.FC<{
|
||||
}> = (props) => {
|
||||
const { baseClass, children } = props
|
||||
|
||||
const { navOpen, navRef } = useNav()
|
||||
const { hydrated, navOpen, navRef, shouldAnimate } = useNav()
|
||||
|
||||
return (
|
||||
<aside className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
|
||||
<aside
|
||||
className={[
|
||||
baseClass,
|
||||
navOpen && `${baseClass}--nav-open`,
|
||||
shouldAnimate && `${baseClass}--nav-animate`,
|
||||
hydrated && `${baseClass}--nav-hydrated`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__scroll`} ref={navRef}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
width: var(--nav-width);
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
opacity: 0;
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
overflow: hidden;
|
||||
|
||||
&--nav-animate {
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
}
|
||||
|
||||
&--nav-open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -106,6 +106,39 @@ export const RootLayout = async ({
|
||||
})
|
||||
}
|
||||
|
||||
const navPreferences = user
|
||||
? (
|
||||
await payload.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
req,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: 'nav',
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)?.docs?.[0]
|
||||
: null
|
||||
|
||||
const isNavOpen = (navPreferences?.value as any)?.open ?? true
|
||||
|
||||
return (
|
||||
<html data-theme={theme} dir={dir} lang={languageCode}>
|
||||
<body>
|
||||
@@ -113,6 +146,7 @@ export const RootLayout = async ({
|
||||
config={clientConfig}
|
||||
dateFNSKey={i18n.dateFNSKey}
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
isNavOpen={isNavOpen}
|
||||
languageCode={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
permissions={permissions}
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
position: relative;
|
||||
grid-template-columns: 0 auto;
|
||||
transition: grid-template-columns var(--nav-trans-time) linear;
|
||||
isolation: isolate;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&--nav-animate {
|
||||
transition: grid-template-columns var(--nav-trans-time) linear;
|
||||
}
|
||||
|
||||
&--nav-open {
|
||||
width: 100%;
|
||||
grid-template-columns: var(--nav-width) auto;
|
||||
|
||||
.template-default {
|
||||
&__nav-overlay {
|
||||
@@ -23,3 +23,35 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1441px) {
|
||||
.template-default {
|
||||
grid-template-columns: 0 auto;
|
||||
|
||||
&--nav-open {
|
||||
grid-template-columns: var(--nav-width) auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.template-default--nav-hydrated.template-default--nav-open {
|
||||
grid-template-columns: var(--nav-width) auto;
|
||||
}
|
||||
|
||||
.template-default {
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--nav-hydrated {
|
||||
grid-template-columns: 0 auto;
|
||||
|
||||
.nav {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,17 @@ export const Wrapper: React.FC<{
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
const { baseClass, children, className } = props
|
||||
const { navOpen } = useNav()
|
||||
const { hydrated, navOpen, shouldAnimate } = useNav()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[baseClass, className, navOpen && `${baseClass}--nav-open`]
|
||||
className={[
|
||||
baseClass,
|
||||
className,
|
||||
navOpen && `${baseClass}--nav-open`,
|
||||
shouldAnimate && `${baseClass}--nav-animate`,
|
||||
hydrated && `${baseClass}--nav-hydrated`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
|
||||
@@ -6,15 +6,19 @@ import React, { useEffect, useRef } from 'react'
|
||||
import { usePreferences } from '../../providers/Preferences/index.js'
|
||||
|
||||
type NavContextType = {
|
||||
hydrated: boolean
|
||||
navOpen: boolean
|
||||
navRef: React.RefObject<HTMLDivElement | null>
|
||||
setNavOpen: (value: boolean) => void
|
||||
shouldAnimate: boolean
|
||||
}
|
||||
|
||||
export const NavContext = React.createContext<NavContextType>({
|
||||
hydrated: false,
|
||||
navOpen: true,
|
||||
navRef: null,
|
||||
setNavOpen: () => {},
|
||||
shouldAnimate: false,
|
||||
})
|
||||
|
||||
export const useNav = () => React.useContext(NavContext)
|
||||
@@ -31,7 +35,8 @@ const getNavPreference = async (getPreference): Promise<boolean> => {
|
||||
|
||||
export const NavProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
initialIsOpen?: boolean
|
||||
}> = ({ children, initialIsOpen }) => {
|
||||
const {
|
||||
breakpoints: { l: largeBreak, m: midBreak, s: smallBreak },
|
||||
} = useWindowInfo()
|
||||
@@ -43,7 +48,10 @@ export const NavProvider: React.FC<{
|
||||
// this is because getting the preference is async
|
||||
// so instead of closing it after the preference is loaded
|
||||
// we will open it after the preference is loaded
|
||||
const [navOpen, setNavOpen] = React.useState(false)
|
||||
const [navOpen, setNavOpen] = React.useState(initialIsOpen)
|
||||
|
||||
const [shouldAnimate, setShouldAnimate] = React.useState(false)
|
||||
const [hydrated, setHydrated] = React.useState(false)
|
||||
|
||||
// on load check the user's preference and set "initial" state
|
||||
useEffect(() => {
|
||||
@@ -53,7 +61,7 @@ export const NavProvider: React.FC<{
|
||||
setNavOpen(preferredState)
|
||||
}
|
||||
|
||||
setNavFromPreferences() // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
void setNavFromPreferences()
|
||||
}
|
||||
}, [largeBreak, getPreference, setNavOpen])
|
||||
|
||||
@@ -76,9 +84,14 @@ export const NavProvider: React.FC<{
|
||||
// close the nav when the user resizes down to mobile
|
||||
// the sidebar is a modal on mobile
|
||||
useEffect(() => {
|
||||
if (largeBreak === false || midBreak === false || smallBreak === false) {
|
||||
if (largeBreak === true || midBreak === true || smallBreak === true) {
|
||||
setNavOpen(false)
|
||||
}
|
||||
setHydrated(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setShouldAnimate(true)
|
||||
}, 100)
|
||||
}, [largeBreak, midBreak, smallBreak])
|
||||
|
||||
// when the component unmounts, clear all body scroll locks
|
||||
@@ -89,6 +102,8 @@ export const NavProvider: React.FC<{
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<NavContext.Provider value={{ navOpen, navRef, setNavOpen }}>{children}</NavContext.Provider>
|
||||
<NavContext.Provider value={{ hydrated, navOpen, navRef, setNavOpen, shouldAnimate }}>
|
||||
{children}
|
||||
</NavContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ type Props = {
|
||||
config: ClientConfig
|
||||
dateFNSKey: Language['dateFNSKey']
|
||||
fallbackLang: ClientConfig['i18n']['fallbackLanguage']
|
||||
isNavOpen?: boolean
|
||||
languageCode: string
|
||||
languageOptions: LanguageOptions
|
||||
permissions: Permissions
|
||||
@@ -48,6 +49,7 @@ export const RootProvider: React.FC<Props> = ({
|
||||
config,
|
||||
dateFNSKey,
|
||||
fallbackLang,
|
||||
isNavOpen,
|
||||
languageCode,
|
||||
languageOptions,
|
||||
permissions,
|
||||
@@ -90,7 +92,9 @@ export const RootProvider: React.FC<Props> = ({
|
||||
<LoadingOverlayProvider>
|
||||
<DocumentEventsProvider>
|
||||
<ActionsProvider>
|
||||
<NavProvider>{children}</NavProvider>
|
||||
<NavProvider initialIsOpen={isNavOpen}>
|
||||
{children}
|
||||
</NavProvider>
|
||||
</ActionsProvider>
|
||||
</DocumentEventsProvider>
|
||||
</LoadingOverlayProvider>
|
||||
|
||||
Reference in New Issue
Block a user