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:
Alessio Gravili
2024-08-14 12:23:57 -04:00
committed by GitHub
parent 9d1997e6a0
commit 39d7b717a9
8 changed files with 122 additions and 16 deletions

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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}

View File

@@ -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;
}
}
}
}

View File

@@ -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(' ')}
> >

View File

@@ -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>
) )
} }

View File

@@ -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>