revamp template

This commit is contained in:
fro1991
2025-02-01 16:44:51 +07:00
parent c432df568a
commit 276a289580
1212 changed files with 112762 additions and 0 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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
View 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[];

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