Files
payloadcms/packages/ui/src/elements/Nav/context.tsx
Jacob Fletcher 355bd12c61 chore: infer React context providers and prefer use (#11669)
As of [React 19](https://react.dev/blog/2024/12/05/react-19), context
providers no longer require the `<MyContext.Provider>` syntax and can be
rendered as `<MyContext>` directly. This will be deprecated in future
versions of React, which is now being caught by the
[`@eslint-react/no-context-provider`](https://eslint-react.xyz/docs/rules/no-context-provider)
ESLint rule.

Similarly, the [`use`](https://react.dev/reference/react/use) API is now
preferred over `useContext` because it is more flexible, for example
they can be called within loops and conditional statements. See the
[`@eslint-react/no-use-context`](https://eslint-react.xyz/docs/rules/no-use-context)
ESLint rule for more details.
2025-03-12 15:48:20 -04:00

119 lines
3.2 KiB
TypeScript

'use client'
import { useWindowInfo } from '@faceless-ui/window-info'
import { usePathname } from 'next/navigation.js'
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.use(NavContext)
const getNavPreference = async (getPreference): Promise<boolean> => {
const navPrefs = await getPreference('nav')
const preferredState = navPrefs?.open
if (typeof preferredState === 'boolean') {
return preferredState
} else {
return true
}
}
export const NavProvider: React.FC<{
children: React.ReactNode
initialIsOpen?: boolean
}> = ({ children, initialIsOpen }) => {
const {
breakpoints: { l: largeBreak, m: midBreak, s: smallBreak },
} = useWindowInfo()
const pathname = usePathname()
const { getPreference } = usePreferences()
const navRef = useRef(null)
// initialize the nav to be closed
// 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(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(() => {
if (largeBreak === false) {
const setNavFromPreferences = async () => {
const preferredState = await getNavPreference(getPreference)
setNavOpen(preferredState)
}
void setNavFromPreferences()
}
}, [largeBreak, getPreference, setNavOpen])
// on smaller screens where the nav is a modal
// close the nav when the user navigates away
useEffect(() => {
if (smallBreak === true) {
setNavOpen(false)
}
}, [pathname])
// on open and close, lock the body scroll
// do not do this on desktop, the sidebar is not a modal
useEffect(() => {
if (navRef.current) {
if (navOpen && midBreak) {
navRef.current.style.overscrollBehavior = 'contain'
} else {
navRef.current.style.overscrollBehavior = 'auto'
}
}
}, [navOpen, midBreak])
// on smaller screens where the nav is a modal
// close the nav when the user resizes down to mobile
// the sidebar is a modal on mobile
useEffect(() => {
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
useEffect(() => {
return () => {
if (navRef.current) {
navRef.current.style.overscrollBehavior = 'auto'
}
}
}, [])
return (
<NavContext value={{ hydrated, navOpen, navRef, setNavOpen, shouldAnimate }}>
{children}
</NavContext>
)
}