Skip to content

Commit 39d7b71

Browse files
authored
fix: sidebar nav jumping around when loading page (#7574)
Fixes this: https://github.com/user-attachments/assets/1c637bca-0c13-43f6-bcd7-6ca58da9ae77
1 parent 9d1997e commit 39d7b71

File tree

8 files changed

+122
-16
lines changed

8 files changed

+122
-16
lines changed

packages/next/src/elements/Nav/NavWrapper/index.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
width: var(--nav-width);
1010
border-right: 1px solid var(--theme-elevation-100);
1111
opacity: 0;
12-
transition: opacity var(--nav-trans-time) ease-in-out;
12+
13+
&--nav-animate {
14+
transition: opacity var(--nav-trans-time) ease-in-out;
15+
}
1316

1417
&--nav-open {
1518
opacity: 1;

packages/next/src/elements/Nav/NavWrapper/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ export const NavWrapper: React.FC<{
1010
}> = (props) => {
1111
const { baseClass, children } = props
1212

13-
const { navOpen, navRef } = useNav()
13+
const { hydrated, navOpen, navRef, shouldAnimate } = useNav()
1414

1515
return (
16-
<aside className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
16+
<aside
17+
className={[
18+
baseClass,
19+
navOpen && `${baseClass}--nav-open`,
20+
shouldAnimate && `${baseClass}--nav-animate`,
21+
hydrated && `${baseClass}--nav-hydrated`,
22+
]
23+
.filter(Boolean)
24+
.join(' ')}
25+
>
1726
<div className={`${baseClass}__scroll`} ref={navRef}>
1827
{children}
1928
</div>

packages/next/src/elements/Nav/index.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
width: var(--nav-width);
1010
border-right: 1px solid var(--theme-elevation-100);
1111
opacity: 0;
12-
transition: opacity var(--nav-trans-time) ease-in-out;
1312
overflow: hidden;
1413

14+
&--nav-animate {
15+
transition: opacity var(--nav-trans-time) ease-in-out;
16+
}
17+
1518
&--nav-open {
1619
opacity: 1;
1720
}

packages/next/src/layouts/Root/index.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,47 @@ export const RootLayout = async ({
106106
})
107107
}
108108

109+
const navPreferences = user
110+
? (
111+
await payload.find({
112+
collection: 'payload-preferences',
113+
depth: 0,
114+
limit: 1,
115+
req,
116+
user,
117+
where: {
118+
and: [
119+
{
120+
key: {
121+
equals: 'nav',
122+
},
123+
},
124+
{
125+
'user.relationTo': {
126+
equals: user.collection,
127+
},
128+
},
129+
{
130+
'user.value': {
131+
equals: user.id,
132+
},
133+
},
134+
],
135+
},
136+
})
137+
)?.docs?.[0]
138+
: null
139+
140+
const isNavOpen = (navPreferences?.value as any)?.open ?? true
141+
109142
return (
110143
<html data-theme={theme} dir={dir} lang={languageCode}>
111144
<body>
112145
<RootProvider
113146
config={clientConfig}
114147
dateFNSKey={i18n.dateFNSKey}
115148
fallbackLang={clientConfig.i18n.fallbackLanguage}
149+
isNavOpen={isNavOpen}
116150
languageCode={languageCode}
117151
languageOptions={languageOptions}
118152
permissions={permissions}

packages/next/src/templates/Default/Wrapper/index.scss

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
min-height: 100vh;
55
display: grid;
66
position: relative;
7-
grid-template-columns: 0 auto;
8-
transition: grid-template-columns var(--nav-trans-time) linear;
97
isolation: isolate;
108

119
@media (prefers-reduced-motion) {
1210
transition: none;
1311
}
1412

13+
&--nav-animate {
14+
transition: grid-template-columns var(--nav-trans-time) linear;
15+
}
16+
1517
&--nav-open {
16-
width: 100%;
17-
grid-template-columns: var(--nav-width) auto;
1818

1919
.template-default {
2020
&__nav-overlay {
@@ -23,3 +23,35 @@
2323
}
2424
}
2525
}
26+
27+
@media (min-width: 1441px) {
28+
.template-default {
29+
grid-template-columns: 0 auto;
30+
31+
&--nav-open {
32+
grid-template-columns: var(--nav-width) auto;
33+
}
34+
}
35+
}
36+
37+
@media (max-width: 1440px) {
38+
.template-default--nav-hydrated.template-default--nav-open {
39+
grid-template-columns: var(--nav-width) auto;
40+
}
41+
42+
.template-default {
43+
grid-template-columns: 1fr auto;
44+
45+
.nav {
46+
display: none;
47+
}
48+
49+
&--nav-hydrated {
50+
grid-template-columns: 0 auto;
51+
52+
.nav {
53+
display: unset;
54+
}
55+
}
56+
}
57+
}

packages/next/src/templates/Default/Wrapper/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ export const Wrapper: React.FC<{
1010
className?: string
1111
}> = (props) => {
1212
const { baseClass, children, className } = props
13-
const { navOpen } = useNav()
13+
const { hydrated, navOpen, shouldAnimate } = useNav()
1414

1515
return (
1616
<div
17-
className={[baseClass, className, navOpen && `${baseClass}--nav-open`]
17+
className={[
18+
baseClass,
19+
className,
20+
navOpen && `${baseClass}--nav-open`,
21+
shouldAnimate && `${baseClass}--nav-animate`,
22+
hydrated && `${baseClass}--nav-hydrated`,
23+
]
1824
.filter(Boolean)
1925
.join(' ')}
2026
>

packages/ui/src/elements/Nav/context.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import React, { useEffect, useRef } from 'react'
66
import { usePreferences } from '../../providers/Preferences/index.js'
77

88
type NavContextType = {
9+
hydrated: boolean
910
navOpen: boolean
1011
navRef: React.RefObject<HTMLDivElement | null>
1112
setNavOpen: (value: boolean) => void
13+
shouldAnimate: boolean
1214
}
1315

1416
export const NavContext = React.createContext<NavContextType>({
17+
hydrated: false,
1518
navOpen: true,
1619
navRef: null,
1720
setNavOpen: () => {},
21+
shouldAnimate: false,
1822
})
1923

2024
export const useNav = () => React.useContext(NavContext)
@@ -31,7 +35,8 @@ const getNavPreference = async (getPreference): Promise<boolean> => {
3135

3236
export const NavProvider: React.FC<{
3337
children: React.ReactNode
34-
}> = ({ children }) => {
38+
initialIsOpen?: boolean
39+
}> = ({ children, initialIsOpen }) => {
3540
const {
3641
breakpoints: { l: largeBreak, m: midBreak, s: smallBreak },
3742
} = useWindowInfo()
@@ -43,7 +48,10 @@ export const NavProvider: React.FC<{
4348
// this is because getting the preference is async
4449
// so instead of closing it after the preference is loaded
4550
// we will open it after the preference is loaded
46-
const [navOpen, setNavOpen] = React.useState(false)
51+
const [navOpen, setNavOpen] = React.useState(initialIsOpen)
52+
53+
const [shouldAnimate, setShouldAnimate] = React.useState(false)
54+
const [hydrated, setHydrated] = React.useState(false)
4755

4856
// on load check the user's preference and set "initial" state
4957
useEffect(() => {
@@ -53,7 +61,7 @@ export const NavProvider: React.FC<{
5361
setNavOpen(preferredState)
5462
}
5563

56-
setNavFromPreferences() // eslint-disable-line @typescript-eslint/no-floating-promises
64+
void setNavFromPreferences()
5765
}
5866
}, [largeBreak, getPreference, setNavOpen])
5967

@@ -76,9 +84,14 @@ export const NavProvider: React.FC<{
7684
// close the nav when the user resizes down to mobile
7785
// the sidebar is a modal on mobile
7886
useEffect(() => {
79-
if (largeBreak === false || midBreak === false || smallBreak === false) {
87+
if (largeBreak === true || midBreak === true || smallBreak === true) {
8088
setNavOpen(false)
8189
}
90+
setHydrated(true)
91+
92+
setTimeout(() => {
93+
setShouldAnimate(true)
94+
}, 100)
8295
}, [largeBreak, midBreak, smallBreak])
8396

8497
// when the component unmounts, clear all body scroll locks
@@ -89,6 +102,8 @@ export const NavProvider: React.FC<{
89102
}, [])
90103

91104
return (
92-
<NavContext.Provider value={{ navOpen, navRef, setNavOpen }}>{children}</NavContext.Provider>
105+
<NavContext.Provider value={{ hydrated, navOpen, navRef, setNavOpen, shouldAnimate }}>
106+
{children}
107+
</NavContext.Provider>
93108
)
94109
}

packages/ui/src/providers/Root/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Props = {
3434
config: ClientConfig
3535
dateFNSKey: Language['dateFNSKey']
3636
fallbackLang: ClientConfig['i18n']['fallbackLanguage']
37+
isNavOpen?: boolean
3738
languageCode: string
3839
languageOptions: LanguageOptions
3940
permissions: Permissions
@@ -48,6 +49,7 @@ export const RootProvider: React.FC<Props> = ({
4849
config,
4950
dateFNSKey,
5051
fallbackLang,
52+
isNavOpen,
5153
languageCode,
5254
languageOptions,
5355
permissions,
@@ -90,7 +92,9 @@ export const RootProvider: React.FC<Props> = ({
9092
<LoadingOverlayProvider>
9193
<DocumentEventsProvider>
9294
<ActionsProvider>
93-
<NavProvider>{children}</NavProvider>
95+
<NavProvider initialIsOpen={isNavOpen}>
96+
{children}
97+
</NavProvider>
9498
</ActionsProvider>
9599
</DocumentEventsProvider>
96100
</LoadingOverlayProvider>

0 commit comments

Comments
 (0)