This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { isDesktopViewport, useSidePanelMachine } from '../panels/useSidePanelMachine';
|
||||
|
||||
@@ -19,207 +19,207 @@ const SIDEBAR_MAX_WIDTH = 420;
|
||||
const SIDEBAR_COLLAPSED_WIDTH = 56;
|
||||
|
||||
export type LeftMenuRenderState = {
|
||||
collapsed: boolean;
|
||||
mobileOpen: boolean;
|
||||
isDesktop: boolean;
|
||||
closeMenu: () => void;
|
||||
collapsed: boolean;
|
||||
mobileOpen: boolean;
|
||||
isDesktop: boolean;
|
||||
closeMenu: () => void;
|
||||
};
|
||||
|
||||
export type LeftMenuContent = {
|
||||
ariaLabel?: string;
|
||||
render: (state: LeftMenuRenderState) => ReactNode;
|
||||
ariaLabel?: string;
|
||||
render: (state: LeftMenuRenderState) => ReactNode;
|
||||
};
|
||||
|
||||
export type LeftMenuStyle = CSSProperties & {
|
||||
'--auth-sidebar-width': string;
|
||||
'--auth-sidebar-width': string;
|
||||
};
|
||||
|
||||
type LeftMenuContextValue = {
|
||||
collapsed: boolean;
|
||||
mobileOpen: boolean;
|
||||
content: LeftMenuContent;
|
||||
desktopMenuStyle: LeftMenuStyle;
|
||||
openMenu: (content?: LeftMenuContent) => void;
|
||||
closeMenu: () => void;
|
||||
toggleMenu: (content?: LeftMenuContent) => void;
|
||||
expandMenu: () => void;
|
||||
collapseMenu: () => void;
|
||||
toggleCollapsed: () => void;
|
||||
setMenuContent: (content: LeftMenuContent | null) => void;
|
||||
startResize: ReturnType<typeof useSidePanelMachine>['startResize'];
|
||||
collapsed: boolean;
|
||||
mobileOpen: boolean;
|
||||
content: LeftMenuContent;
|
||||
desktopMenuStyle: LeftMenuStyle;
|
||||
openMenu: (content?: LeftMenuContent) => void;
|
||||
closeMenu: () => void;
|
||||
toggleMenu: (content?: LeftMenuContent) => void;
|
||||
expandMenu: () => void;
|
||||
collapseMenu: () => void;
|
||||
toggleCollapsed: () => void;
|
||||
setMenuContent: (content: LeftMenuContent | null) => void;
|
||||
startResize: ReturnType<typeof useSidePanelMachine>['startResize'];
|
||||
};
|
||||
|
||||
type LeftMenuProviderProps = {
|
||||
children: ReactNode;
|
||||
defaultContent: LeftMenuContent;
|
||||
closeOnPathname?: string;
|
||||
children: ReactNode;
|
||||
defaultContent: LeftMenuContent;
|
||||
closeOnPathname?: string;
|
||||
};
|
||||
|
||||
const LeftMenuContext = createContext<LeftMenuContextValue | undefined>(undefined);
|
||||
|
||||
function readStoredCollapsed(): boolean {
|
||||
if (!globalThis.window) {
|
||||
return false;
|
||||
}
|
||||
if (!globalThis.window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1';
|
||||
return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1';
|
||||
}
|
||||
|
||||
export function LeftMenuProvider({
|
||||
children,
|
||||
defaultContent,
|
||||
closeOnPathname,
|
||||
}: Readonly<LeftMenuProviderProps>) {
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => readStoredCollapsed());
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [content, setContent] = useState<LeftMenuContent>(defaultContent);
|
||||
const defaultContentRef = useRef(defaultContent);
|
||||
|
||||
useEffect(() => {
|
||||
const previousDefaultContent = defaultContentRef.current;
|
||||
defaultContentRef.current = defaultContent;
|
||||
setContent((currentContent) => {
|
||||
if (currentContent === previousDefaultContent) {
|
||||
return defaultContent;
|
||||
}
|
||||
return currentContent;
|
||||
});
|
||||
}, [defaultContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!globalThis.window) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed ? '1' : '0');
|
||||
}, [collapsed]);
|
||||
|
||||
const expandMenu = useCallback(() => {
|
||||
setCollapsed(false);
|
||||
}, []);
|
||||
|
||||
const collapseMenu = useCallback(() => {
|
||||
setCollapsed(true);
|
||||
}, []);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed((previous) => !previous);
|
||||
}, []);
|
||||
|
||||
const closeMobile = useCallback(() => {
|
||||
setMobileOpen(false);
|
||||
}, []);
|
||||
|
||||
const setMenuContent = useCallback((nextContent: LeftMenuContent | null) => {
|
||||
setContent(nextContent ?? defaultContentRef.current);
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
if (isDesktopViewport()) {
|
||||
collapseMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
closeMobile();
|
||||
}, [collapseMenu, closeMobile]);
|
||||
|
||||
const openMenu = useCallback(
|
||||
(nextContent?: LeftMenuContent) => {
|
||||
if (nextContent) {
|
||||
setContent(nextContent);
|
||||
}
|
||||
|
||||
if (isDesktopViewport()) {
|
||||
expandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileOpen(true);
|
||||
},
|
||||
[expandMenu],
|
||||
);
|
||||
|
||||
const toggleMenu = useCallback(
|
||||
(nextContent?: LeftMenuContent) => {
|
||||
if (nextContent) {
|
||||
setContent(nextContent);
|
||||
}
|
||||
|
||||
if (isDesktopViewport()) {
|
||||
toggleCollapsed();
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileOpen((previous) => !previous);
|
||||
},
|
||||
[toggleCollapsed],
|
||||
);
|
||||
|
||||
const handleCloseOnPathname = useCallback(() => {
|
||||
setMobileOpen(false);
|
||||
setContent(defaultContentRef.current);
|
||||
}, []);
|
||||
|
||||
const { width, startResize } = useSidePanelMachine({
|
||||
storageKey: SIDEBAR_WIDTH_KEY,
|
||||
defaultWidth: SIDEBAR_DEFAULT_WIDTH,
|
||||
minWidth: SIDEBAR_MIN_WIDTH,
|
||||
maxWidth: SIDEBAR_MAX_WIDTH,
|
||||
resizeAxis: 'from-left',
|
||||
resizingBodyClass: 'auth-sidebar-resizing',
|
||||
isOpen: mobileOpen,
|
||||
canResize: !collapsed,
|
||||
shouldPersistWidth: !collapsed,
|
||||
children,
|
||||
defaultContent,
|
||||
closeOnPathname,
|
||||
onCloseOnPathname: handleCloseOnPathname,
|
||||
onEscape: closeMobile,
|
||||
});
|
||||
}: Readonly<LeftMenuProviderProps>) {
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => readStoredCollapsed());
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [content, setContent] = useState<LeftMenuContent>(defaultContent);
|
||||
const defaultContentRef = useRef(defaultContent);
|
||||
|
||||
const desktopMenuStyle = useMemo<LeftMenuStyle>(
|
||||
() => ({
|
||||
'--auth-sidebar-width': `${collapsed ? SIDEBAR_COLLAPSED_WIDTH : width}px`,
|
||||
}),
|
||||
[collapsed, width],
|
||||
);
|
||||
useEffect(() => {
|
||||
const previousDefaultContent = defaultContentRef.current;
|
||||
defaultContentRef.current = defaultContent;
|
||||
setContent((currentContent) => {
|
||||
if (currentContent === previousDefaultContent) {
|
||||
return defaultContent;
|
||||
}
|
||||
return currentContent;
|
||||
});
|
||||
}, [defaultContent]);
|
||||
|
||||
const value = useMemo<LeftMenuContextValue>(
|
||||
() => ({
|
||||
collapsed,
|
||||
mobileOpen,
|
||||
content,
|
||||
desktopMenuStyle,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
toggleMenu,
|
||||
expandMenu,
|
||||
collapseMenu,
|
||||
toggleCollapsed,
|
||||
setMenuContent,
|
||||
startResize,
|
||||
}),
|
||||
[
|
||||
collapsed,
|
||||
mobileOpen,
|
||||
content,
|
||||
desktopMenuStyle,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
toggleMenu,
|
||||
expandMenu,
|
||||
collapseMenu,
|
||||
toggleCollapsed,
|
||||
setMenuContent,
|
||||
startResize,
|
||||
],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!globalThis.window) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <LeftMenuContext.Provider value={value}>{children}</LeftMenuContext.Provider>;
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed ? '1' : '0');
|
||||
}, [collapsed]);
|
||||
|
||||
const expandMenu = useCallback(() => {
|
||||
setCollapsed(false);
|
||||
}, []);
|
||||
|
||||
const collapseMenu = useCallback(() => {
|
||||
setCollapsed(true);
|
||||
}, []);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed((previous) => !previous);
|
||||
}, []);
|
||||
|
||||
const closeMobile = useCallback(() => {
|
||||
setMobileOpen(false);
|
||||
}, []);
|
||||
|
||||
const setMenuContent = useCallback((nextContent: LeftMenuContent | null) => {
|
||||
setContent(nextContent ?? defaultContentRef.current);
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
if (isDesktopViewport()) {
|
||||
collapseMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
closeMobile();
|
||||
}, [collapseMenu, closeMobile]);
|
||||
|
||||
const openMenu = useCallback(
|
||||
(nextContent?: LeftMenuContent) => {
|
||||
if (nextContent) {
|
||||
setContent(nextContent);
|
||||
}
|
||||
|
||||
if (isDesktopViewport()) {
|
||||
expandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileOpen(true);
|
||||
},
|
||||
[expandMenu],
|
||||
);
|
||||
|
||||
const toggleMenu = useCallback(
|
||||
(nextContent?: LeftMenuContent) => {
|
||||
if (nextContent) {
|
||||
setContent(nextContent);
|
||||
}
|
||||
|
||||
if (isDesktopViewport()) {
|
||||
toggleCollapsed();
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileOpen((previous) => !previous);
|
||||
},
|
||||
[toggleCollapsed],
|
||||
);
|
||||
|
||||
const handleCloseOnPathname = useCallback(() => {
|
||||
setMobileOpen(false);
|
||||
setContent(defaultContentRef.current);
|
||||
}, []);
|
||||
|
||||
const { width, startResize } = useSidePanelMachine({
|
||||
storageKey: SIDEBAR_WIDTH_KEY,
|
||||
defaultWidth: SIDEBAR_DEFAULT_WIDTH,
|
||||
minWidth: SIDEBAR_MIN_WIDTH,
|
||||
maxWidth: SIDEBAR_MAX_WIDTH,
|
||||
resizeAxis: 'from-left',
|
||||
resizingBodyClass: 'auth-sidebar-resizing',
|
||||
isOpen: mobileOpen,
|
||||
canResize: !collapsed,
|
||||
shouldPersistWidth: !collapsed,
|
||||
closeOnPathname,
|
||||
onCloseOnPathname: handleCloseOnPathname,
|
||||
onEscape: closeMobile,
|
||||
});
|
||||
|
||||
const desktopMenuStyle = useMemo<LeftMenuStyle>(
|
||||
() => ({
|
||||
'--auth-sidebar-width': `${collapsed ? SIDEBAR_COLLAPSED_WIDTH : width}px`,
|
||||
}),
|
||||
[collapsed, width],
|
||||
);
|
||||
|
||||
const value = useMemo<LeftMenuContextValue>(
|
||||
() => ({
|
||||
collapsed,
|
||||
mobileOpen,
|
||||
content,
|
||||
desktopMenuStyle,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
toggleMenu,
|
||||
expandMenu,
|
||||
collapseMenu,
|
||||
toggleCollapsed,
|
||||
setMenuContent,
|
||||
startResize,
|
||||
}),
|
||||
[
|
||||
collapsed,
|
||||
mobileOpen,
|
||||
content,
|
||||
desktopMenuStyle,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
toggleMenu,
|
||||
expandMenu,
|
||||
collapseMenu,
|
||||
toggleCollapsed,
|
||||
setMenuContent,
|
||||
startResize,
|
||||
],
|
||||
);
|
||||
|
||||
return <LeftMenuContext.Provider value={value}>{children}</LeftMenuContext.Provider>;
|
||||
}
|
||||
|
||||
export function useLeftMenu() {
|
||||
const ctx = useContext(LeftMenuContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useLeftMenu must be used within LeftMenuProvider');
|
||||
}
|
||||
return ctx;
|
||||
const ctx = useContext(LeftMenuContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useLeftMenu must be used within LeftMenuProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user