revamp template
This commit is contained in:
93
src/components/menu/Menu.tsx
Normal file
93
src/components/menu/Menu.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
createContext,
|
||||
isValidElement,
|
||||
memo,
|
||||
useContext,
|
||||
useState
|
||||
} from 'react';
|
||||
import { IMenuContextProps, IMenuItemProps, IMenuProps } from './';
|
||||
import { MenuItem } from './';
|
||||
|
||||
const initalProps: IMenuContextProps = {
|
||||
disabled: false,
|
||||
highlight: false,
|
||||
multipleExpand: false,
|
||||
dropdownTimeout: 0,
|
||||
// Default function for opening an accordion (to be overridden)
|
||||
setOpenAccordion: (parentId: string, id: string) => {
|
||||
console.log(`Accordion at level ${parentId}, with ID ${id} is now open`);
|
||||
},
|
||||
// Default function for checking if an accordion is open (to be overridden)
|
||||
isOpenAccordion: (parentId: string, id: string) => {
|
||||
console.log(`Checking if accordion at level ${parentId}, with ID ${id} is open`);
|
||||
return false; // By default, no accordion is open
|
||||
}
|
||||
};
|
||||
|
||||
// Create a Menu Context
|
||||
const MenuContext = createContext(initalProps);
|
||||
|
||||
// Custom hook to use the Menu Context
|
||||
const useMenu = () => useContext(MenuContext);
|
||||
|
||||
const MenuComponent = ({
|
||||
className,
|
||||
children,
|
||||
disabled = false,
|
||||
highlight = false,
|
||||
dropdownTimeout = 150,
|
||||
multipleExpand = false
|
||||
}: IMenuProps) => {
|
||||
const [openAccordions, setOpenAccordions] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
// Function to handle the accordion toggle
|
||||
const setOpenAccordion = (parentId: string, id: string) => {
|
||||
setOpenAccordions((prevState) => ({
|
||||
...prevState,
|
||||
[parentId]: prevState[parentId] === id ? null : id // Toggle the current item and collapse others at the same level
|
||||
}));
|
||||
};
|
||||
|
||||
const isOpenAccordion = (parentId: string, id: string) => {
|
||||
return openAccordions[parentId] === id;
|
||||
};
|
||||
|
||||
const modifiedChildren = Children.map(children, (child, index) => {
|
||||
if (isValidElement(child)) {
|
||||
if (child.type === MenuItem) {
|
||||
const modifiedProps: IMenuItemProps = {
|
||||
parentId: 'root',
|
||||
id: `root-${index}`
|
||||
};
|
||||
|
||||
return cloneElement(child, modifiedProps);
|
||||
} else {
|
||||
return cloneElement(child);
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<MenuContext.Provider
|
||||
value={{
|
||||
disabled,
|
||||
highlight,
|
||||
dropdownTimeout,
|
||||
multipleExpand,
|
||||
setOpenAccordion,
|
||||
isOpenAccordion
|
||||
}}
|
||||
>
|
||||
<div className={clsx('menu', className && className)}>{modifiedChildren}</div>
|
||||
</MenuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu = memo(MenuComponent);
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Menu, useMenu };
|
||||
9
src/components/menu/MenuArrow.tsx
Normal file
9
src/components/menu/MenuArrow.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuToggleProps } from './';
|
||||
|
||||
const MenuArrow = ({ className, children }: IMenuToggleProps) => {
|
||||
return <div className={clsx('menu-arrow', className && className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export { MenuArrow };
|
||||
9
src/components/menu/MenuBadge.tsx
Normal file
9
src/components/menu/MenuBadge.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuBadgeProps } from './';
|
||||
|
||||
const MenuBadge = ({ className, children }: IMenuBadgeProps) => {
|
||||
return <div className={clsx('menu-badge', className && className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export { MenuBadge };
|
||||
9
src/components/menu/MenuBullet.tsx
Normal file
9
src/components/menu/MenuBullet.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuBulletProps } from './';
|
||||
|
||||
const MenuBullet = ({ className, children }: IMenuBulletProps) => {
|
||||
return <div className={clsx('menu-bullet', className && className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export { MenuBullet };
|
||||
9
src/components/menu/MenuHeading.tsx
Normal file
9
src/components/menu/MenuHeading.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuHeadingProps } from './';
|
||||
|
||||
const MenuHeading = ({ className, children }: IMenuHeadingProps) => {
|
||||
return <div className={clsx('menu-heading', className && className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export { MenuHeading };
|
||||
9
src/components/menu/MenuIcon.tsx
Normal file
9
src/components/menu/MenuIcon.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuIconProps } from './';
|
||||
|
||||
const MenuIcon = ({ className, children }: IMenuIconProps) => {
|
||||
return <div className={clsx('menu-icon', className && className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export { MenuIcon };
|
||||
425
src/components/menu/MenuItem.tsx
Normal file
425
src/components/menu/MenuItem.tsx
Normal file
@ -0,0 +1,425 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ClickAwayListener, Popper } from '@mui/base';
|
||||
import clsx from 'clsx';
|
||||
import React, {
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
memo,
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import useResponsiveProp from '@/hooks/useResponsiveProp';
|
||||
import { useMatchPath } from '../../hooks/useMatchPath';
|
||||
import {
|
||||
IMenuItemRef,
|
||||
IMenuItemProps,
|
||||
IMenuLabelProps,
|
||||
IMenuLinkProps,
|
||||
IMenuSubProps,
|
||||
MenuHeading,
|
||||
MenuLabel,
|
||||
MenuLink,
|
||||
MenuSub,
|
||||
TMenuToggle,
|
||||
TMenuTrigger,
|
||||
IMenuToggleProps,
|
||||
MenuToggle,
|
||||
useMenu
|
||||
} from './';
|
||||
import { usePathname } from '@/providers';
|
||||
import { getMenuLinkPath, hasMenuActiveChild } from './utils';
|
||||
|
||||
const MenuItemComponent = forwardRef<IMenuItemRef | null, IMenuItemProps>(
|
||||
function MenuItem(props, ref) {
|
||||
const {
|
||||
toggle,
|
||||
trigger,
|
||||
dropdownProps,
|
||||
dropdownZIndex = 1300,
|
||||
disabled,
|
||||
tabIndex,
|
||||
className,
|
||||
handleParentHide,
|
||||
onShow,
|
||||
onHide,
|
||||
onClick,
|
||||
containerProps: ContainerPropsProp = {},
|
||||
children,
|
||||
open = false,
|
||||
parentId,
|
||||
id
|
||||
} = props;
|
||||
|
||||
const { ...containerProps } = ContainerPropsProp;
|
||||
|
||||
const menuItemRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const path = props.path || getMenuLinkPath(children);
|
||||
|
||||
const {
|
||||
disabled: isMenuDisabled,
|
||||
highlight,
|
||||
multipleExpand,
|
||||
setOpenAccordion,
|
||||
isOpenAccordion,
|
||||
dropdownTimeout
|
||||
} = useMenu();
|
||||
const finalParentId = parentId !== undefined ? parentId : '';
|
||||
const finalId = id !== undefined ? id : '';
|
||||
|
||||
const menuContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const { pathname, prevPathname } = usePathname();
|
||||
|
||||
const { match } = useMatchPath(path);
|
||||
|
||||
const propToggle: TMenuToggle = useResponsiveProp(toggle, 'accordion');
|
||||
|
||||
const propTrigger: TMenuTrigger = useResponsiveProp(trigger, 'click');
|
||||
|
||||
const propDropdownProps = useResponsiveProp(dropdownProps);
|
||||
|
||||
const active: boolean = highlight ? path.length > 0 && match : false;
|
||||
|
||||
const [here, setHere] = useState(open);
|
||||
|
||||
const accordionShow = isOpenAccordion(finalParentId, finalId);
|
||||
|
||||
const [show, setShow] = useState(open);
|
||||
|
||||
const [transitioning, setTransitioning] = useState(open);
|
||||
|
||||
const [accordionEnter, setAccordionEnter] = useState(open);
|
||||
|
||||
const hasSub = Children.toArray(children).some(
|
||||
(child) => isValidElement(child) && child.type === MenuSub
|
||||
);
|
||||
|
||||
const handleHide = () => {
|
||||
if (hasSub) {
|
||||
setShow(false);
|
||||
}
|
||||
|
||||
if (hasSub && propToggle === 'accordion' && multipleExpand === false) {
|
||||
setOpenAccordion(finalParentId, '');
|
||||
}
|
||||
|
||||
if (handleParentHide) {
|
||||
handleParentHide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleShow = () => {
|
||||
if (hasSub) {
|
||||
setShow(true);
|
||||
}
|
||||
|
||||
if (hasSub && propToggle === 'accordion' && multipleExpand === false) {
|
||||
setOpenAccordion(finalParentId, finalId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent<HTMLElement>) => {
|
||||
if (isMenuDisabled) return;
|
||||
|
||||
// Cancel any previously set hide timeout
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (propTrigger === 'hover') {
|
||||
setShow(true);
|
||||
|
||||
if (containerProps.onMouseEnter) {
|
||||
containerProps.onMouseEnter(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: MouseEvent<HTMLElement>) => {
|
||||
if (isMenuDisabled) return;
|
||||
|
||||
if (propTrigger === 'hover') {
|
||||
// Set a timeout to hide the dropdown after `dropdownTimeout` delay
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setShow(false);
|
||||
|
||||
if (containerProps.onMouseLeave) {
|
||||
containerProps.onMouseLeave(e);
|
||||
}
|
||||
|
||||
hideTimeoutRef.current = null; // Reset the timeout reference
|
||||
}, dropdownTimeout);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = (e: MouseEvent<HTMLElement>) => {
|
||||
if (isMenuDisabled) return;
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
if (show) {
|
||||
if (propToggle === 'accordion') {
|
||||
setAccordionEnter(true);
|
||||
}
|
||||
|
||||
handleHide();
|
||||
} else {
|
||||
if (propToggle === 'accordion') {
|
||||
setAccordionEnter(true);
|
||||
}
|
||||
|
||||
handleShow();
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
onClick(e, props);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: MouseEvent<HTMLElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleHide();
|
||||
|
||||
if (onClick) {
|
||||
onClick(e, props);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLink = (child: ReactElement) => {
|
||||
// Add some props to each child
|
||||
const modifiedProps: IMenuLinkProps = {
|
||||
hasItemSub: hasSub,
|
||||
tabIndex,
|
||||
handleToggle,
|
||||
handleClick
|
||||
};
|
||||
|
||||
// Return the child with modified props
|
||||
return cloneElement(child, modifiedProps);
|
||||
};
|
||||
|
||||
const renderToggle = (child: ReactElement) => {
|
||||
// Add some props to each child
|
||||
const modifiedProps: IMenuToggleProps = {
|
||||
hasItemSub: hasSub,
|
||||
tabIndex,
|
||||
handleToggle
|
||||
};
|
||||
|
||||
// Return the child with modified props
|
||||
return cloneElement(child, modifiedProps);
|
||||
};
|
||||
|
||||
const renderLabel = (child: ReactElement) => {
|
||||
// Add some props to each child
|
||||
const modifiedProps: IMenuLabelProps = {
|
||||
hasItemSub: hasSub,
|
||||
tabIndex,
|
||||
handleToggle,
|
||||
handleClick
|
||||
};
|
||||
|
||||
// Return the child with modified props
|
||||
return cloneElement(child, modifiedProps);
|
||||
};
|
||||
|
||||
const renderHeading = (child: ReactElement) => {
|
||||
return cloneElement(child);
|
||||
};
|
||||
|
||||
const renderSubDropdown = (child: ReactElement) => {
|
||||
// Add some props to each child
|
||||
const modifiedProps: IMenuSubProps = {
|
||||
parentId: `${parentId}-${finalId}`,
|
||||
toggle: propToggle,
|
||||
handleParentHide: handleHide,
|
||||
tabIndex,
|
||||
menuItemRef: ref
|
||||
};
|
||||
|
||||
const modofiedChild = cloneElement(child, modifiedProps);
|
||||
|
||||
return (
|
||||
<Popper
|
||||
style={{
|
||||
zIndex: dropdownZIndex,
|
||||
pointerEvents: trigger === 'click' ? 'auto' : 'none'
|
||||
}}
|
||||
{...propDropdownProps}
|
||||
anchorEl={show ? menuItemRef.current : null}
|
||||
open={show}
|
||||
autoFocus={false}
|
||||
className={clsx(child.props.rootClassName && child.props.rootClassName)}
|
||||
>
|
||||
<ClickAwayListener onClickAway={handleHide}>
|
||||
<div
|
||||
className={clsx(
|
||||
'menu-container',
|
||||
child.props.baseClassName && child.props.baseClassName
|
||||
)}
|
||||
ref={menuContainerRef}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
{modofiedChild}
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSubAccordion = (child: ReactElement) => {
|
||||
const handleEntered = () => {
|
||||
setTransitioning(true);
|
||||
};
|
||||
|
||||
const handleExited = () => {
|
||||
setTransitioning(false);
|
||||
setAccordionEnter(true);
|
||||
};
|
||||
|
||||
// Add some props to each child
|
||||
const modifiedProps: IMenuSubProps = {
|
||||
parentId: `${parentId}-${finalId}`,
|
||||
tabIndex,
|
||||
show,
|
||||
enter: accordionEnter,
|
||||
toggle: propToggle,
|
||||
handleClick,
|
||||
handleEntered,
|
||||
handleExited
|
||||
};
|
||||
|
||||
return cloneElement(child, modifiedProps);
|
||||
};
|
||||
|
||||
const renderChildren = () => {
|
||||
const modifiedChildren = Children.map(children, (child) => {
|
||||
if (isValidElement(child)) {
|
||||
if (child.type === MenuLink) {
|
||||
return renderLink(child);
|
||||
} else if (child.type === MenuToggle) {
|
||||
return renderToggle(child);
|
||||
} else if (child.type === MenuLabel) {
|
||||
return renderLabel(child);
|
||||
} else if (child.type === MenuHeading) {
|
||||
return renderHeading(child);
|
||||
} else if (child.type === MenuSub && propToggle === 'dropdown') {
|
||||
return renderSubDropdown(child);
|
||||
} else if (child.type === MenuSub && propToggle === 'accordion') {
|
||||
return renderSubAccordion(child);
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
});
|
||||
|
||||
return modifiedChildren;
|
||||
};
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
current: menuItemRef.current,
|
||||
show: () => {
|
||||
handleShow();
|
||||
},
|
||||
hide: () => {
|
||||
handleHide();
|
||||
},
|
||||
isOpen: () => {
|
||||
return show;
|
||||
}
|
||||
}),
|
||||
[show]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
if (onShow) {
|
||||
onShow();
|
||||
}
|
||||
} else {
|
||||
if (onHide) {
|
||||
onHide();
|
||||
}
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
useEffect(() => {
|
||||
if (propToggle === 'accordion' && multipleExpand === false) {
|
||||
setShow(accordionShow);
|
||||
}
|
||||
}, [accordionShow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (highlight) {
|
||||
if (hasMenuActiveChild(pathname, children)) {
|
||||
if (propToggle === 'accordion') {
|
||||
setShow(true);
|
||||
}
|
||||
|
||||
setHere(true);
|
||||
} else {
|
||||
if (propToggle === 'accordion') {
|
||||
setShow(false);
|
||||
}
|
||||
|
||||
setHere(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevPathname !== pathname && hasSub && propToggle === 'dropdown') {
|
||||
handleHide();
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
// Cleanup: ensure that any timeouts are cleared when the component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...containerProps}
|
||||
ref={menuItemRef}
|
||||
tabIndex={tabIndex}
|
||||
{...(propToggle === 'dropdown' && {
|
||||
onMouseEnter: handleMouseEnter,
|
||||
onMouseLeave: handleMouseLeave
|
||||
})}
|
||||
className={clsx(
|
||||
'menu-item',
|
||||
propToggle === 'dropdown' && 'menu-item-dropdown',
|
||||
className && className,
|
||||
active && 'active',
|
||||
show && 'show',
|
||||
here && 'here',
|
||||
transitioning && 'transitioning'
|
||||
)}
|
||||
>
|
||||
{renderChildren()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const MenuItem = memo(MenuItemComponent);
|
||||
export { MenuItem };
|
||||
26
src/components/menu/MenuLabel.tsx
Normal file
26
src/components/menu/MenuLabel.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import clsx from 'clsx';
|
||||
import { IMenuLabelProps } from './';
|
||||
|
||||
const MenuLabel = ({
|
||||
className,
|
||||
hasItemSub,
|
||||
handleToggle,
|
||||
handleClick,
|
||||
children
|
||||
}: IMenuLabelProps) => {
|
||||
if (hasItemSub) {
|
||||
return (
|
||||
<div className={clsx('menu-label', className && className)} onClick={handleToggle}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={clsx('menu-label', className && className)} onClick={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { MenuLabel };
|
||||
54
src/components/menu/MenuLink.tsx
Normal file
54
src/components/menu/MenuLink.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import clsx from 'clsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { IMenuLinkProps } from './';
|
||||
|
||||
const MenuLink = ({
|
||||
path,
|
||||
newTab,
|
||||
hasItemSub = false,
|
||||
externalLink,
|
||||
className,
|
||||
handleToggle,
|
||||
handleClick,
|
||||
children
|
||||
}: IMenuLinkProps) => {
|
||||
if (!hasItemSub && path) {
|
||||
if (externalLink) {
|
||||
const target = newTab ? '_blank' : '_self';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={path}
|
||||
target={target}
|
||||
rel="noopener"
|
||||
onClick={handleClick}
|
||||
className={clsx('menu-link', className && className)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link to={path} onClick={handleClick} className={clsx('menu-link', className && className)}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (hasItemSub) {
|
||||
return (
|
||||
<div className={clsx('menu-link', className && className)} onClick={handleToggle}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={clsx('menu-link', className && className)} onClick={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { MenuLink };
|
||||
9
src/components/menu/MenuSeparator.tsx
Normal file
9
src/components/menu/MenuSeparator.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuSeparatorProps } from './';
|
||||
|
||||
const MenuSeparator = ({ className }: IMenuSeparatorProps) => {
|
||||
return <div className={clsx('menu-separator', className && className)}></div>;
|
||||
};
|
||||
|
||||
export { MenuSeparator };
|
||||
77
src/components/menu/MenuSub.tsx
Normal file
77
src/components/menu/MenuSub.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { Collapse } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import { Children, cloneElement, forwardRef, isValidElement, memo } from 'react';
|
||||
import { IMenuItemProps, IMenuSubProps, MenuItem } from './';
|
||||
|
||||
const MenuSubComponent = forwardRef<HTMLDivElement | null, IMenuSubProps>(
|
||||
function MenuSub(props, ref) {
|
||||
const {
|
||||
show,
|
||||
enter,
|
||||
toggle = 'accordion',
|
||||
className,
|
||||
handleParentHide,
|
||||
handleEntered,
|
||||
handleExited,
|
||||
children,
|
||||
parentId
|
||||
} = props;
|
||||
|
||||
const finalParentId = parentId !== undefined ? parentId : 'root';
|
||||
|
||||
const modifiedChildren = Children.map(children, (child, index) => {
|
||||
if (isValidElement(child)) {
|
||||
if (child.type === MenuItem) {
|
||||
// Add some props to each child
|
||||
const modifiedProps: IMenuItemProps = {
|
||||
handleParentHide,
|
||||
parentId: finalParentId,
|
||||
id: `${finalParentId}-${index}`
|
||||
};
|
||||
|
||||
// Return the child with modified props
|
||||
return cloneElement(child, modifiedProps);
|
||||
} else {
|
||||
return cloneElement(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the child as is if it's not a valid React element
|
||||
return child;
|
||||
});
|
||||
|
||||
const renderContent = () => {
|
||||
if (toggle === 'accordion') {
|
||||
return (
|
||||
<Collapse
|
||||
in={show}
|
||||
onEntered={handleEntered}
|
||||
onExited={handleExited}
|
||||
timeout="auto"
|
||||
enter={enter}
|
||||
>
|
||||
{modifiedChildren}
|
||||
</Collapse>
|
||||
);
|
||||
} else {
|
||||
return modifiedChildren;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
toggle === 'accordion' && 'menu-accordion',
|
||||
toggle === 'dropdown' && 'menu-dropdown',
|
||||
className && className
|
||||
)}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const MenuSub = memo(MenuSubComponent);
|
||||
export { MenuSub };
|
||||
11
src/components/menu/MenuTitle.tsx
Normal file
11
src/components/menu/MenuTitle.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { IMenuTitleProps } from './';
|
||||
|
||||
const MenuTitleComponent = ({ className, children }: IMenuTitleProps) => {
|
||||
return <div className={clsx('menu-title', className && className)}>{children}</div>;
|
||||
};
|
||||
|
||||
const MenuTitle = memo(MenuTitleComponent);
|
||||
export { MenuTitle };
|
||||
22
src/components/menu/MenuToggle.tsx
Normal file
22
src/components/menu/MenuToggle.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IMenuToggleProps } from './';
|
||||
|
||||
const MenuToggle = ({
|
||||
className,
|
||||
hasItemSub = false,
|
||||
handleToggle,
|
||||
children
|
||||
}: IMenuToggleProps) => {
|
||||
if (hasItemSub) {
|
||||
return (
|
||||
<div className={clsx('menu-toggle', className && className)} onClick={handleToggle}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div className={clsx('menu-toggle', className && className)}>{children}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
export { MenuToggle };
|
||||
37
src/components/menu/hooks/useMenuBreadcrumbs.ts
Normal file
37
src/components/menu/hooks/useMenuBreadcrumbs.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { matchPath } from 'react-router';
|
||||
|
||||
import { TMenuBreadcrumbs, TMenuConfig } from '../types';
|
||||
|
||||
const useMenuBreadcrumbs = (pathname: string, items: TMenuConfig | null): TMenuBreadcrumbs => {
|
||||
pathname = pathname.trim();
|
||||
|
||||
const findParents = (items: TMenuConfig | null): TMenuBreadcrumbs => {
|
||||
if (!items) return [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.path && matchPath(pathname, item.path)) {
|
||||
return [
|
||||
{
|
||||
title: item.title,
|
||||
path: item.path,
|
||||
active: true
|
||||
}
|
||||
];
|
||||
} else if (item.children) {
|
||||
const parents = findParents(item.children as TMenuConfig);
|
||||
|
||||
if (parents.length > 0) {
|
||||
return [item, ...parents];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
return findParents(items);
|
||||
};
|
||||
|
||||
export { useMenuBreadcrumbs };
|
||||
59
src/components/menu/hooks/useMenuChildren.ts
Normal file
59
src/components/menu/hooks/useMenuChildren.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { matchPath } from 'react-router';
|
||||
import { TMenuConfig } from '../types.d';
|
||||
|
||||
const useMenuChildren = (
|
||||
pathname: string,
|
||||
items: TMenuConfig,
|
||||
level: number
|
||||
): TMenuConfig | null => {
|
||||
const hasActiveChild = (items: TMenuConfig): boolean => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.path && matchPath(pathname, item.path)) {
|
||||
return true;
|
||||
} else if (item.children) {
|
||||
if (hasActiveChild(item.children as TMenuConfig)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getChildren = (
|
||||
items: TMenuConfig,
|
||||
level: number = 0,
|
||||
currentLevel: number = 0
|
||||
): TMenuConfig | null => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.children) {
|
||||
// Check if we're at the desired level and if any child is active
|
||||
if (level === currentLevel && hasActiveChild(item.children)) {
|
||||
return item.children;
|
||||
}
|
||||
|
||||
// Recursively check the children, incrementing the current level
|
||||
const children = getChildren(item.children, level, currentLevel + 1);
|
||||
|
||||
// If valid children were found, return them
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
} else if (level === currentLevel && item.path && matchPath(pathname, item.path)) {
|
||||
// If it's a leaf node and matches the path, return the current items
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
// Return null if no match was found at this level
|
||||
return null;
|
||||
};
|
||||
|
||||
return getChildren(items, level);
|
||||
};
|
||||
|
||||
export { useMenuChildren };
|
||||
33
src/components/menu/hooks/useMenuCurrentItem.ts
Normal file
33
src/components/menu/hooks/useMenuCurrentItem.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { matchPath } from 'react-router';
|
||||
|
||||
import { IMenuItemConfig, type TMenuConfig } from '../types';
|
||||
|
||||
const useMenuCurrentItem = (
|
||||
pathname: string,
|
||||
items: TMenuConfig | null
|
||||
): IMenuItemConfig | null => {
|
||||
pathname = pathname.trim();
|
||||
|
||||
const findCurrentItem = (items: TMenuConfig | null): IMenuItemConfig | null => {
|
||||
if (!items) return null;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.path && matchPath(pathname, item.path)) {
|
||||
return item ?? null;
|
||||
} else if (item.children) {
|
||||
const childItem = findCurrentItem(item.children as TMenuConfig);
|
||||
if (childItem) {
|
||||
return childItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return findCurrentItem(items);
|
||||
};
|
||||
|
||||
export { useMenuCurrentItem };
|
||||
17
src/components/menu/index.ts
Normal file
17
src/components/menu/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export * from './Menu';
|
||||
export * from './MenuBullet';
|
||||
export * from './MenuSeparator';
|
||||
export * from './MenuHeading';
|
||||
export * from './MenuIcon';
|
||||
export * from './MenuBadge';
|
||||
export * from './MenuItem';
|
||||
export * from './MenuLink';
|
||||
export * from './MenuToggle';
|
||||
export * from './MenuSub';
|
||||
export * from './MenuTitle';
|
||||
export * from './MenuLabel';
|
||||
export * from './MenuArrow';
|
||||
export * from './hooks/useMenuCurrentItem';
|
||||
export * from './hooks/useMenuBreadcrumbs';
|
||||
export * from './hooks/useMenuChildren';
|
||||
export * from './types.d';
|
||||
184
src/components/menu/types.d.ts
vendored
Normal file
184
src/components/menu/types.d.ts
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { PopperProps } from '@mui/base';
|
||||
import { TooltipProps } from '@mui/material/Tooltip';
|
||||
import { HTMLAttributes, MouseEvent, ReactNode, RefAttributes, RefObject } from 'react';
|
||||
|
||||
export type TMenuEventHandler = (e: MouseEvent<HTMLElement>) => void;
|
||||
|
||||
export type TMenuClickEvent = (e: MouseEvent<HTMLElement>, props: unknown) => void;
|
||||
|
||||
export type TMenuShow = boolean;
|
||||
|
||||
export type TMenuTrigger = 'click' | 'hover';
|
||||
|
||||
export type TMenuItemTrigger = Record<string, TMenuToggle> | TMenuTrigger;
|
||||
|
||||
export type TMenuToggle = 'accordion' | 'dropdown';
|
||||
|
||||
export type TMenuItemToggle = Record<string, TMenuToggle> | TMenuToggle;
|
||||
|
||||
export type TMenuDropdown = Partial<Omit<PopperProps, 'children'>>;
|
||||
|
||||
export type TMenuTabIndex = number;
|
||||
|
||||
export interface IMenuProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
disabled?: boolean;
|
||||
highlight?: boolean;
|
||||
dropdownTimeout?: number;
|
||||
multipleExpand?: boolean;
|
||||
}
|
||||
|
||||
export interface IMenuContextProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
highlight?: boolean;
|
||||
disabled?: boolean;
|
||||
dropdownTimeout?: number;
|
||||
multipleExpand?: boolean;
|
||||
setOpenAccordion: (parentId: string, id: string) => void;
|
||||
isOpenAccordion: (parentId: string, id: string) => boolean;
|
||||
}
|
||||
|
||||
export interface IMenuItemRef {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
isOpen: () => boolean;
|
||||
}
|
||||
|
||||
export interface IMenuItemProps {
|
||||
path?: string;
|
||||
id?: string;
|
||||
parentId?: string;
|
||||
open?: boolean;
|
||||
toggle?: TMenuItemToggle;
|
||||
trigger?: TMenuItemTrigger;
|
||||
disabled?: boolean;
|
||||
dropdownProps?: TMenuDropdown;
|
||||
dropdownZIndex?: number;
|
||||
className?: string;
|
||||
closeParentMenu?: CallableFunction;
|
||||
onClick?: TMenuClickEvent;
|
||||
onShow?: CallableFunction;
|
||||
onHide?: CallableFunction;
|
||||
handleParentHide?: CallableFunction;
|
||||
handleClick?: TMenuEventHandler;
|
||||
tabIndex?: TMenuTabIndex;
|
||||
itemRef?: unknown;
|
||||
containerProps?: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement | null>;
|
||||
containerRef?: RefObject<HTMLDivElement>;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuLinkProps {
|
||||
ref?: unknown;
|
||||
path?: string;
|
||||
externalLink?: boolean;
|
||||
newTab?: boolean;
|
||||
hasItemSub?: boolean;
|
||||
className?: string;
|
||||
tabIndex?: TMenuTabIndex;
|
||||
handleToggle?: TMenuEventHandler;
|
||||
handleClick?: TMenuEventHandler;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuLabelProps {
|
||||
hasItemSub?: boolean;
|
||||
className?: string;
|
||||
tabIndex?: TMenuTabIndex;
|
||||
handleToggle?: TMenuEventHandler;
|
||||
handleClick?: TMenuEventHandler;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuToggleProps {
|
||||
className?: string;
|
||||
tabIndex?: TMenuTabIndex;
|
||||
hasItemSub?: boolean;
|
||||
menuItemRef?: unknown;
|
||||
handleToggle?: TMenuEventHandler;
|
||||
handleClick?: TMenuEventHandler;
|
||||
onClick?: TMenuClickEvent;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuSubProps {
|
||||
parentId?: string;
|
||||
show?: TMenuShow;
|
||||
enter?: boolean;
|
||||
toggle?: TMenuToggle;
|
||||
ref?: unknown;
|
||||
menuItemRef?: unknown;
|
||||
tabIndex?: number;
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
baseClassName?: string;
|
||||
onClick?: TMenuClickEvent;
|
||||
handleParentHide?: CallableFunction;
|
||||
handleClick?: TMenuEventHandler;
|
||||
handleEntered?: () => void;
|
||||
handleExited?: () => void;
|
||||
accordionIn?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
export interface IMenuTitleProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuIconProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuBadgeProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuSeparatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface IMenuBulletProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuHeadingProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface IMenuItemConfig {
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
heading?: string;
|
||||
icon?: string;
|
||||
badge?: string;
|
||||
separator?: boolean;
|
||||
tooltip?: Partial<TooltipProps>;
|
||||
path?: string;
|
||||
rootPath?: string;
|
||||
bullet?: boolean;
|
||||
collapse?: boolean;
|
||||
collapseTitle?: string;
|
||||
expandTitle?: string;
|
||||
toggle?: TMenuItemToggle;
|
||||
dropdownProps?: TMenuDropdown;
|
||||
trigger?: TMenuItemTrigger;
|
||||
children?: IMenuItemConfig[];
|
||||
childrenIndex?: number;
|
||||
}
|
||||
|
||||
export type TMenuConfig = IMenuItemConfig[];
|
||||
|
||||
export interface IMenuBreadcrumb {
|
||||
title?: string;
|
||||
path?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export type TMenuBreadcrumbs = IMenuBreadcrumb[];
|
||||
39
src/components/menu/utils.ts
Normal file
39
src/components/menu/utils.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Children, isValidElement, ReactNode } from 'react';
|
||||
import { MenuLink } from './MenuLink';
|
||||
import { matchPath } from 'react-router';
|
||||
|
||||
export const getMenuLinkPath = (children: ReactNode): string => {
|
||||
let path = '';
|
||||
|
||||
Children.forEach(children, (child) => {
|
||||
if (isValidElement(child) && child.type === MenuLink && child.props.path) {
|
||||
path = child.props.path; // Assign the path when found
|
||||
}
|
||||
});
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const hasMenuActiveChild = (path: string, children: ReactNode): boolean => {
|
||||
const childrenArray: ReactNode[] = Children.toArray(children);
|
||||
|
||||
for (const child of childrenArray) {
|
||||
if (isValidElement(child)) {
|
||||
if (child.type === MenuLink && child.props.path) {
|
||||
if (path === '/') {
|
||||
if (child.props.path === path) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (matchPath(child.props.path as string, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (hasMenuActiveChild(path, child.props.children as ReactNode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
Reference in New Issue
Block a user