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