initial commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType } from '../types'
|
||||
|
||||
type RouterLinkProps = LinkProps &
|
||||
Partial<ChildrenType> & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const RouterLink = forwardRef((props: RouterLinkProps, ref: any) => {
|
||||
// Props
|
||||
const { href, className, ...other } = props
|
||||
|
||||
return (
|
||||
<Link ref={ref} href={href} className={className} {...other}>
|
||||
{props.children}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { BreakpointType, ChildrenType } from '../../types'
|
||||
import type { VerticalNavProps } from '../vertical-menu/VerticalNav'
|
||||
|
||||
// Component Imports
|
||||
import VerticalNavInHorizontal from './VerticalNavInHorizontal'
|
||||
|
||||
// Hook Imports
|
||||
import useMediaQuery from '../../hooks/useMediaQuery'
|
||||
import useHorizontalNav from '../../hooks/useHorizontalNav'
|
||||
|
||||
// Util Imports
|
||||
import { horizontalNavClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledHorizontalNav from '../../styles/horizontal/StyledHorizontalNav'
|
||||
|
||||
// Default Config Imports
|
||||
import { defaultBreakpoints } from '../../defaultConfigs'
|
||||
|
||||
export type HorizontalNavProps = HTMLAttributes<HTMLDivElement> & {
|
||||
switchToVertical?: boolean
|
||||
hideMenu?: boolean
|
||||
breakpoint?: BreakpointType
|
||||
customBreakpoint?: string
|
||||
breakpoints?: Partial<typeof defaultBreakpoints>
|
||||
customStyles?: CSSObject
|
||||
verticalNavProps?: Pick<VerticalNavProps, 'width' | 'backgroundColor' | 'backgroundImage' | 'customStyles'>
|
||||
verticalNavContent?: ({ children }: ChildrenType) => JSX.Element
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
setIsBreakpointReached?: (isBreakpointReached: boolean) => void
|
||||
}
|
||||
|
||||
const HorizontalNav = (props: HorizontalNavProps) => {
|
||||
// Props
|
||||
const {
|
||||
switchToVertical = false,
|
||||
hideMenu = false,
|
||||
breakpoint = 'lg',
|
||||
customBreakpoint,
|
||||
breakpoints,
|
||||
customStyles,
|
||||
className,
|
||||
children,
|
||||
verticalNavProps,
|
||||
verticalNavContent: VerticalNavContent
|
||||
} = props
|
||||
|
||||
// Vars
|
||||
const mergedBreakpoints = { ...defaultBreakpoints, ...breakpoints }
|
||||
const horizontalMenuClasses = classnames(horizontalNavClasses.root, className)
|
||||
|
||||
// Refs
|
||||
const prevBreakpoint = useRef(false)
|
||||
|
||||
// Hooks
|
||||
const { updateIsBreakpointReached } = useHorizontalNav()
|
||||
|
||||
// Find the breakpoint from which screen size responsive behavior should enable and if its reached or not
|
||||
const breakpointReached = useMediaQuery(customBreakpoint ?? (breakpoint ? mergedBreakpoints[breakpoint] : breakpoint))
|
||||
|
||||
// Set the breakpointReached value in the state
|
||||
useEffect(() => {
|
||||
if (prevBreakpoint.current === breakpointReached) return
|
||||
updateIsBreakpointReached(breakpointReached)
|
||||
prevBreakpoint.current = breakpointReached
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [breakpointReached])
|
||||
|
||||
// If switchToVertical is true, then render the VerticalNav component if breakpoint is reached
|
||||
if (switchToVertical && breakpointReached) {
|
||||
return (
|
||||
<VerticalNavInHorizontal
|
||||
breakpoint={breakpoint}
|
||||
className={horizontalMenuClasses}
|
||||
customBreakpoint={customBreakpoint}
|
||||
verticalNavProps={verticalNavProps}
|
||||
>
|
||||
{VerticalNavContent && <VerticalNavContent>{children}</VerticalNavContent>}
|
||||
</VerticalNavInHorizontal>
|
||||
)
|
||||
}
|
||||
|
||||
// If hideMenu is true, then hide the HorizontalNav component if breakpoint is reached
|
||||
if (hideMenu && breakpointReached) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If switchToVertical & hideMenu are false, then render the HorizontalNav component
|
||||
return (
|
||||
<StyledHorizontalNav customStyles={customStyles} className={horizontalMenuClasses}>
|
||||
{children}
|
||||
</StyledHorizontalNav>
|
||||
)
|
||||
}
|
||||
|
||||
export default HorizontalNav
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { createContext, forwardRef, useMemo } from 'react'
|
||||
import type { ForwardRefRenderFunction, MenuHTMLAttributes, ReactElement } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { FloatingTree } from '@floating-ui/react'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuProps as VerticalMenuProps } from '../vertical-menu/Menu'
|
||||
import type {
|
||||
ChildrenType,
|
||||
MenuItemStyles,
|
||||
RenderExpandIconParams,
|
||||
RenderExpandedMenuItemIcon,
|
||||
RootStylesType
|
||||
} from '../../types'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledHorizontalMenu from '../../styles/horizontal/StyledHorizontalMenu'
|
||||
|
||||
// Style Imports
|
||||
import styles from '../../styles/horizontal/horizontalUl.module.css'
|
||||
|
||||
// Default Config Imports
|
||||
import { horizontalSubMenuToggleDuration } from '../../defaultConfigs'
|
||||
|
||||
export type HorizontalMenuContextProps = {
|
||||
triggerPopout?: 'hover' | 'click'
|
||||
browserScroll?: boolean
|
||||
menuItemStyles?: MenuItemStyles
|
||||
renderExpandIcon?: (params: RenderExpandIconParams) => ReactElement
|
||||
renderExpandedMenuItemIcon?: RenderExpandedMenuItemIcon
|
||||
transitionDuration?: number
|
||||
popoutMenuOffset?: {
|
||||
mainAxis?: number | ((params: { level?: number }) => number)
|
||||
alignmentAxis?: number | ((params: { level?: number }) => number)
|
||||
}
|
||||
textTruncate?: boolean
|
||||
verticalMenuProps?: Pick<
|
||||
VerticalMenuProps,
|
||||
| 'transitionDuration'
|
||||
| 'menuSectionStyles'
|
||||
| 'menuItemStyles'
|
||||
| 'subMenuOpenBehavior'
|
||||
| 'renderExpandIcon'
|
||||
| 'renderExpandedMenuItemIcon'
|
||||
| 'textTruncate'
|
||||
| 'rootStyles'
|
||||
>
|
||||
}
|
||||
|
||||
export type MenuProps = HorizontalMenuContextProps &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> &
|
||||
MenuHTMLAttributes<HTMLMenuElement>
|
||||
|
||||
export const HorizontalMenuContext = createContext({} as HorizontalMenuContextProps)
|
||||
|
||||
const Menu: ForwardRefRenderFunction<HTMLMenuElement, MenuProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
rootStyles,
|
||||
menuItemStyles,
|
||||
triggerPopout = 'hover',
|
||||
browserScroll = false,
|
||||
transitionDuration = horizontalSubMenuToggleDuration,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
popoutMenuOffset = { mainAxis: 0 },
|
||||
textTruncate = true,
|
||||
verticalMenuProps,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const providerValue = useMemo(
|
||||
() => ({
|
||||
triggerPopout,
|
||||
browserScroll,
|
||||
menuItemStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
transitionDuration,
|
||||
popoutMenuOffset,
|
||||
textTruncate,
|
||||
verticalMenuProps
|
||||
}),
|
||||
[
|
||||
triggerPopout,
|
||||
browserScroll,
|
||||
menuItemStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
transitionDuration,
|
||||
popoutMenuOffset,
|
||||
textTruncate,
|
||||
verticalMenuProps
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<HorizontalMenuContext.Provider value={providerValue}>
|
||||
<FloatingTree>
|
||||
<StyledHorizontalMenu
|
||||
ref={ref}
|
||||
className={classnames(menuClasses.root, className)}
|
||||
rootStyles={rootStyles}
|
||||
{...rest}
|
||||
>
|
||||
<ul className={styles.root}>{children}</ul>
|
||||
</StyledHorizontalMenu>
|
||||
</FloatingTree>
|
||||
</HorizontalMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(Menu)
|
||||
@@ -0,0 +1,115 @@
|
||||
// React Imports
|
||||
import { cloneElement, createElement, forwardRef } from 'react'
|
||||
import type { ForwardRefRenderFunction } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { css } from '@emotion/react'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, MenuButtonProps } from '../../types'
|
||||
|
||||
// Component Imports
|
||||
import { RouterLink } from '../RouterLink'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
type MenuButtonStylesProps = Partial<ChildrenType> & {
|
||||
level: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const menuButtonStyles = (props: MenuButtonStylesProps) => {
|
||||
// Props
|
||||
const { level, disabled, children } = props
|
||||
|
||||
return css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minBlockSize: '30px',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'pointer',
|
||||
paddingInline: '20px',
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: '#f3f3f3'
|
||||
},
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: 'none',
|
||||
backgroundColor: '#f3f3f3'
|
||||
},
|
||||
|
||||
...(disabled && {
|
||||
pointerEvents: 'none',
|
||||
cursor: 'default',
|
||||
color: '#adadad'
|
||||
}),
|
||||
|
||||
// All the active styles are applied to the button including menu items or submenu
|
||||
[`&.${menuClasses.active}`]: {
|
||||
...(level === 0
|
||||
? {
|
||||
color: 'white',
|
||||
backgroundColor: '#765feb'
|
||||
}
|
||||
: {
|
||||
...(children ? { backgroundColor: '#f3f3f3' } : { color: '#765feb', backgroundColor: '#765feb1f' })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const MenuButton: ForwardRefRenderFunction<HTMLAnchorElement, MenuButtonProps> = (
|
||||
{ className, component, children, ...rest },
|
||||
ref
|
||||
) => {
|
||||
if (component) {
|
||||
// If component is a string, create a new element of that type
|
||||
if (typeof component === 'string') {
|
||||
return createElement(
|
||||
component,
|
||||
{
|
||||
className: classnames(className),
|
||||
...rest,
|
||||
ref
|
||||
},
|
||||
children
|
||||
)
|
||||
} else {
|
||||
// Otherwise, clone the element
|
||||
const { className: classNameProp, ...props } = component.props
|
||||
|
||||
return cloneElement(
|
||||
component,
|
||||
{
|
||||
className: classnames(className, classNameProp),
|
||||
...rest,
|
||||
...props,
|
||||
ref
|
||||
},
|
||||
children
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If there is no component but href is defined, render RouterLink
|
||||
if (rest.href) {
|
||||
return (
|
||||
<RouterLink ref={ref} className={className} href={rest.href} {...rest}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<a ref={ref} className={className} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default forwardRef(MenuButton)
|
||||
@@ -0,0 +1,207 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { forwardRef, useContext, useEffect, useState } from 'react'
|
||||
import type { AnchorHTMLAttributes, ForwardRefRenderFunction, ReactElement, MouseEvent, ReactNode } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { useUpdateEffect } from 'react-use'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
import { useFloatingTree } from '@floating-ui/react'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, MenuItemElement, MenuItemExactMatchUrlProps, RootStylesType } from '../../types'
|
||||
|
||||
// Context Imports
|
||||
import { HorizontalSubMenuContext } from './SubMenu'
|
||||
|
||||
// Component Imports
|
||||
import MenuButton from './MenuButton'
|
||||
|
||||
// Hook Imports
|
||||
import useHorizontalMenu from '../../hooks/useHorizontalMenu'
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
|
||||
// Util Imports
|
||||
import { renderMenuIcon } from '../../utils/menuUtils'
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledMenuLabel from '../../styles/StyledMenuLabel'
|
||||
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
|
||||
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
|
||||
import StyledHorizontalMenuItem from '../../styles/horizontal/StyledHorizontalMenuItem'
|
||||
|
||||
// Style Imports
|
||||
import styles from '../../styles/horizontal/horizontalUl.module.css'
|
||||
|
||||
export type MenuItemProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> &
|
||||
MenuItemExactMatchUrlProps & {
|
||||
icon?: ReactElement
|
||||
prefix?: ReactNode
|
||||
suffix?: ReactNode
|
||||
disabled?: boolean
|
||||
target?: string
|
||||
rel?: string
|
||||
component?: string | ReactElement
|
||||
onActiveChange?: (active: boolean) => void
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
level?: number
|
||||
}
|
||||
|
||||
const MenuItem: ForwardRefRenderFunction<HTMLLIElement, MenuItemProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
prefix,
|
||||
suffix,
|
||||
level = 0,
|
||||
disabled = false,
|
||||
exactMatch = true,
|
||||
activeUrl,
|
||||
component,
|
||||
onActiveChange,
|
||||
rootStyles,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const tree = useFloatingTree()
|
||||
const pathname = usePathname()
|
||||
const { toggleVerticalNav, isToggled } = useVerticalNav()
|
||||
const { getItemProps } = useContext(HorizontalSubMenuContext)
|
||||
const { menuItemStyles, renderExpandedMenuItemIcon, textTruncate } = useHorizontalMenu()
|
||||
|
||||
const getMenuItemStyles = (element: MenuItemElement): CSSObject | undefined => {
|
||||
// If the menuItemStyles prop is provided, get the styles for the specified element.
|
||||
if (menuItemStyles) {
|
||||
// Define the parameters that are passed to the style functions.
|
||||
const params = { level, disabled, active, isSubmenu: false }
|
||||
|
||||
// Get the style function for the specified element.
|
||||
const styleFunction = menuItemStyles[element]
|
||||
|
||||
if (styleFunction) {
|
||||
// If the style function is a function, call it and return the result.
|
||||
// Otherwise, return the style function itself.
|
||||
return typeof styleFunction === 'function' ? styleFunction(params) : styleFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the click event.
|
||||
const handleClick = () => {
|
||||
if (isToggled) {
|
||||
toggleVerticalNav()
|
||||
}
|
||||
}
|
||||
|
||||
// Change active state when the url changes
|
||||
useEffect(() => {
|
||||
const href = rest.href || (component && typeof component !== 'string' && component.props.href)
|
||||
|
||||
if (href) {
|
||||
// Check if the current url matches any of the children urls
|
||||
if (exactMatch ? pathname === href : activeUrl && pathname.includes(activeUrl)) {
|
||||
setActive(true)
|
||||
} else {
|
||||
setActive(false)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname])
|
||||
|
||||
// Call the onActiveChange callback when the active state changes.
|
||||
useUpdateEffect(() => {
|
||||
onActiveChange?.(active)
|
||||
}, [active])
|
||||
|
||||
return (
|
||||
<StyledHorizontalMenuItem
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
{ [menuClasses.menuItemRoot]: level === 0 },
|
||||
{ [menuClasses.active]: active },
|
||||
{ [menuClasses.disabled]: disabled },
|
||||
styles.li,
|
||||
className
|
||||
)}
|
||||
level={level}
|
||||
disabled={disabled}
|
||||
buttonStyles={getMenuItemStyles('button')}
|
||||
menuItemStyles={getMenuItemStyles('root')}
|
||||
rootStyles={rootStyles}
|
||||
>
|
||||
<MenuButton
|
||||
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
|
||||
component={component}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={handleClick}
|
||||
{...getItemProps({
|
||||
onClick(event: MouseEvent<HTMLAnchorElement>) {
|
||||
props.onClick?.(event)
|
||||
tree?.events.emit('click')
|
||||
}
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
{/* Menu Item Icon */}
|
||||
{renderMenuIcon({
|
||||
icon,
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
renderExpandedMenuItemIcon,
|
||||
styles: getMenuItemStyles('icon')
|
||||
})}
|
||||
|
||||
{/* Menu Item Prefix */}
|
||||
{prefix && (
|
||||
<StyledMenuPrefix
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.prefix}
|
||||
rootStyles={getMenuItemStyles('prefix')}
|
||||
>
|
||||
{prefix}
|
||||
</StyledMenuPrefix>
|
||||
)}
|
||||
|
||||
{/* Menu Item Label */}
|
||||
<StyledMenuLabel
|
||||
className={menuClasses.label}
|
||||
rootStyles={getMenuItemStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{children}
|
||||
</StyledMenuLabel>
|
||||
|
||||
{/* Menu Item Suffix */}
|
||||
{suffix && (
|
||||
<StyledMenuSuffix
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.suffix}
|
||||
rootStyles={getMenuItemStyles('suffix')}
|
||||
>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
</MenuButton>
|
||||
</StyledHorizontalMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(MenuItem)
|
||||
@@ -0,0 +1,463 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { Children, cloneElement, createContext, forwardRef, useEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
ForwardRefRenderFunction,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
HTMLProps,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
|
||||
// Next Imports
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import styled from '@emotion/styled'
|
||||
import {
|
||||
useFloating,
|
||||
autoUpdate,
|
||||
offset,
|
||||
flip,
|
||||
shift,
|
||||
useHover,
|
||||
useRole,
|
||||
useInteractions,
|
||||
useClick,
|
||||
safePolygon,
|
||||
useDismiss,
|
||||
useFloatingNodeId,
|
||||
FloatingNode,
|
||||
FloatingPortal,
|
||||
useMergeRefs,
|
||||
useFloatingParentNodeId,
|
||||
useFloatingTree,
|
||||
useTransitionStyles
|
||||
} from '@floating-ui/react'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, RootStylesType, SubMenuItemElement } from '../../types'
|
||||
import type { MenuItemProps } from './MenuItem'
|
||||
|
||||
// Component Imports
|
||||
import SubMenuContent from './SubMenuContent'
|
||||
|
||||
// Hook Imports
|
||||
import useHorizontalMenu from '../../hooks/useHorizontalMenu'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
import { confirmUrlInChildren, renderMenuIcon } from '../../utils/menuUtils'
|
||||
|
||||
// Styled Component Imports
|
||||
import MenuButton, { menuButtonStyles } from './MenuButton'
|
||||
import StyledMenuLabel from '../../styles/StyledMenuLabel'
|
||||
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
|
||||
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
|
||||
import StyledHorizontalNavExpandIcon, {
|
||||
StyledHorizontalNavExpandIconWrapper
|
||||
} from '../../styles/horizontal/StyledHorizontalNavExpandIcon'
|
||||
import StyledSubMenuContentWrapper from '../../styles/horizontal/StyledHorizontalSubMenuContentWrapper'
|
||||
|
||||
// Style Imports
|
||||
import ulStyles from '../../styles/horizontal/horizontalUl.module.css'
|
||||
|
||||
// Icon Imports
|
||||
import ChevronRight from '../../svg/ChevronRight'
|
||||
|
||||
export type SubMenuProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> & {
|
||||
label: ReactNode
|
||||
icon?: ReactElement
|
||||
prefix?: ReactNode
|
||||
suffix?: ReactNode
|
||||
disabled?: boolean
|
||||
component?: string | ReactElement
|
||||
contentClassName?: string
|
||||
onOpenChange?: (open: boolean) => void
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
level?: number
|
||||
}
|
||||
|
||||
type StyledSubMenuProps = Pick<SubMenuProps, 'rootStyles' | 'disabled'> & {
|
||||
level: number
|
||||
active?: boolean
|
||||
menuItemStyles?: CSSObject
|
||||
buttonStyles?: CSSObject
|
||||
}
|
||||
|
||||
type HorizontalSubMenuContextProps = {
|
||||
getItemProps: (userProps?: HTMLProps<HTMLElement>) => Record<string, unknown>
|
||||
}
|
||||
|
||||
const StyledSubMenu = styled.li<StyledSubMenuProps>`
|
||||
${({ level }) => level === 0 && { borderRadius: '6px', overflow: 'hidden' }}
|
||||
|
||||
&.${menuClasses.open} > .${menuClasses.button} {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
${({ menuItemStyles }) => menuItemStyles};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
|
||||
> .${menuClasses.button} {
|
||||
${({ level, disabled, children }) =>
|
||||
menuButtonStyles({
|
||||
level,
|
||||
disabled,
|
||||
children
|
||||
})};
|
||||
${({ buttonStyles }) => buttonStyles};
|
||||
}
|
||||
`
|
||||
|
||||
export const HorizontalSubMenuContext = createContext<HorizontalSubMenuContextProps>({ getItemProps: () => ({}) })
|
||||
|
||||
const SubMenu: ForwardRefRenderFunction<HTMLLIElement, SubMenuProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
label,
|
||||
icon,
|
||||
title,
|
||||
prefix,
|
||||
suffix,
|
||||
level = 0,
|
||||
disabled = false,
|
||||
rootStyles,
|
||||
component,
|
||||
onClick,
|
||||
onKeyUp,
|
||||
onOpenChange,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
// Refs
|
||||
const dir = useRef('ltr')
|
||||
const listItemsRef = useRef<Array<HTMLButtonElement | null>>([])
|
||||
|
||||
// Hooks
|
||||
const pathname = usePathname()
|
||||
const tree = useFloatingTree()
|
||||
const nodeId = useFloatingNodeId()
|
||||
const parentId = useFloatingParentNodeId()
|
||||
|
||||
const {
|
||||
triggerPopout,
|
||||
renderExpandIcon,
|
||||
menuItemStyles,
|
||||
browserScroll,
|
||||
transitionDuration,
|
||||
renderExpandedMenuItemIcon,
|
||||
popoutMenuOffset,
|
||||
textTruncate
|
||||
} = useHorizontalMenu()
|
||||
|
||||
// Vars
|
||||
// Filter out falsy values from children
|
||||
const childNodes = Children.toArray(children).filter(Boolean) as [ReactElement<SubMenuProps | MenuItemProps>]
|
||||
|
||||
const mainAxisOffset =
|
||||
popoutMenuOffset &&
|
||||
popoutMenuOffset.mainAxis &&
|
||||
(typeof popoutMenuOffset.mainAxis === 'function' ? popoutMenuOffset.mainAxis({ level }) : popoutMenuOffset.mainAxis)
|
||||
|
||||
const alignmentAxisOffset =
|
||||
popoutMenuOffset &&
|
||||
popoutMenuOffset.alignmentAxis &&
|
||||
(typeof popoutMenuOffset.alignmentAxis === 'function'
|
||||
? popoutMenuOffset.alignmentAxis({ level })
|
||||
: popoutMenuOffset.alignmentAxis)
|
||||
|
||||
useEffect(() => {
|
||||
dir.current = window.getComputedStyle(document.documentElement).getPropertyValue('direction')
|
||||
}, [])
|
||||
|
||||
const { y, refs, floatingStyles, context } = useFloating({
|
||||
open,
|
||||
nodeId,
|
||||
onOpenChange: setOpen,
|
||||
placement: level > 0 ? (dir.current !== 'rtl' ? 'right-start' : 'left-start') : 'bottom-start',
|
||||
middleware: [
|
||||
offset({
|
||||
mainAxis: mainAxisOffset,
|
||||
alignmentAxis: alignmentAxisOffset
|
||||
}),
|
||||
flip({ crossAxis: false }),
|
||||
shift()
|
||||
],
|
||||
whileElementsMounted: autoUpdate
|
||||
})
|
||||
|
||||
// Floating UI Transition Styles
|
||||
const { isMounted, styles } = useTransitionStyles(context, {
|
||||
// Configure both open and close durations:
|
||||
duration: transitionDuration,
|
||||
|
||||
initial: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(10px)'
|
||||
},
|
||||
open: {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0px)'
|
||||
},
|
||||
close: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(10px)'
|
||||
}
|
||||
})
|
||||
|
||||
const hover = useHover(context, {
|
||||
handleClose: safePolygon({
|
||||
blockPointerEvents: true
|
||||
}), // safePolygon function allows us to reach to submenu
|
||||
restMs: 25, // Only opens submenu when cursor rests for 25ms on a menu
|
||||
enabled: triggerPopout === 'hover', // Only enable hover effect when triggerPopout option is set to 'hover',
|
||||
delay: { open: 75 } // Delay opening submenu by 75ms
|
||||
})
|
||||
|
||||
const click = useClick(context, {
|
||||
enabled: triggerPopout === 'click', // Only enable click effect when triggerPopout option is set to 'click'
|
||||
toggle: false
|
||||
})
|
||||
|
||||
const dismiss = useDismiss(context)
|
||||
const role = useRole(context, { role: 'menu' })
|
||||
|
||||
// Merge all the interactions into prop getters
|
||||
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([hover, click, dismiss, role])
|
||||
|
||||
const handleOnClick = (event: MouseEvent<HTMLAnchorElement, globalThis.MouseEvent>) => {
|
||||
onClick?.(event)
|
||||
triggerPopout === 'click' && setOpen(!open)
|
||||
}
|
||||
|
||||
const handleOnKeyUp = (event: KeyboardEvent<HTMLAnchorElement>) => {
|
||||
onKeyUp?.(event)
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
setOpen(!open)
|
||||
}
|
||||
}
|
||||
|
||||
const getSubMenuItemStyles = (element: SubMenuItemElement): CSSObject | undefined => {
|
||||
// If the menuItemStyles prop is provided, get the styles for the specified element.
|
||||
if (menuItemStyles) {
|
||||
// Define the parameters that are passed to the style functions.
|
||||
const params = { level, disabled, active, isSubmenu: true, open: open }
|
||||
|
||||
// Get the style function for the specified element.
|
||||
const styleFunction = menuItemStyles[element]
|
||||
|
||||
if (styleFunction) {
|
||||
// If the style function is a function, call it and return the result.
|
||||
// Otherwise, return the style function itself.
|
||||
return typeof styleFunction === 'function' ? styleFunction(params) : styleFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event emitter allows you to communicate across tree components.
|
||||
// This effect closes all menus when an item gets clicked anywhere in the tree.
|
||||
useEffect(() => {
|
||||
const handleTreeClick = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onSubMenuOpen = (event: { nodeId: string; parentId: string }) => {
|
||||
if (event.nodeId !== nodeId && event.parentId === parentId) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
tree?.events.on('click', handleTreeClick)
|
||||
tree?.events.on('menuopen', onSubMenuOpen)
|
||||
|
||||
return () => {
|
||||
tree?.events.off('click', handleTreeClick)
|
||||
tree?.events.off('menuopen', onSubMenuOpen)
|
||||
}
|
||||
}, [tree, nodeId, parentId])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
tree?.events.emit('menuopen', {
|
||||
parentId,
|
||||
nodeId
|
||||
})
|
||||
}
|
||||
}, [tree, open, nodeId, parentId])
|
||||
|
||||
// Change active state when the url changes
|
||||
useEffect(() => {
|
||||
// Check if the current url matches any of the children urls
|
||||
if (confirmUrlInChildren(children, pathname)) {
|
||||
setActive(true)
|
||||
} else {
|
||||
setActive(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname])
|
||||
|
||||
// User event handler for open state change
|
||||
useEffect(() => {
|
||||
onOpenChange?.(open)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
// Merge the reference ref with the ref passed to the component
|
||||
const referenceRef = useMergeRefs([refs.setReference, ref])
|
||||
|
||||
return (
|
||||
<FloatingNode id={nodeId}>
|
||||
{/* Sub Menu */}
|
||||
<StyledSubMenu
|
||||
{...(!disabled && { ref: referenceRef, ...getReferenceProps() })}
|
||||
className={classnames(
|
||||
{ [menuClasses.subMenuRoot]: level === 0 },
|
||||
{ [menuClasses.active]: active },
|
||||
{ [menuClasses.disabled]: disabled },
|
||||
{ [menuClasses.open]: open },
|
||||
ulStyles.li,
|
||||
className
|
||||
)}
|
||||
menuItemStyles={getSubMenuItemStyles('root')}
|
||||
level={level}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
buttonStyles={getSubMenuItemStyles('button')}
|
||||
rootStyles={rootStyles}
|
||||
>
|
||||
{/* Sub Menu */}
|
||||
<MenuButton
|
||||
title={title}
|
||||
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
|
||||
component={component}
|
||||
onClick={handleOnClick}
|
||||
onKeyUp={handleOnKeyUp}
|
||||
{...rest}
|
||||
>
|
||||
{/* Sub Menu Icon */}
|
||||
{renderMenuIcon({
|
||||
icon,
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
renderExpandedMenuItemIcon,
|
||||
styles: getSubMenuItemStyles('icon')
|
||||
})}
|
||||
|
||||
{/* Sub Menu Prefix */}
|
||||
{prefix && (
|
||||
<StyledMenuPrefix
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.prefix}
|
||||
rootStyles={getSubMenuItemStyles('prefix')}
|
||||
>
|
||||
{prefix}
|
||||
</StyledMenuPrefix>
|
||||
)}
|
||||
|
||||
{/* Sub Menu Label */}
|
||||
<StyledMenuLabel
|
||||
className={menuClasses.label}
|
||||
rootStyles={getSubMenuItemStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{label}
|
||||
</StyledMenuLabel>
|
||||
|
||||
{/* Sub Menu Suffix */}
|
||||
{suffix && (
|
||||
<StyledMenuSuffix
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.suffix}
|
||||
rootStyles={getSubMenuItemStyles('suffix')}
|
||||
>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
|
||||
{/* Sub Menu Toggle Icon Wrapper */}
|
||||
<StyledHorizontalNavExpandIconWrapper
|
||||
className={menuClasses.subMenuExpandIcon}
|
||||
rootStyles={getSubMenuItemStyles('subMenuExpandIcon')}
|
||||
>
|
||||
{renderExpandIcon ? (
|
||||
renderExpandIcon({
|
||||
level,
|
||||
disabled,
|
||||
active,
|
||||
open: open
|
||||
})
|
||||
) : (
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
/* Expanded Arrow Icon */
|
||||
<StyledHorizontalNavExpandIcon level={level}>
|
||||
<ChevronRight fontSize='1rem' />
|
||||
</StyledHorizontalNavExpandIcon>
|
||||
)}
|
||||
</StyledHorizontalNavExpandIconWrapper>
|
||||
</MenuButton>
|
||||
|
||||
<HorizontalSubMenuContext.Provider value={{ getItemProps }}>
|
||||
<FloatingPortal>
|
||||
{isMounted && (
|
||||
<StyledSubMenuContentWrapper
|
||||
ref={refs.setFloating}
|
||||
{...getFloatingProps()}
|
||||
style={floatingStyles}
|
||||
rootStyles={getSubMenuItemStyles('subMenuStyles')}
|
||||
>
|
||||
<SubMenuContent
|
||||
open={open}
|
||||
top={y ? y - window.scrollY : 0}
|
||||
firstLevel={level === 0}
|
||||
browserScroll={browserScroll}
|
||||
className={classnames(menuClasses.subMenuContent, contentClassName)}
|
||||
rootStyles={getSubMenuItemStyles('subMenuContent')}
|
||||
style={{ ...styles }}
|
||||
>
|
||||
{childNodes.map((node, index) =>
|
||||
cloneElement(node, {
|
||||
...getItemProps({
|
||||
ref(node: HTMLButtonElement) {
|
||||
listItemsRef.current[index] = node
|
||||
},
|
||||
onClick(event: MouseEvent<HTMLAnchorElement>) {
|
||||
if (node.props.children && !Array.isArray(node.props.children)) {
|
||||
node.props.onClick?.(event)
|
||||
tree?.events.emit('click')
|
||||
}
|
||||
}
|
||||
}),
|
||||
level: level + 1
|
||||
})
|
||||
)}
|
||||
</SubMenuContent>
|
||||
</StyledSubMenuContentWrapper>
|
||||
)}
|
||||
</FloatingPortal>
|
||||
</HorizontalSubMenuContext.Provider>
|
||||
</StyledSubMenu>
|
||||
</FloatingNode>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLLIElement, SubMenuProps>(SubMenu)
|
||||
@@ -0,0 +1,54 @@
|
||||
// React Imports
|
||||
import { forwardRef } from 'react'
|
||||
import type { ForwardRefRenderFunction, HTMLAttributes } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, RootStylesType } from '../../types'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledHorizontalSubMenuContent from '../../styles/horizontal/StyledHorizontalSubMenuContent'
|
||||
|
||||
// Style Imports
|
||||
import styles from '../../styles/styles.module.css'
|
||||
|
||||
export type SubMenuContentProps = HTMLAttributes<HTMLDivElement> &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> & {
|
||||
open?: boolean
|
||||
browserScroll?: boolean
|
||||
firstLevel?: boolean
|
||||
top?: number
|
||||
}
|
||||
|
||||
const SubMenuContent: ForwardRefRenderFunction<HTMLDivElement, SubMenuContentProps> = (props, ref) => {
|
||||
// Props
|
||||
const { children, open, firstLevel, top, browserScroll, ...rest } = props
|
||||
|
||||
return (
|
||||
<StyledHorizontalSubMenuContent
|
||||
ref={ref}
|
||||
firstLevel={firstLevel}
|
||||
open={open}
|
||||
top={top}
|
||||
browserScroll={browserScroll}
|
||||
{...rest}
|
||||
>
|
||||
{/* If browserScroll is false render PerfectScrollbar */}
|
||||
{!browserScroll ? (
|
||||
<PerfectScrollbar
|
||||
options={{ wheelPropagation: false, suppressScrollX: true }}
|
||||
style={{ maxBlockSize: `calc((var(--vh, 1vh) * 100) - ${top}px)` }}
|
||||
>
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
</PerfectScrollbar>
|
||||
) : (
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
)}
|
||||
</StyledHorizontalSubMenuContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(SubMenuContent)
|
||||
@@ -0,0 +1,32 @@
|
||||
// Type Imports
|
||||
import type { BreakpointType, ChildrenType } from '../../types'
|
||||
import type { VerticalNavProps } from '../vertical-menu/VerticalNav'
|
||||
|
||||
// Component Imports
|
||||
import VerticalNav from '../../vertical-menu'
|
||||
|
||||
// Type
|
||||
type VerticalNavInHorizontalProps = ChildrenType & {
|
||||
className?: string
|
||||
breakpoint?: BreakpointType
|
||||
customBreakpoint?: string
|
||||
verticalNavProps?: Pick<VerticalNavProps, 'width' | 'backgroundColor' | 'backgroundImage' | 'customStyles'>
|
||||
}
|
||||
|
||||
const VerticalNavInHorizontal = (props: VerticalNavInHorizontalProps) => {
|
||||
// Props
|
||||
const { children, className, breakpoint, customBreakpoint, verticalNavProps } = props
|
||||
|
||||
return (
|
||||
<VerticalNav
|
||||
{...verticalNavProps}
|
||||
className={className}
|
||||
breakpoint={breakpoint}
|
||||
customBreakpoint={customBreakpoint}
|
||||
>
|
||||
{children}
|
||||
</VerticalNav>
|
||||
)
|
||||
}
|
||||
|
||||
export default VerticalNavInHorizontal
|
||||
@@ -0,0 +1,229 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { createContext, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ForwardRefRenderFunction, MenuHTMLAttributes, MutableRefObject, ReactElement, ReactNode } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { FloatingTree } from '@floating-ui/react'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type {
|
||||
ChildrenType,
|
||||
MenuItemStyles,
|
||||
RootStylesType,
|
||||
RenderExpandIconParams,
|
||||
RenderExpandedMenuItemIcon
|
||||
} from '../../types'
|
||||
|
||||
// Hook Imports
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledVerticalMenu from '../../styles/vertical/StyledVerticalMenu'
|
||||
|
||||
// Style Imports
|
||||
import styles from '../../styles/styles.module.css'
|
||||
|
||||
// Default Config Imports
|
||||
import { verticalSubMenuToggleDuration } from '../../defaultConfigs'
|
||||
|
||||
export type MenuSectionStyles = {
|
||||
root?: CSSObject
|
||||
label?: CSSObject
|
||||
prefix?: CSSObject
|
||||
suffix?: CSSObject
|
||||
icon?: CSSObject
|
||||
}
|
||||
|
||||
export type OpenSubmenu = {
|
||||
level: number
|
||||
label: ReactNode
|
||||
active: boolean
|
||||
id: string
|
||||
}
|
||||
|
||||
export type VerticalMenuContextProps = {
|
||||
browserScroll?: boolean
|
||||
triggerPopout?: 'hover' | 'click'
|
||||
transitionDuration?: number
|
||||
menuSectionStyles?: MenuSectionStyles
|
||||
menuItemStyles?: MenuItemStyles
|
||||
subMenuOpenBehavior?: 'accordion' | 'collapse'
|
||||
renderExpandIcon?: (params: RenderExpandIconParams) => ReactElement
|
||||
renderExpandedMenuItemIcon?: RenderExpandedMenuItemIcon
|
||||
collapsedMenuSectionLabel?: ReactNode
|
||||
popoutMenuOffset?: {
|
||||
mainAxis?: number | ((params: { level?: number }) => number)
|
||||
alignmentAxis?: number | ((params: { level?: number }) => number)
|
||||
}
|
||||
textTruncate?: boolean
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
openSubmenu?: OpenSubmenu[]
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
openSubmenusRef?: MutableRefObject<OpenSubmenu[]>
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
toggleOpenSubmenu?: (...submenus: { level: number; label: ReactNode; active?: boolean; id: string }[]) => void
|
||||
}
|
||||
|
||||
export type MenuProps = VerticalMenuContextProps &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> &
|
||||
MenuHTMLAttributes<HTMLMenuElement> & {
|
||||
popoutWhenCollapsed?: boolean
|
||||
}
|
||||
|
||||
export const VerticalMenuContext = createContext({} as VerticalMenuContextProps)
|
||||
|
||||
const Menu: ForwardRefRenderFunction<HTMLMenuElement, MenuProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
rootStyles,
|
||||
menuItemStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
menuSectionStyles,
|
||||
browserScroll = false,
|
||||
triggerPopout = 'hover',
|
||||
popoutWhenCollapsed = false,
|
||||
subMenuOpenBehavior = 'accordion', // accordion, collapse
|
||||
transitionDuration = verticalSubMenuToggleDuration,
|
||||
collapsedMenuSectionLabel = '-',
|
||||
popoutMenuOffset = { mainAxis: 0 },
|
||||
textTruncate = true,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [openSubmenu, setOpenSubmenu] = useState<OpenSubmenu[]>([])
|
||||
|
||||
// Refs
|
||||
const openSubmenusRef = useRef<OpenSubmenu[]>([])
|
||||
|
||||
// Hooks
|
||||
const pathname = usePathname()
|
||||
const { updateVerticalNavState } = useVerticalNav()
|
||||
|
||||
const toggleOpenSubmenu = useCallback(
|
||||
(...submenus: { level: number; label: ReactNode; active?: boolean; id: string }[]): void => {
|
||||
if (!submenus.length) return
|
||||
|
||||
const openSubmenuCopy = [...openSubmenu]
|
||||
|
||||
submenus.forEach(({ level, label, active = false, id }) => {
|
||||
const submenuIndex = openSubmenuCopy.findIndex(submenu => submenu.id === id)
|
||||
const submenuExists = submenuIndex >= 0
|
||||
const isAccordion = subMenuOpenBehavior === 'accordion'
|
||||
|
||||
const inactiveSubmenuIndex = openSubmenuCopy.findIndex(submenu => !submenu.active && submenu.level === 0)
|
||||
|
||||
// Delete submenu if it exists
|
||||
if (submenuExists) {
|
||||
openSubmenuCopy.splice(submenuIndex, 1)
|
||||
}
|
||||
|
||||
if (isAccordion) {
|
||||
// Add submenu if it doesn't exist
|
||||
if (!submenuExists) {
|
||||
if (inactiveSubmenuIndex >= 0 && !active && level === 0) {
|
||||
openSubmenuCopy.splice(inactiveSubmenuIndex, 1, { level, label, active, id })
|
||||
} else {
|
||||
openSubmenuCopy.push({ level, label, active, id })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add submenu if it doesn't exist
|
||||
if (!submenuExists) {
|
||||
openSubmenuCopy.push({ level, label, active, id })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setOpenSubmenu(openSubmenuCopy)
|
||||
},
|
||||
[openSubmenu, subMenuOpenBehavior]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setOpenSubmenu([...openSubmenusRef.current])
|
||||
openSubmenusRef.current = []
|
||||
}, [pathname])
|
||||
|
||||
// UseEffect, update verticalNav state to set initial values and update values on change
|
||||
useEffect(() => {
|
||||
updateVerticalNavState({
|
||||
isPopoutWhenCollapsed: popoutWhenCollapsed
|
||||
})
|
||||
}, [popoutWhenCollapsed, updateVerticalNavState])
|
||||
|
||||
const providerValue = useMemo(
|
||||
() => ({
|
||||
browserScroll,
|
||||
triggerPopout,
|
||||
transitionDuration,
|
||||
menuItemStyles,
|
||||
menuSectionStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
openSubmenu,
|
||||
openSubmenusRef,
|
||||
toggleOpenSubmenu,
|
||||
subMenuOpenBehavior,
|
||||
collapsedMenuSectionLabel,
|
||||
popoutMenuOffset,
|
||||
textTruncate
|
||||
}),
|
||||
[
|
||||
browserScroll,
|
||||
triggerPopout,
|
||||
transitionDuration,
|
||||
menuItemStyles,
|
||||
menuSectionStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
openSubmenu,
|
||||
openSubmenusRef,
|
||||
toggleOpenSubmenu,
|
||||
subMenuOpenBehavior,
|
||||
collapsedMenuSectionLabel,
|
||||
popoutMenuOffset,
|
||||
textTruncate
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<VerticalMenuContext.Provider value={providerValue}>
|
||||
<FloatingTree>
|
||||
<StyledVerticalMenu
|
||||
ref={ref}
|
||||
className={classnames(menuClasses.root, className)}
|
||||
rootStyles={rootStyles}
|
||||
{...rest}
|
||||
>
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
</StyledVerticalMenu>
|
||||
</FloatingTree>
|
||||
</VerticalMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(Menu)
|
||||
@@ -0,0 +1,113 @@
|
||||
// React Imports
|
||||
import { cloneElement, createElement, forwardRef } from 'react'
|
||||
import type { ForwardRefRenderFunction } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { css } from '@emotion/react'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, MenuButtonProps } from '../../types'
|
||||
|
||||
// Component Imports
|
||||
import { RouterLink } from '../RouterLink'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
type MenuButtonStylesProps = Partial<ChildrenType> & {
|
||||
level: number
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
isCollapsed?: boolean
|
||||
isPopoutWhenCollapsed?: boolean
|
||||
}
|
||||
|
||||
export const menuButtonStyles = (props: MenuButtonStylesProps) => {
|
||||
// Props
|
||||
const { level, disabled, children, isCollapsed, isPopoutWhenCollapsed } = props
|
||||
|
||||
return css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minBlockSize: '30px',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'pointer',
|
||||
paddingInlineEnd: '20px',
|
||||
paddingInlineStart: `${level === 0 ? 20 : (isPopoutWhenCollapsed && isCollapsed ? level : level + 1) * 20}px`,
|
||||
|
||||
'&:hover, &[aria-expanded="true"]': {
|
||||
backgroundColor: '#f3f3f3'
|
||||
},
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: 'none',
|
||||
backgroundColor: '#f3f3f3'
|
||||
},
|
||||
|
||||
...(disabled && {
|
||||
pointerEvents: 'none',
|
||||
cursor: 'default',
|
||||
color: '#adadad'
|
||||
}),
|
||||
|
||||
// All the active styles are applied to the button including menu items or submenu
|
||||
[`&.${menuClasses.active}`]: {
|
||||
...(!children && { color: 'white' }),
|
||||
backgroundColor: children ? '#f3f3f3' : '#765feb'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const MenuButton: ForwardRefRenderFunction<HTMLAnchorElement, MenuButtonProps> = (
|
||||
{ className, component, children, ...rest },
|
||||
ref
|
||||
) => {
|
||||
if (component) {
|
||||
// If component is a string, create a new element of that type
|
||||
if (typeof component === 'string') {
|
||||
return createElement(
|
||||
component,
|
||||
{
|
||||
className: classnames(className),
|
||||
...rest,
|
||||
ref
|
||||
},
|
||||
children
|
||||
)
|
||||
} else {
|
||||
// Otherwise, clone the element
|
||||
const { className: classNameProp, ...props } = component.props
|
||||
|
||||
return cloneElement(
|
||||
component,
|
||||
{
|
||||
className: classnames(className, classNameProp),
|
||||
...rest,
|
||||
...props,
|
||||
ref
|
||||
},
|
||||
children
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If there is no component but href is defined, render RouterLink
|
||||
if (rest.href) {
|
||||
return (
|
||||
<RouterLink ref={ref} className={className} href={rest.href} {...rest}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<a ref={ref} className={className} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default forwardRef(MenuButton)
|
||||
@@ -0,0 +1,204 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import type { AnchorHTMLAttributes, ForwardRefRenderFunction, ReactElement, ReactNode } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { useUpdateEffect } from 'react-use'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, MenuItemElement, MenuItemExactMatchUrlProps, RootStylesType } from '../../types'
|
||||
|
||||
// Component Imports
|
||||
import MenuButton from './MenuButton'
|
||||
|
||||
// Hook Imports
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
import useVerticalMenu from '../../hooks/useVerticalMenu'
|
||||
|
||||
// Util Imports
|
||||
import { renderMenuIcon } from '../../utils/menuUtils'
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledMenuLabel from '../../styles/StyledMenuLabel'
|
||||
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
|
||||
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
|
||||
import StyledVerticalMenuItem from '../../styles/vertical/StyledVerticalMenuItem'
|
||||
|
||||
export type MenuItemProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> &
|
||||
MenuItemExactMatchUrlProps & {
|
||||
icon?: ReactElement
|
||||
prefix?: ReactNode
|
||||
suffix?: ReactNode
|
||||
disabled?: boolean
|
||||
target?: string
|
||||
rel?: string
|
||||
component?: string | ReactElement
|
||||
onActiveChange?: (active: boolean) => void
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
level?: number
|
||||
}
|
||||
|
||||
const MenuItem: ForwardRefRenderFunction<HTMLLIElement, MenuItemProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
prefix,
|
||||
suffix,
|
||||
level = 0,
|
||||
disabled = false,
|
||||
exactMatch = true,
|
||||
activeUrl,
|
||||
component,
|
||||
onActiveChange,
|
||||
rootStyles,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const pathname = usePathname()
|
||||
const { menuItemStyles, renderExpandedMenuItemIcon, textTruncate } = useVerticalMenu()
|
||||
|
||||
const { isCollapsed, isHovered, isPopoutWhenCollapsed, toggleVerticalNav, isToggled, isBreakpointReached } =
|
||||
useVerticalNav()
|
||||
|
||||
// Get the styles for the specified element.
|
||||
const getMenuItemStyles = (element: MenuItemElement): CSSObject | undefined => {
|
||||
// If the menuItemStyles prop is provided, get the styles for the specified element.
|
||||
if (menuItemStyles) {
|
||||
// Define the parameters that are passed to the style functions.
|
||||
const params = { level, disabled, active, isSubmenu: false }
|
||||
|
||||
// Get the style function for the specified element.
|
||||
const styleFunction = menuItemStyles[element]
|
||||
|
||||
if (styleFunction) {
|
||||
// If the style function is a function, call it and return the result.
|
||||
// Otherwise, return the style function itself.
|
||||
return typeof styleFunction === 'function' ? styleFunction(params) : styleFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the click event.
|
||||
const handleClick = () => {
|
||||
if (isToggled) {
|
||||
toggleVerticalNav()
|
||||
}
|
||||
}
|
||||
|
||||
// Change active state when the url changes
|
||||
useEffect(() => {
|
||||
const href = rest.href || (component && typeof component !== 'string' && component.props.href)
|
||||
|
||||
if (href) {
|
||||
// Check if the current url matches any of the children urls
|
||||
if (exactMatch ? pathname === href : activeUrl && pathname.includes(activeUrl)) {
|
||||
setActive(true)
|
||||
} else {
|
||||
setActive(false)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname])
|
||||
|
||||
// Call the onActiveChange callback when the active state changes.
|
||||
useUpdateEffect(() => {
|
||||
onActiveChange?.(active)
|
||||
}, [active])
|
||||
|
||||
return (
|
||||
<StyledVerticalMenuItem
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
menuClasses.menuItemRoot,
|
||||
{ [menuClasses.disabled]: disabled },
|
||||
{ [menuClasses.active]: active },
|
||||
className
|
||||
)}
|
||||
level={level}
|
||||
isCollapsed={isCollapsed}
|
||||
isPopoutWhenCollapsed={isPopoutWhenCollapsed}
|
||||
disabled={disabled}
|
||||
buttonStyles={getMenuItemStyles('button')}
|
||||
menuItemStyles={getMenuItemStyles('root')}
|
||||
rootStyles={rootStyles}
|
||||
>
|
||||
<MenuButton
|
||||
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
|
||||
component={component}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
{...rest}
|
||||
onClick={e => {
|
||||
handleClick()
|
||||
rest.onClick && rest.onClick(e)
|
||||
}}
|
||||
>
|
||||
{/* Menu Item Icon */}
|
||||
{renderMenuIcon({
|
||||
icon,
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
renderExpandedMenuItemIcon,
|
||||
styles: getMenuItemStyles('icon'),
|
||||
isBreakpointReached
|
||||
})}
|
||||
|
||||
{/* Menu Item Prefix */}
|
||||
{prefix && (
|
||||
<StyledMenuPrefix
|
||||
isHovered={isHovered}
|
||||
isCollapsed={isCollapsed}
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.prefix}
|
||||
rootStyles={getMenuItemStyles('prefix')}
|
||||
>
|
||||
{prefix}
|
||||
</StyledMenuPrefix>
|
||||
)}
|
||||
|
||||
{/* Menu Item Label */}
|
||||
<StyledMenuLabel
|
||||
className={menuClasses.label}
|
||||
rootStyles={getMenuItemStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{children}
|
||||
</StyledMenuLabel>
|
||||
|
||||
{/* Menu Item Suffix */}
|
||||
{suffix && (
|
||||
<StyledMenuSuffix
|
||||
isHovered={isHovered}
|
||||
isCollapsed={isCollapsed}
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.suffix}
|
||||
rootStyles={getMenuItemStyles('suffix')}
|
||||
>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
</MenuButton>
|
||||
</StyledVerticalMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(MenuItem)
|
||||
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { forwardRef } from 'react'
|
||||
import type { ForwardRefRenderFunction, CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuSectionStyles } from './Menu'
|
||||
import type { ChildrenType, RootStylesType } from '../../types'
|
||||
|
||||
// Hook Imports
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
import useVerticalMenu from '../../hooks/useVerticalMenu'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledMenuIcon from '../../styles/StyledMenuIcon'
|
||||
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
|
||||
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
|
||||
import StyledMenuSectionLabel from '../../styles/StyledMenuSectionLabel'
|
||||
import StyledVerticalMenuSection from '../../styles/vertical/StyledVerticalMenuSection'
|
||||
|
||||
export type MenuSectionProps = Partial<ChildrenType> &
|
||||
RootStylesType & {
|
||||
label: ReactNode
|
||||
icon?: ReactElement
|
||||
prefix?: ReactNode
|
||||
suffix?: ReactNode
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
className?: string
|
||||
}
|
||||
|
||||
type MenuSectionElement = keyof MenuSectionStyles
|
||||
|
||||
const menuSectionWrapperStyles: CSSProperties = {
|
||||
display: 'inline-block',
|
||||
inlineSize: '100%',
|
||||
position: 'relative',
|
||||
listStyle: 'none',
|
||||
padding: 0,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
const menuSectionContentStyles: CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
inlineSize: '100%',
|
||||
position: 'relative',
|
||||
paddingBlock: '0.75rem',
|
||||
paddingInline: '1.25rem',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
const MenuSection: ForwardRefRenderFunction<HTMLLIElement, MenuSectionProps> = (props, ref) => {
|
||||
// Props
|
||||
const { children, icon, className, prefix, suffix, label, rootStyles, ...rest } = props
|
||||
|
||||
// Hooks
|
||||
const { isCollapsed, isHovered } = useVerticalNav()
|
||||
const { menuSectionStyles, collapsedMenuSectionLabel, textTruncate } = useVerticalMenu()
|
||||
|
||||
const getMenuSectionStyles = (element: MenuSectionElement): CSSObject | undefined => {
|
||||
// If the menuSectionStyles prop is provided, get the styles for the element from the prop
|
||||
if (menuSectionStyles) {
|
||||
return menuSectionStyles[element]
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// Menu Section
|
||||
<StyledVerticalMenuSection
|
||||
ref={ref}
|
||||
rootStyles={rootStyles}
|
||||
menuSectionStyles={getMenuSectionStyles('root')}
|
||||
className={classnames(menuClasses.menuSectionRoot, className)}
|
||||
>
|
||||
{/* Menu Section Content Wrapper */}
|
||||
<ul className={menuClasses.menuSectionWrapper} {...rest} style={menuSectionWrapperStyles}>
|
||||
{/* Menu Section Content */}
|
||||
<li className={menuClasses.menuSectionContent} style={menuSectionContentStyles}>
|
||||
{icon && (
|
||||
<StyledMenuIcon className={menuClasses.icon} rootStyles={getMenuSectionStyles('icon')}>
|
||||
{icon}
|
||||
</StyledMenuIcon>
|
||||
)}
|
||||
{prefix && (
|
||||
<StyledMenuPrefix
|
||||
isCollapsed={isCollapsed}
|
||||
className={menuClasses.prefix}
|
||||
rootStyles={getMenuSectionStyles('prefix')}
|
||||
>
|
||||
{prefix}
|
||||
</StyledMenuPrefix>
|
||||
)}
|
||||
{collapsedMenuSectionLabel && isCollapsed && !isHovered ? (
|
||||
<StyledMenuSectionLabel
|
||||
isCollapsed={isCollapsed}
|
||||
isHovered={isHovered}
|
||||
className={menuClasses.menuSectionLabel}
|
||||
rootStyles={getMenuSectionStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{collapsedMenuSectionLabel}
|
||||
</StyledMenuSectionLabel>
|
||||
) : (
|
||||
label && (
|
||||
<StyledMenuSectionLabel
|
||||
isCollapsed={isCollapsed}
|
||||
isHovered={isHovered}
|
||||
className={menuClasses.menuSectionLabel}
|
||||
rootStyles={getMenuSectionStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{label}
|
||||
</StyledMenuSectionLabel>
|
||||
)
|
||||
)}
|
||||
{suffix && (
|
||||
<StyledMenuSuffix
|
||||
isCollapsed={isCollapsed}
|
||||
className={menuClasses.suffix}
|
||||
rootStyles={getMenuSectionStyles('suffix')}
|
||||
>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
</li>
|
||||
{/* Render Child */}
|
||||
{children}
|
||||
</ul>
|
||||
</StyledVerticalMenuSection>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLLIElement, MenuSectionProps>(MenuSection)
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import type { HTMLAttributes, ReactElement } from 'react'
|
||||
|
||||
// Hook Imports
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
|
||||
// Icon Imports
|
||||
import CloseIcon from '../../svg/Close'
|
||||
import RadioCircleIcon from '../../svg/RadioCircle'
|
||||
import RadioCircleMarkedIcon from '../../svg/RadioCircleMarked'
|
||||
|
||||
type NavCollapseIconsProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
closeIcon?: ReactElement
|
||||
lockedIcon?: ReactElement
|
||||
unlockedIcon?: ReactElement
|
||||
onClick?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const NavCollapseIcons = (props: NavCollapseIconsProps) => {
|
||||
// Props
|
||||
const { closeIcon, lockedIcon, unlockedIcon, onClick, onClose, ...rest } = props
|
||||
|
||||
// Hooks
|
||||
const { isCollapsed, collapseVerticalNav, isBreakpointReached, toggleVerticalNav } = useVerticalNav()
|
||||
|
||||
// Handle Lock / Unlock Icon Buttons click
|
||||
const handleClick = (action: 'lock' | 'unlock') => {
|
||||
// Setup the verticalNav to be locked or unlocked
|
||||
const collapse = action === 'lock' ? false : true
|
||||
|
||||
// Tell the verticalNav to lock or unlock
|
||||
collapseVerticalNav(collapse)
|
||||
|
||||
// Call onClick function if passed
|
||||
onClick && onClick()
|
||||
}
|
||||
|
||||
// Handle Close button click
|
||||
const handleClose = () => {
|
||||
// Close verticalNav using toggle verticalNav function
|
||||
toggleVerticalNav(false)
|
||||
|
||||
// Call onClose function if passed
|
||||
onClose && onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isBreakpointReached ? (
|
||||
<span role='button' tabIndex={0} style={{ display: 'flex', cursor: 'pointer' }} onClick={handleClose} {...rest}>
|
||||
{closeIcon ?? <CloseIcon />}
|
||||
</span>
|
||||
) : isCollapsed ? (
|
||||
<span
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
style={{ display: 'flex', cursor: 'pointer' }}
|
||||
onClick={() => handleClick('lock')}
|
||||
{...rest}
|
||||
>
|
||||
{unlockedIcon ?? <RadioCircleIcon />}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
style={{ display: 'flex', cursor: 'pointer' }}
|
||||
onClick={() => handleClick('unlock')}
|
||||
{...rest}
|
||||
>
|
||||
{lockedIcon ?? <RadioCircleMarkedIcon />}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavCollapseIcons
|
||||
@@ -0,0 +1,50 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType } from '../../types'
|
||||
import type { VerticalNavContextProps } from '../../contexts/verticalNavContext'
|
||||
|
||||
// Hook Imports
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
|
||||
// Util Imports
|
||||
import { verticalNavClasses } from '../../utils/menuClasses'
|
||||
|
||||
type StyledNavHeaderProps = {
|
||||
isHovered?: VerticalNavContextProps['isHovered']
|
||||
isCollapsed?: VerticalNavContextProps['isCollapsed']
|
||||
collapsedWidth?: VerticalNavContextProps['collapsedWidth']
|
||||
transitionDuration?: VerticalNavContextProps['transitionDuration']
|
||||
}
|
||||
|
||||
const StyledNavHeader = styled.div<StyledNavHeaderProps>`
|
||||
padding: 15px;
|
||||
padding-inline-start: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: ${({ transitionDuration }) => `padding-inline ${transitionDuration}ms ease-in-out`};
|
||||
|
||||
${({ isHovered, isCollapsed, collapsedWidth }) =>
|
||||
isCollapsed && !isHovered && `padding-inline: calc((${collapsedWidth}px - 1px - 22px) / 2);`}
|
||||
`
|
||||
|
||||
const NavHeader = ({ children }: ChildrenType) => {
|
||||
// Hooks
|
||||
const { isHovered, isCollapsed, collapsedWidth, transitionDuration } = useVerticalNav()
|
||||
|
||||
return (
|
||||
<StyledNavHeader
|
||||
className={verticalNavClasses.header}
|
||||
isHovered={isHovered}
|
||||
isCollapsed={isCollapsed}
|
||||
collapsedWidth={collapsedWidth}
|
||||
transitionDuration={transitionDuration}
|
||||
>
|
||||
{children}
|
||||
</StyledNavHeader>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavHeader
|
||||
@@ -0,0 +1,466 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { Children, cloneElement, forwardRef, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
ForwardRefRenderFunction,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
|
||||
// Next Imports
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import styled from '@emotion/styled'
|
||||
import {
|
||||
useFloating,
|
||||
autoUpdate,
|
||||
offset,
|
||||
flip,
|
||||
shift,
|
||||
useHover,
|
||||
useRole,
|
||||
useInteractions,
|
||||
useClick,
|
||||
safePolygon,
|
||||
useDismiss,
|
||||
hide,
|
||||
useFloatingTree,
|
||||
FloatingPortal
|
||||
} from '@floating-ui/react'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { OpenSubmenu } from './Menu'
|
||||
import type { MenuItemProps } from './MenuItem'
|
||||
import type { ChildrenType, RootStylesType, SubMenuItemElement } from '../../types'
|
||||
|
||||
// Component Imports
|
||||
import SubMenuContent from './SubMenuContent'
|
||||
import MenuButton, { menuButtonStyles } from './MenuButton'
|
||||
|
||||
// Icon Imports
|
||||
import ChevronRight from '../../svg/ChevronRight'
|
||||
|
||||
// Hook Imports
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
import useVerticalMenu from '../../hooks/useVerticalMenu'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
import { confirmUrlInChildren, renderMenuIcon } from '../../utils/menuUtils'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledMenuLabel from '../../styles/StyledMenuLabel'
|
||||
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
|
||||
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
|
||||
import StyledVerticalNavExpandIcon, {
|
||||
StyledVerticalNavExpandIconWrapper
|
||||
} from '../../styles/vertical/StyledVerticalNavExpandIcon'
|
||||
|
||||
export type SubMenuProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> & {
|
||||
label: ReactNode
|
||||
icon?: ReactElement
|
||||
prefix?: ReactNode
|
||||
suffix?: ReactNode
|
||||
defaultOpen?: boolean
|
||||
disabled?: boolean
|
||||
component?: string | ReactElement
|
||||
contentClassName?: string
|
||||
onOpenChange?: (open: boolean) => void
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
level?: number
|
||||
}
|
||||
|
||||
type StyledSubMenuProps = Pick<SubMenuProps, 'rootStyles' | 'disabled'> & {
|
||||
level: number
|
||||
active?: boolean
|
||||
menuItemStyles?: CSSObject
|
||||
isPopoutWhenCollapsed?: boolean
|
||||
isCollapsed?: boolean
|
||||
buttonStyles?: CSSObject
|
||||
}
|
||||
|
||||
const StyledSubMenu = styled.li<StyledSubMenuProps>`
|
||||
position: relative;
|
||||
inline-size: 100%;
|
||||
margin-block-start: 4px;
|
||||
|
||||
&.${menuClasses.open} > .${menuClasses.button} {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
${({ menuItemStyles }) => menuItemStyles};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
|
||||
> .${menuClasses.button} {
|
||||
${({ level, disabled, active, children, isCollapsed, isPopoutWhenCollapsed }) =>
|
||||
menuButtonStyles({
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
children,
|
||||
isCollapsed,
|
||||
isPopoutWhenCollapsed
|
||||
})};
|
||||
${({ buttonStyles }) => buttonStyles};
|
||||
}
|
||||
`
|
||||
|
||||
const SubMenu: ForwardRefRenderFunction<HTMLLIElement, SubMenuProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
label,
|
||||
icon,
|
||||
title,
|
||||
prefix,
|
||||
suffix,
|
||||
defaultOpen,
|
||||
level = 0,
|
||||
disabled = false,
|
||||
rootStyles,
|
||||
component,
|
||||
onOpenChange,
|
||||
onClick,
|
||||
onKeyUp,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [openWhenCollapsed, setOpenWhenCollapsed] = useState<boolean>(false)
|
||||
const [active, setActive] = useState<boolean>(false)
|
||||
|
||||
// Refs
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Hooks
|
||||
const id = useId()
|
||||
const pathname = usePathname()
|
||||
const { isCollapsed, isPopoutWhenCollapsed, isHovered, isBreakpointReached } = useVerticalNav()
|
||||
const tree = useFloatingTree()
|
||||
|
||||
const {
|
||||
browserScroll,
|
||||
triggerPopout,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
menuItemStyles,
|
||||
openSubmenu,
|
||||
toggleOpenSubmenu,
|
||||
transitionDuration,
|
||||
openSubmenusRef,
|
||||
popoutMenuOffset,
|
||||
textTruncate
|
||||
} = useVerticalMenu()
|
||||
|
||||
// Vars
|
||||
// Filter out falsy values from children
|
||||
const childNodes = Children.toArray(children).filter(Boolean) as [ReactElement<SubMenuProps | MenuItemProps>]
|
||||
|
||||
const mainAxisOffset =
|
||||
popoutMenuOffset &&
|
||||
popoutMenuOffset.mainAxis &&
|
||||
(typeof popoutMenuOffset.mainAxis === 'function' ? popoutMenuOffset.mainAxis({ level }) : popoutMenuOffset.mainAxis)
|
||||
|
||||
const alignmentAxisOffset =
|
||||
popoutMenuOffset &&
|
||||
popoutMenuOffset.alignmentAxis &&
|
||||
(typeof popoutMenuOffset.alignmentAxis === 'function'
|
||||
? popoutMenuOffset.alignmentAxis({ level })
|
||||
: popoutMenuOffset.alignmentAxis)
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
strategy: 'fixed',
|
||||
open: openWhenCollapsed,
|
||||
onOpenChange: setOpenWhenCollapsed,
|
||||
placement: 'right-start',
|
||||
middleware: [
|
||||
offset({
|
||||
mainAxis: mainAxisOffset,
|
||||
alignmentAxis: alignmentAxisOffset
|
||||
}),
|
||||
flip({ crossAxis: false }),
|
||||
shift(),
|
||||
hide()
|
||||
],
|
||||
whileElementsMounted: autoUpdate
|
||||
})
|
||||
|
||||
const hover = useHover(context, {
|
||||
handleClose: safePolygon({
|
||||
blockPointerEvents: true
|
||||
}), // safePolygon function allows us to reach to submenu
|
||||
restMs: 25, // Only opens submenu when cursor rests for 25ms on a menu
|
||||
enabled: triggerPopout === 'hover', // Only enable hover effect when triggerPopout option is set to 'hover'
|
||||
delay: { open: 75 } // Delay opening submenu by 75ms
|
||||
})
|
||||
|
||||
const click = useClick(context, {
|
||||
enabled: triggerPopout === 'click' // Only enable click effect when triggerPopout option is set to 'click'
|
||||
})
|
||||
|
||||
const dismiss = useDismiss(context)
|
||||
const role = useRole(context, { role: 'menu' })
|
||||
|
||||
// Merge all the interactions into prop getters
|
||||
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([hover, click, dismiss, role])
|
||||
|
||||
const isSubMenuOpen = openSubmenu?.some((item: OpenSubmenu) => item.id === id) ?? false
|
||||
|
||||
const handleSlideToggle = (): void => {
|
||||
if (level === 0 && isCollapsed && !isHovered) {
|
||||
return
|
||||
}
|
||||
|
||||
toggleOpenSubmenu?.({ level, label, active, id })
|
||||
onOpenChange?.(!isSubMenuOpen)
|
||||
if (openSubmenusRef?.current && openSubmenusRef?.current.length > 0) openSubmenusRef.current = []
|
||||
}
|
||||
|
||||
const handleOnClick = (event: MouseEvent<HTMLAnchorElement, globalThis.MouseEvent>) => {
|
||||
onClick?.(event)
|
||||
handleSlideToggle()
|
||||
}
|
||||
|
||||
const handleOnKeyUp = (event: KeyboardEvent<HTMLAnchorElement>) => {
|
||||
onKeyUp?.(event)
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
handleSlideToggle()
|
||||
}
|
||||
}
|
||||
|
||||
const getSubMenuItemStyles = (element: SubMenuItemElement): CSSObject | undefined => {
|
||||
// If the menuItemStyles prop is provided, get the styles for the specified element.
|
||||
if (menuItemStyles) {
|
||||
// Define the parameters that are passed to the style functions.
|
||||
const params = {
|
||||
level,
|
||||
disabled,
|
||||
active,
|
||||
isSubmenu: true,
|
||||
open: isSubMenuOpen
|
||||
}
|
||||
|
||||
// Get the style function for the specified element.
|
||||
const styleFunction = menuItemStyles[element]
|
||||
|
||||
if (styleFunction) {
|
||||
// If the style function is a function, call it and return the result.
|
||||
// Otherwise, return the style function itself.
|
||||
return typeof styleFunction === 'function' ? styleFunction(params) : styleFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event emitter allows you to communicate across tree components.
|
||||
// This effect closes all menus when an item gets clicked anywhere in the tree.
|
||||
useEffect(() => {
|
||||
const handleTreeClick = () => {
|
||||
setOpenWhenCollapsed(false)
|
||||
}
|
||||
|
||||
tree?.events.on('click', handleTreeClick)
|
||||
|
||||
return () => {
|
||||
tree?.events.off('click', handleTreeClick)
|
||||
}
|
||||
}, [tree])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isCollapsed && level === 0) {
|
||||
setOpenWhenCollapsed(false)
|
||||
}
|
||||
}, [isCollapsed, level, active])
|
||||
|
||||
useEffect(() => {
|
||||
if (confirmUrlInChildren(children, pathname)) {
|
||||
openSubmenusRef?.current.push({ level, label, active: true, id })
|
||||
} else {
|
||||
if (defaultOpen) {
|
||||
openSubmenusRef?.current.push({ level, label, active: false, id })
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Change active state when the url changes
|
||||
useEffect(() => {
|
||||
// Check if the current url matches any of the children urls
|
||||
if (confirmUrlInChildren(children, pathname)) {
|
||||
setActive(true)
|
||||
|
||||
if (openSubmenusRef?.current.findIndex(submenu => submenu.id === id) === -1) {
|
||||
openSubmenusRef?.current.push({ level, label, active: true, id })
|
||||
}
|
||||
} else {
|
||||
setActive(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname])
|
||||
|
||||
/* useEffect(() => {
|
||||
console.log(openSubmenu)
|
||||
}, [openSubmenu]) */
|
||||
|
||||
const submenuContent = (
|
||||
<SubMenuContent
|
||||
ref={isCollapsed && level === 0 && isPopoutWhenCollapsed ? refs.setFloating : contentRef}
|
||||
{...(isCollapsed && level === 0 && isPopoutWhenCollapsed && getFloatingProps())}
|
||||
browserScroll={browserScroll}
|
||||
openWhenCollapsed={openWhenCollapsed}
|
||||
isPopoutWhenCollapsed={isPopoutWhenCollapsed}
|
||||
transitionDuration={transitionDuration}
|
||||
open={isSubMenuOpen}
|
||||
level={level}
|
||||
isCollapsed={isCollapsed}
|
||||
isHovered={isHovered}
|
||||
className={classnames(menuClasses.subMenuContent, contentClassName)}
|
||||
rootStyles={{
|
||||
...(isCollapsed && level === 0 && isPopoutWhenCollapsed && floatingStyles),
|
||||
...getSubMenuItemStyles('subMenuContent')
|
||||
}}
|
||||
>
|
||||
{childNodes.map(node =>
|
||||
cloneElement(node, {
|
||||
...getItemProps({
|
||||
onClick(event: MouseEvent<HTMLAnchorElement>) {
|
||||
if (node.props.children && !Array.isArray(node.props.children)) {
|
||||
node.props.onClick?.(event)
|
||||
tree?.events.emit('click')
|
||||
}
|
||||
}
|
||||
}),
|
||||
level: level + 1
|
||||
})
|
||||
)}
|
||||
</SubMenuContent>
|
||||
)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
/* Sub Menu */
|
||||
<StyledSubMenu
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
menuClasses.subMenuRoot,
|
||||
{ [menuClasses.active]: active },
|
||||
{ [menuClasses.disabled]: disabled },
|
||||
{ [menuClasses.open]: isSubMenuOpen },
|
||||
className
|
||||
)}
|
||||
menuItemStyles={getSubMenuItemStyles('root')}
|
||||
level={level}
|
||||
isPopoutWhenCollapsed={isPopoutWhenCollapsed}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
isCollapsed={isCollapsed}
|
||||
buttonStyles={getSubMenuItemStyles('button')}
|
||||
rootStyles={rootStyles}
|
||||
>
|
||||
{/* Menu Item */}
|
||||
<MenuButton
|
||||
ref={isCollapsed && level === 0 && isPopoutWhenCollapsed && !disabled ? refs.setReference : null}
|
||||
onClick={handleOnClick}
|
||||
{...(isCollapsed && level === 0 && isPopoutWhenCollapsed && !disabled && getReferenceProps())}
|
||||
onKeyUp={handleOnKeyUp}
|
||||
title={title}
|
||||
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
|
||||
component={component}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
{...rest}
|
||||
>
|
||||
{/* Sub Menu Icon */}
|
||||
{renderMenuIcon({
|
||||
icon,
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
renderExpandedMenuItemIcon,
|
||||
styles: getSubMenuItemStyles('icon'),
|
||||
isBreakpointReached
|
||||
})}
|
||||
|
||||
{/* Sub Menu Prefix */}
|
||||
{prefix && (
|
||||
<StyledMenuPrefix
|
||||
isHovered={isHovered}
|
||||
isCollapsed={isCollapsed}
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.prefix}
|
||||
rootStyles={getSubMenuItemStyles('prefix')}
|
||||
>
|
||||
{prefix}
|
||||
</StyledMenuPrefix>
|
||||
)}
|
||||
|
||||
{/* Sub Menu Label */}
|
||||
<StyledMenuLabel
|
||||
className={menuClasses.label}
|
||||
rootStyles={getSubMenuItemStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{label}
|
||||
</StyledMenuLabel>
|
||||
|
||||
{/* Sub Menu Suffix */}
|
||||
{suffix && (
|
||||
<StyledMenuSuffix
|
||||
isHovered={isHovered}
|
||||
isCollapsed={isCollapsed}
|
||||
firstLevel={level === 0}
|
||||
className={menuClasses.suffix}
|
||||
rootStyles={getSubMenuItemStyles('suffix')}
|
||||
>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
|
||||
{/* Sub Menu Toggle Icon Wrapper */}
|
||||
{isCollapsed && !isHovered && level === 0 ? null : (
|
||||
<StyledVerticalNavExpandIconWrapper
|
||||
className={menuClasses.subMenuExpandIcon}
|
||||
rootStyles={getSubMenuItemStyles('subMenuExpandIcon')}
|
||||
>
|
||||
{renderExpandIcon ? (
|
||||
renderExpandIcon({
|
||||
level,
|
||||
disabled,
|
||||
active,
|
||||
open: isSubMenuOpen
|
||||
})
|
||||
) : (
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
/* Expanded Arrow Icon */
|
||||
<StyledVerticalNavExpandIcon open={isSubMenuOpen} transitionDuration={transitionDuration}>
|
||||
<ChevronRight fontSize='1rem' />
|
||||
</StyledVerticalNavExpandIcon>
|
||||
)}
|
||||
</StyledVerticalNavExpandIconWrapper>
|
||||
)}
|
||||
</MenuButton>
|
||||
|
||||
{/* Sub Menu Content */}
|
||||
{isCollapsed && level === 0 && isPopoutWhenCollapsed ? (
|
||||
<FloatingPortal>{openWhenCollapsed && submenuContent}</FloatingPortal>
|
||||
) : (
|
||||
submenuContent
|
||||
)}
|
||||
</StyledSubMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLLIElement, SubMenuProps>(SubMenu)
|
||||
@@ -0,0 +1,125 @@
|
||||
// React Imports
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import type { ForwardRefRenderFunction, HTMLAttributes, MutableRefObject } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { VerticalMenuContextProps } from './Menu'
|
||||
import type { ChildrenType, RootStylesType } from '../../types'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledSubMenuContent from '../../styles/StyledSubMenuContent'
|
||||
|
||||
// Style Imports
|
||||
import styles from '../../styles/styles.module.css'
|
||||
|
||||
export type SubMenuContentProps = HTMLAttributes<HTMLDivElement> &
|
||||
RootStylesType &
|
||||
Partial<ChildrenType> & {
|
||||
open?: boolean
|
||||
openWhenCollapsed?: boolean
|
||||
openWhenHovered?: boolean
|
||||
transitionDuration?: VerticalMenuContextProps['transitionDuration']
|
||||
isPopoutWhenCollapsed?: boolean
|
||||
level?: number
|
||||
isCollapsed?: boolean
|
||||
isHovered?: boolean
|
||||
browserScroll?: boolean
|
||||
}
|
||||
|
||||
const SubMenuContent: ForwardRefRenderFunction<HTMLDivElement, SubMenuContentProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
open,
|
||||
level,
|
||||
isCollapsed,
|
||||
isHovered,
|
||||
transitionDuration,
|
||||
isPopoutWhenCollapsed,
|
||||
openWhenCollapsed,
|
||||
browserScroll,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Refs
|
||||
const SubMenuContentRef = ref as MutableRefObject<HTMLDivElement>
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
if (open || (open && isHovered)) {
|
||||
const target = SubMenuContentRef?.current
|
||||
|
||||
if (target) {
|
||||
target.style.display = 'block'
|
||||
target.style.overflow = 'hidden'
|
||||
target.style.blockSize = 'auto'
|
||||
const height = target.offsetHeight
|
||||
|
||||
target.style.blockSize = '0px'
|
||||
target.offsetHeight
|
||||
|
||||
target.style.blockSize = `${height}px`
|
||||
|
||||
setTimeout(() => {
|
||||
target.style.overflow = 'auto'
|
||||
target.style.blockSize = 'auto'
|
||||
}, transitionDuration)
|
||||
}
|
||||
} else {
|
||||
const target = SubMenuContentRef?.current
|
||||
|
||||
if (target) {
|
||||
target.style.overflow = 'hidden'
|
||||
target.style.blockSize = `${target.offsetHeight}px`
|
||||
target.offsetHeight
|
||||
target.style.blockSize = '0px'
|
||||
|
||||
setTimeout(() => {
|
||||
target.style.overflow = 'auto'
|
||||
target.style.display = 'none'
|
||||
}, transitionDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, mounted, SubMenuContentRef])
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [isHovered])
|
||||
|
||||
return (
|
||||
<StyledSubMenuContent
|
||||
ref={ref}
|
||||
level={level}
|
||||
isCollapsed={isCollapsed}
|
||||
isHovered={isHovered}
|
||||
open={open}
|
||||
openWhenCollapsed={openWhenCollapsed}
|
||||
isPopoutWhenCollapsed={isPopoutWhenCollapsed}
|
||||
transitionDuration={transitionDuration}
|
||||
browserScroll={browserScroll}
|
||||
{...rest}
|
||||
>
|
||||
{/* If browserScroll is false render PerfectScrollbar */}
|
||||
{!browserScroll && level === 0 && isPopoutWhenCollapsed && isCollapsed ? (
|
||||
<PerfectScrollbar
|
||||
options={{ wheelPropagation: false, suppressScrollX: true }}
|
||||
style={{ maxBlockSize: `calc((var(--vh, 1vh) * 100))` }}
|
||||
>
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
</PerfectScrollbar>
|
||||
) : (
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
)}
|
||||
</StyledSubMenuContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(SubMenuContent)
|
||||
@@ -0,0 +1,245 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { BreakpointType } from '../../types'
|
||||
|
||||
// Context Imports
|
||||
import type { VerticalNavState } from '../../contexts/verticalNavContext'
|
||||
|
||||
// Hook Imports
|
||||
import useMediaQuery from '../../hooks/useMediaQuery'
|
||||
import useVerticalNav from '../../hooks/useVerticalNav'
|
||||
|
||||
// Util Imports
|
||||
import { verticalNavClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledBackdrop from '../../styles/StyledBackdrop'
|
||||
import StyledVerticalNav from '../../styles/vertical/StyledVerticalNav'
|
||||
import StyledVerticalNavContainer from '../../styles/vertical/StyledVerticalNavContainer'
|
||||
import StyledVerticalNavBgColorContainer from '../../styles/vertical/StyledVerticalNavBgColorContainer'
|
||||
|
||||
// Style Imports
|
||||
import styles from '../../styles/vertical/verticalNavBgImage.module.css'
|
||||
|
||||
// Default Config Imports
|
||||
import { defaultBreakpoints, verticalNavToggleDuration } from '../../defaultConfigs'
|
||||
|
||||
export type VerticalNavProps = HTMLAttributes<HTMLHtmlElement> & {
|
||||
width?: VerticalNavState['width']
|
||||
collapsedWidth?: VerticalNavState['collapsedWidth']
|
||||
defaultCollapsed?: boolean
|
||||
backgroundColor?: string
|
||||
backgroundImage?: string
|
||||
breakpoint?: BreakpointType
|
||||
customBreakpoint?: string
|
||||
breakpoints?: Partial<typeof defaultBreakpoints>
|
||||
transitionDuration?: VerticalNavState['transitionDuration']
|
||||
backdropColor?: string
|
||||
scrollWithContent?: boolean
|
||||
customStyles?: CSSObject
|
||||
}
|
||||
|
||||
const VerticalNav = (props: VerticalNavProps) => {
|
||||
// Props
|
||||
const {
|
||||
width = 260,
|
||||
collapsedWidth = 80,
|
||||
defaultCollapsed = false,
|
||||
backgroundColor = 'white',
|
||||
backgroundImage,
|
||||
breakpoint = 'lg',
|
||||
customBreakpoint,
|
||||
breakpoints,
|
||||
transitionDuration = verticalNavToggleDuration,
|
||||
backdropColor,
|
||||
scrollWithContent = false,
|
||||
className,
|
||||
customStyles,
|
||||
children,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// Vars
|
||||
const mergedBreakpoints = { ...defaultBreakpoints, ...breakpoints }
|
||||
|
||||
// Refs
|
||||
const verticalNavCollapsedRef = useRef(false)
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
updateVerticalNavState,
|
||||
isCollapsed: isCollapsedContext,
|
||||
width: widthContext,
|
||||
isBreakpointReached: isBreakpointReachedContext,
|
||||
isToggled: isToggledContext,
|
||||
isHovered: isHoveredContext,
|
||||
collapsing: collapsingContext,
|
||||
expanding: expandingContext,
|
||||
isScrollWithContent: isScrollWithContentContext,
|
||||
transitionDuration: transitionDurationContext,
|
||||
isPopoutWhenCollapsed: isPopoutWhenCollapsedContext
|
||||
} = useVerticalNav()
|
||||
|
||||
// Find the breakpoint from which screen size responsive behavior should enable and if its reached or not
|
||||
const breakpointReached = useMediaQuery(customBreakpoint ?? (breakpoint ? mergedBreakpoints[breakpoint] : breakpoint))
|
||||
|
||||
// UseEffect, update verticalNav state to set initial values and update values on change
|
||||
useEffect(() => {
|
||||
updateVerticalNavState({
|
||||
width,
|
||||
collapsedWidth,
|
||||
transitionDuration,
|
||||
isScrollWithContent: scrollWithContent,
|
||||
isBreakpointReached: breakpointReached
|
||||
})
|
||||
|
||||
if (!breakpointReached) {
|
||||
updateVerticalNavState({ isToggled: false })
|
||||
verticalNavCollapsedRef.current && updateVerticalNavState({ isCollapsed: true })
|
||||
} else {
|
||||
if (isCollapsedContext && !verticalNavCollapsedRef.current) {
|
||||
verticalNavCollapsedRef.current = true
|
||||
}
|
||||
|
||||
isCollapsedContext && updateVerticalNavState({ isCollapsed: false })
|
||||
isHoveredContext && updateVerticalNavState({ isHovered: false })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [width, collapsedWidth, scrollWithContent, breakpointReached, updateVerticalNavState])
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultCollapsed) {
|
||||
updateVerticalNavState({
|
||||
isCollapsed: defaultCollapsed,
|
||||
isToggled: false
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
updateVerticalNavState({
|
||||
expanding: false,
|
||||
collapsing: false
|
||||
})
|
||||
}, transitionDuration)
|
||||
|
||||
if (!isCollapsedContext && !breakpointReached && verticalNavCollapsedRef.current) {
|
||||
verticalNavCollapsedRef.current = false
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCollapsedContext])
|
||||
|
||||
// Handle Backdrop(Content Overlay) Click
|
||||
const handleBackdropClick = () => {
|
||||
// Close the verticalNav
|
||||
updateVerticalNavState({ isToggled: false })
|
||||
}
|
||||
|
||||
// Handle VerticalNav Hover Event
|
||||
const handleVerticalNavHover = () => {
|
||||
/* If verticalNav is collapsed then only hover class should be added to verticalNav
|
||||
and hover functionality should work (expand verticalNav width) */
|
||||
if (isCollapsedContext && !isHoveredContext) {
|
||||
updateVerticalNavState({ isHovered: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Handle VerticalNav Hover Out Event
|
||||
const handleVerticalNavHoverOut = () => {
|
||||
// If verticalNav is collapsed then only remove hover class should contract verticalNav width
|
||||
if (isCollapsedContext && isHoveredContext) {
|
||||
updateVerticalNavState({ isHovered: false })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledVerticalNav
|
||||
width={defaultCollapsed && !widthContext ? collapsedWidth : width}
|
||||
isBreakpointReached={isBreakpointReachedContext}
|
||||
collapsedWidth={collapsedWidth}
|
||||
collapsing={collapsingContext}
|
||||
expanding={expandingContext}
|
||||
customStyles={customStyles}
|
||||
scrollWithContent={isScrollWithContentContext}
|
||||
transitionDuration={transitionDurationContext}
|
||||
className={classnames(
|
||||
verticalNavClasses.root,
|
||||
{
|
||||
[verticalNavClasses.collapsed]: isCollapsedContext,
|
||||
[verticalNavClasses.toggled]: isToggledContext,
|
||||
[verticalNavClasses.hovered]: isHoveredContext,
|
||||
[verticalNavClasses.breakpointReached]: isBreakpointReachedContext,
|
||||
[verticalNavClasses.scrollWithContent]: isScrollWithContentContext,
|
||||
[verticalNavClasses.collapsing]: collapsingContext,
|
||||
[verticalNavClasses.expanding]: expandingContext
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{/* VerticalNav Container for hover effect when verticalNav is collapsed */}
|
||||
<StyledVerticalNavContainer
|
||||
width={widthContext}
|
||||
className={verticalNavClasses.container}
|
||||
transitionDuration={transitionDurationContext}
|
||||
{
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
/* Toggle verticalNav on hover only when isPopoutWhenCollapsedContext(default false) is false */
|
||||
...(!isPopoutWhenCollapsedContext &&
|
||||
isCollapsedContext &&
|
||||
!breakpointReached && {
|
||||
onMouseEnter: handleVerticalNavHover,
|
||||
onMouseLeave: handleVerticalNavHoverOut
|
||||
})
|
||||
}
|
||||
>
|
||||
{/* VerticalNav Container to apply styling like background */}
|
||||
<StyledVerticalNavBgColorContainer
|
||||
className={verticalNavClasses.bgColorContainer}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{children}
|
||||
</StyledVerticalNavBgColorContainer>
|
||||
|
||||
{/* Display verticalNav background image if provided by user through props */}
|
||||
{backgroundImage && (
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
/* VerticalNav Background Image */
|
||||
<img
|
||||
className={classnames(verticalNavClasses.image, styles.root)}
|
||||
src={backgroundImage}
|
||||
alt='verticalNav background'
|
||||
/>
|
||||
)}
|
||||
</StyledVerticalNavContainer>
|
||||
|
||||
{/* When verticalNav is toggled on smaller screen, show/hide verticalNav backdrop */}
|
||||
{isToggledContext && breakpointReached && (
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
/* VerticalNav Backdrop */
|
||||
<StyledBackdrop
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label='backdrop'
|
||||
onClick={handleBackdropClick}
|
||||
onKeyPress={handleBackdropClick}
|
||||
className={verticalNavClasses.backdrop}
|
||||
backdropColor={backdropColor}
|
||||
/>
|
||||
)}
|
||||
</StyledVerticalNav>
|
||||
)
|
||||
}
|
||||
|
||||
export default VerticalNav
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { createContext, useMemo, useState } from 'react'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType } from '../types'
|
||||
|
||||
export type HorizontalNavContextProps = {
|
||||
isBreakpointReached?: boolean
|
||||
updateIsBreakpointReached: (isBreakpointReached: boolean) => void
|
||||
}
|
||||
|
||||
const HorizontalNavContext = createContext({} as HorizontalNavContextProps)
|
||||
|
||||
export const HorizontalNavProvider = ({ children }: ChildrenType) => {
|
||||
// States
|
||||
const [isBreakpointReached, setIsBreakpointReached] = useState(false)
|
||||
|
||||
// update isBreakpointReached value
|
||||
const updateIsBreakpointReached = (isBreakpointReached: boolean) => {
|
||||
setIsBreakpointReached(isBreakpointReached)
|
||||
}
|
||||
|
||||
// Hooks
|
||||
const HorizontalNavProviderValue = useMemo(
|
||||
() => ({
|
||||
isBreakpointReached,
|
||||
updateIsBreakpointReached
|
||||
}),
|
||||
[isBreakpointReached]
|
||||
)
|
||||
|
||||
return <HorizontalNavContext.Provider value={HorizontalNavProviderValue}>{children}</HorizontalNavContext.Provider>
|
||||
}
|
||||
|
||||
export default HorizontalNavContext
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { createContext, useCallback, useMemo, useState } from 'react'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType } from '../types'
|
||||
|
||||
export type VerticalNavState = {
|
||||
width?: number
|
||||
collapsedWidth?: number
|
||||
isCollapsed?: boolean
|
||||
isHovered?: boolean
|
||||
isToggled?: boolean
|
||||
isScrollWithContent?: boolean
|
||||
isBreakpointReached?: boolean
|
||||
isPopoutWhenCollapsed?: boolean
|
||||
collapsing?: boolean // for internal use only
|
||||
expanding?: boolean // for internal use only
|
||||
transitionDuration?: number
|
||||
}
|
||||
|
||||
export type VerticalNavContextProps = VerticalNavState & {
|
||||
updateVerticalNavState: (values: VerticalNavState) => void
|
||||
collapseVerticalNav: (value?: VerticalNavState['isCollapsed']) => void
|
||||
hoverVerticalNav: (value?: VerticalNavState['isHovered']) => void
|
||||
toggleVerticalNav: (value?: VerticalNavState['isToggled']) => void
|
||||
}
|
||||
|
||||
const VerticalNavContext = createContext({} as VerticalNavContextProps)
|
||||
|
||||
export const VerticalNavProvider = ({ children }: ChildrenType) => {
|
||||
// States
|
||||
const [verticalNavState, setVerticalNavState] = useState<VerticalNavState>()
|
||||
|
||||
// Hooks
|
||||
const updateVerticalNavState = useCallback((values: Partial<VerticalNavState>) => {
|
||||
setVerticalNavState(prevState => ({
|
||||
...prevState,
|
||||
...values,
|
||||
collapsing: values.isCollapsed === true,
|
||||
expanding: values.isCollapsed === false
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const collapseVerticalNav = useCallback((value?: boolean) => {
|
||||
setVerticalNavState(prevState => ({
|
||||
...prevState,
|
||||
isHovered: value !== undefined && false,
|
||||
isCollapsed: value !== undefined ? Boolean(value) : !Boolean(prevState?.isCollapsed),
|
||||
collapsing: value === true,
|
||||
expanding: value !== true
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const hoverVerticalNav = useCallback((value?: boolean) => {
|
||||
setVerticalNavState(prevState => ({
|
||||
...prevState,
|
||||
isHovered: value !== undefined ? Boolean(value) : !Boolean(prevState?.isHovered)
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const toggleVerticalNav = useCallback((value?: boolean) => {
|
||||
setVerticalNavState(prevState => ({
|
||||
...prevState,
|
||||
isToggled: value !== undefined ? Boolean(value) : !Boolean(prevState?.isToggled)
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const verticalNavProviderValue = useMemo(
|
||||
() => ({
|
||||
...verticalNavState,
|
||||
updateVerticalNavState,
|
||||
collapseVerticalNav,
|
||||
hoverVerticalNav,
|
||||
toggleVerticalNav
|
||||
}),
|
||||
[verticalNavState, updateVerticalNavState, collapseVerticalNav, hoverVerticalNav, toggleVerticalNav]
|
||||
)
|
||||
|
||||
return <VerticalNavContext.Provider value={verticalNavProviderValue}>{children}</VerticalNavContext.Provider>
|
||||
}
|
||||
|
||||
export default VerticalNavContext
|
||||
@@ -0,0 +1,16 @@
|
||||
// Type Imports
|
||||
import type { BreakpointType } from './types'
|
||||
|
||||
export const defaultBreakpoints: Record<BreakpointType, string> = {
|
||||
xs: '480px',
|
||||
sm: '600px',
|
||||
md: '900px',
|
||||
lg: '1200px',
|
||||
xl: '1536px',
|
||||
xxl: '1920px',
|
||||
always: 'always'
|
||||
}
|
||||
|
||||
export const verticalNavToggleDuration = 300
|
||||
export const verticalSubMenuToggleDuration = 300
|
||||
export const horizontalSubMenuToggleDuration = 200
|
||||
@@ -0,0 +1,22 @@
|
||||
// React Imports
|
||||
import { useContext } from 'react'
|
||||
|
||||
// Type Imports
|
||||
import type { HorizontalMenuContextProps } from '../components/horizontal-menu/Menu'
|
||||
|
||||
// Context Imports
|
||||
import { HorizontalMenuContext } from '../components/horizontal-menu/Menu'
|
||||
|
||||
const useHorizontalMenu = (): HorizontalMenuContextProps => {
|
||||
// Hooks
|
||||
const context = useContext(HorizontalMenuContext)
|
||||
|
||||
if (context === undefined) {
|
||||
//TODO: set better error message
|
||||
throw new Error('Menu Component is required!')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export default useHorizontalMenu
|
||||
@@ -0,0 +1,19 @@
|
||||
// React Imports
|
||||
import { useContext } from 'react'
|
||||
|
||||
// Context Imports
|
||||
import HorizontalNavContext from '../contexts/horizontalNavContext'
|
||||
|
||||
const useHorizontalNav = () => {
|
||||
// Hooks
|
||||
const context = useContext(HorizontalNavContext)
|
||||
|
||||
if (context === undefined) {
|
||||
//TODO: set better error message
|
||||
throw new Error('HorizontalNav Component is required!')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export default useHorizontalNav
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const useMediaQuery = (breakpoint?: string): boolean => {
|
||||
// States
|
||||
const [matches, setMatches] = useState(breakpoint === 'always')
|
||||
|
||||
useEffect(() => {
|
||||
if (breakpoint && breakpoint !== 'always') {
|
||||
const media = window.matchMedia(`(max-width: ${breakpoint})`)
|
||||
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches)
|
||||
}
|
||||
|
||||
const listener = () => setMatches(media.matches)
|
||||
|
||||
window.addEventListener('resize', listener)
|
||||
|
||||
return () => window.removeEventListener('resize', listener)
|
||||
}
|
||||
}, [matches, breakpoint])
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
export default useMediaQuery
|
||||
@@ -0,0 +1,22 @@
|
||||
// React Imports
|
||||
import { useContext } from 'react'
|
||||
|
||||
// Type Imports
|
||||
import type { VerticalMenuContextProps } from '../components/vertical-menu/Menu'
|
||||
|
||||
// Context Imports
|
||||
import { VerticalMenuContext } from '../components/vertical-menu/Menu'
|
||||
|
||||
const useVerticalMenu = (): VerticalMenuContextProps => {
|
||||
// Hooks
|
||||
const context = useContext(VerticalMenuContext)
|
||||
|
||||
if (context === undefined) {
|
||||
//TODO: set better error message
|
||||
throw new Error('Menu Component is required!')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export default useVerticalMenu
|
||||
@@ -0,0 +1,19 @@
|
||||
// React Imports
|
||||
import { useContext } from 'react'
|
||||
|
||||
// Context Imports
|
||||
import VerticalNavContext from '../contexts/verticalNavContext'
|
||||
|
||||
const useVerticalNav = () => {
|
||||
// Hooks
|
||||
const context = useContext(VerticalNavContext)
|
||||
|
||||
if (context === undefined) {
|
||||
//TODO: set better error message
|
||||
throw new Error('VerticalNav Component is required!')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export default useVerticalNav
|
||||
@@ -0,0 +1,13 @@
|
||||
// Import all Horizontal Nav components and export them
|
||||
import Menu from '../components/horizontal-menu/Menu'
|
||||
import SubMenu from '../components/horizontal-menu/SubMenu'
|
||||
import MenuItem from '../components/horizontal-menu/MenuItem'
|
||||
import HorizontalNav from '../components/horizontal-menu/HorizontalNav'
|
||||
import type { MenuProps } from '../components/horizontal-menu/Menu'
|
||||
import type { SubMenuProps } from '../components/horizontal-menu/SubMenu'
|
||||
import type { MenuItemProps } from '../components/horizontal-menu/MenuItem'
|
||||
import type { HorizontalNavProps } from '../components/horizontal-menu/HorizontalNav'
|
||||
|
||||
export default HorizontalNav
|
||||
export { Menu, MenuItem, SubMenu }
|
||||
export type { HorizontalNavProps, MenuProps, MenuItemProps, SubMenuProps }
|
||||
@@ -0,0 +1,20 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { VerticalNavProps } from '../vertical-menu'
|
||||
|
||||
type StyledBackdropProps = Pick<VerticalNavProps, 'backdropColor'>
|
||||
|
||||
const StyledBackdrop = styled.div<StyledBackdropProps>`
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-block-end: 0;
|
||||
z-index: 1;
|
||||
background-color: ${({ backdropColor }) => backdropColor || 'rgba(0, 0, 0, 0.3)'};
|
||||
touch-action: none;
|
||||
`
|
||||
|
||||
export default StyledBackdrop
|
||||
@@ -0,0 +1,15 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../types'
|
||||
|
||||
const StyledMenuIcon = styled.span<RootStylesType>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-inline-end: 10px;
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledMenuIcon
|
||||
@@ -0,0 +1,23 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../types'
|
||||
|
||||
type StyledMenuLabelProps = RootStylesType & {
|
||||
textTruncate?: boolean
|
||||
}
|
||||
|
||||
const StyledMenuLabel = styled.span<StyledMenuLabelProps>`
|
||||
flex-grow: 1;
|
||||
${({ textTruncate }) =>
|
||||
textTruncate &&
|
||||
`
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledMenuLabel
|
||||
@@ -0,0 +1,19 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../types'
|
||||
|
||||
type StyledMenuPrefixProps = RootStylesType & {
|
||||
firstLevel?: boolean
|
||||
isCollapsed?: boolean
|
||||
isHovered?: boolean
|
||||
}
|
||||
|
||||
const StyledMenuPrefix = styled.span<StyledMenuPrefixProps>`
|
||||
margin-inline-end: 5px;
|
||||
display: ${({ firstLevel, isCollapsed, isHovered }) => (firstLevel && isCollapsed && !isHovered ? 'none' : 'flex')};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledMenuPrefix
|
||||
@@ -0,0 +1,30 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../types'
|
||||
|
||||
type StyledMenuSectionLabelProps = RootStylesType & {
|
||||
isCollapsed?: boolean
|
||||
isHovered?: boolean
|
||||
textTruncate?: boolean
|
||||
}
|
||||
|
||||
const StyledMenuSectionLabel = styled.span<StyledMenuSectionLabelProps>`
|
||||
${({ textTruncate }) =>
|
||||
textTruncate &&
|
||||
`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`};
|
||||
${({ isCollapsed, isHovered }) =>
|
||||
!isCollapsed || (isCollapsed && isHovered)
|
||||
? `
|
||||
flex-grow: 1;
|
||||
`
|
||||
: ''}
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledMenuSectionLabel
|
||||
@@ -0,0 +1,19 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../types'
|
||||
|
||||
type StyledMenuSuffixProps = RootStylesType & {
|
||||
firstLevel?: boolean
|
||||
isCollapsed?: boolean
|
||||
isHovered?: boolean
|
||||
}
|
||||
|
||||
const StyledMenuSuffix = styled.span<StyledMenuSuffixProps>`
|
||||
margin-inline-start: 5px;
|
||||
display: ${({ firstLevel, isCollapsed, isHovered }) => (firstLevel && isCollapsed && !isHovered ? 'none' : 'flex')};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledMenuSuffix
|
||||
@@ -0,0 +1,46 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { SubMenuContentProps } from '../components/vertical-menu/SubMenuContent'
|
||||
|
||||
const StyledSubMenuContent = styled.div<SubMenuContentProps>`
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
transition: ${({ transitionDuration }) => `block-size ${transitionDuration}ms ease-in-out`};
|
||||
box-sizing: border-box;
|
||||
|
||||
${({ isCollapsed, level, isPopoutWhenCollapsed, isHovered }) =>
|
||||
isCollapsed &&
|
||||
level === 0 &&
|
||||
!isPopoutWhenCollapsed &&
|
||||
!isHovered &&
|
||||
`
|
||||
block-size: 0 !important;
|
||||
`}
|
||||
|
||||
${({ isCollapsed, level, isPopoutWhenCollapsed }) =>
|
||||
isCollapsed && level === 0 && isPopoutWhenCollapsed
|
||||
? `
|
||||
display: block;
|
||||
padding-inline-start: 0px;
|
||||
inline-size: 260px;
|
||||
border-radius: 4px;
|
||||
block-size: auto !important;
|
||||
transition: none !important;
|
||||
background-color: white;
|
||||
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
|
||||
`
|
||||
: `
|
||||
position: static !important;
|
||||
transform: none !important;
|
||||
`}
|
||||
|
||||
${({ browserScroll }) => browserScroll && `overflow-y: auto; max-block-size: calc((var(--vh, 1vh) * 100));`}
|
||||
|
||||
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledSubMenuContent
|
||||
@@ -0,0 +1,16 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuProps } from '../../components/vertical-menu/Menu'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
const StyledHorizontalMenu = styled.nav<Pick<MenuProps, 'rootStyles'>>`
|
||||
&.${menuClasses.root} {
|
||||
${({ rootStyles }) => rootStyles}
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledHorizontalMenu
|
||||
@@ -0,0 +1,36 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuItemProps } from '../../components/horizontal-menu/MenuItem'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Style Imports
|
||||
import { menuButtonStyles } from '../../components/horizontal-menu/MenuButton'
|
||||
|
||||
type StyledHorizontalMenuItemProps = Pick<MenuItemProps, 'rootStyles' | 'disabled'> & {
|
||||
level: number
|
||||
menuItemStyles?: CSSObject
|
||||
buttonStyles?: CSSObject
|
||||
}
|
||||
|
||||
const StyledHorizontalMenuItem = styled.li<StyledHorizontalMenuItemProps>`
|
||||
position: relative;
|
||||
${({ level }) => level === 0 && { borderRadius: '6px', overflow: 'hidden' }}
|
||||
${({ menuItemStyles }) => menuItemStyles};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
|
||||
> .${menuClasses.button} {
|
||||
${({ level, disabled }) =>
|
||||
menuButtonStyles({
|
||||
level,
|
||||
disabled
|
||||
})};
|
||||
${({ buttonStyles }) => buttonStyles};
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledHorizontalMenuItem
|
||||
@@ -0,0 +1,14 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { HorizontalNavProps } from '../../components/horizontal-menu/HorizontalNav'
|
||||
|
||||
const StyledHorizontalNav = styled.div<Pick<HorizontalNavProps, 'customStyles'>>`
|
||||
inline-size: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
${({ customStyles }) => customStyles}
|
||||
`
|
||||
|
||||
export default StyledHorizontalNav
|
||||
@@ -0,0 +1,40 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../../types'
|
||||
|
||||
type StyledHorizontalNavExpandIconProps = {
|
||||
level?: number
|
||||
}
|
||||
|
||||
export const StyledHorizontalNavExpandIconWrapper = styled.span<RootStylesType>`
|
||||
display: flex;
|
||||
margin-inline-start: 5px;
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
const StyledHorizontalNavExpandIcon = styled.span<StyledHorizontalNavExpandIconProps>`
|
||||
display: flex;
|
||||
|
||||
${({ level }) =>
|
||||
level === 0 &&
|
||||
`
|
||||
& > i,
|
||||
& > svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`}
|
||||
|
||||
${({ level }) =>
|
||||
level &&
|
||||
level > 0 &&
|
||||
`
|
||||
[dir='rtl'] & > i,
|
||||
[dir='rtl'] & > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export default StyledHorizontalNavExpandIcon
|
||||
@@ -0,0 +1,21 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { SubMenuContentProps } from '../../components/horizontal-menu/SubMenuContent'
|
||||
|
||||
const StyledHorizontalSubMenuContent = styled.div<SubMenuContentProps>`
|
||||
inline-size: 260px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 9px 28px 8px #00000011;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
|
||||
${({ browserScroll, top }) =>
|
||||
browserScroll && `overflow-y: auto; max-block-size: calc((var(--vh, 1vh) * 100) - ${top}px);`}
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledHorizontalSubMenuContent
|
||||
@@ -0,0 +1,13 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../../types'
|
||||
|
||||
const StyledHorizontalSubMenuContentWrapper = styled.div<RootStylesType>`
|
||||
z-index: 10;
|
||||
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledHorizontalSubMenuContentWrapper
|
||||
@@ -0,0 +1,15 @@
|
||||
.root {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.li:not(:last-of-type) {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuProps } from '../../components/vertical-menu/Menu'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
const StyledVerticalMenu = styled.nav<Pick<MenuProps, 'rootStyles'>>`
|
||||
& > ul > :first-of-type {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
&.${menuClasses.root} {
|
||||
${({ rootStyles }) => rootStyles}
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledVerticalMenu
|
||||
@@ -0,0 +1,40 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuItemProps } from '../../components/vertical-menu/MenuItem'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
// Style Imports
|
||||
import { menuButtonStyles } from '../../components/vertical-menu/MenuButton'
|
||||
|
||||
type StyledVerticalMenuItemProps = Pick<MenuItemProps, 'rootStyles' | 'disabled'> & {
|
||||
level: number
|
||||
menuItemStyles?: CSSObject
|
||||
isCollapsed?: boolean
|
||||
isPopoutWhenCollapsed?: boolean
|
||||
buttonStyles?: CSSObject
|
||||
}
|
||||
|
||||
const StyledVerticalMenuItem = styled.li<StyledVerticalMenuItemProps>`
|
||||
position: relative;
|
||||
margin-block-start: 4px;
|
||||
${({ menuItemStyles }) => menuItemStyles};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
|
||||
> .${menuClasses.button} {
|
||||
${({ level, disabled, isCollapsed, isPopoutWhenCollapsed }) =>
|
||||
menuButtonStyles({
|
||||
level,
|
||||
disabled,
|
||||
isCollapsed,
|
||||
isPopoutWhenCollapsed
|
||||
})};
|
||||
${({ buttonStyles }) => buttonStyles};
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledVerticalMenuItem
|
||||
@@ -0,0 +1,31 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { MenuSectionProps } from '../../components/vertical-menu/MenuSection'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from '../../utils/menuClasses'
|
||||
|
||||
type StyledVerticalMenuSectionProps = Pick<MenuSectionProps, 'rootStyles' | 'children'> & {
|
||||
menuSectionStyles?: CSSObject
|
||||
}
|
||||
|
||||
const StyledVerticalMenuSection = styled.li<StyledVerticalMenuSectionProps>`
|
||||
display: flex;
|
||||
inline-size: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-block-start: 15px;
|
||||
|
||||
& .${menuClasses.menuSectionContent} {
|
||||
font-size: 14px;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
${({ menuSectionStyles }) => menuSectionStyles};
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
export default StyledVerticalMenuSection
|
||||
@@ -0,0 +1,74 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { VerticalNavState } from '../../contexts/verticalNavContext'
|
||||
import type { VerticalNavProps } from '../../components/vertical-menu/VerticalNav'
|
||||
|
||||
// Util Imports
|
||||
import { horizontalNavClasses, menuClasses, verticalNavClasses } from '../../utils/menuClasses'
|
||||
|
||||
type StyledVerticalNavProps = VerticalNavProps &
|
||||
Pick<VerticalNavState, 'isBreakpointReached' | 'collapsing' | 'expanding' | 'transitionDuration'>
|
||||
|
||||
const StyledVerticalNav = styled.aside<StyledVerticalNavProps>`
|
||||
${({ scrollWithContent }) =>
|
||||
!scrollWithContent &&
|
||||
`
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
block-size: 100dvh;
|
||||
`}
|
||||
z-index: 9;
|
||||
|
||||
/* Transition */
|
||||
transition-property: inline-size, min-inline-size, margin-inline-start, inset-inline-start;
|
||||
transition-duration: ${({ transitionDuration }) => `${transitionDuration}ms`};
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
/* Width & Min Width & Margin */
|
||||
inline-size: ${({ width }) => `${width}px`};
|
||||
min-inline-size: ${({ width }) => `${width}px`};
|
||||
&.${verticalNavClasses.collapsed} {
|
||||
inline-size: ${({ collapsedWidth }) => `${collapsedWidth}px`};
|
||||
min-inline-size: ${({ collapsedWidth }) => `${collapsedWidth}px`};
|
||||
}
|
||||
|
||||
&.${verticalNavClasses.collapsing}, &.${verticalNavClasses.expanding} {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Collapsed & Toggled */
|
||||
&.${verticalNavClasses.breakpointReached} {
|
||||
position: fixed;
|
||||
block-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: ${({ width }) => `-${width}px`};
|
||||
z-index: 100;
|
||||
margin: 0;
|
||||
&.${verticalNavClasses.collapsed} {
|
||||
inset-inline-start: -${({ collapsedWidth }) => `${collapsedWidth}px`};
|
||||
}
|
||||
&.${verticalNavClasses.toggled} {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
${({ width, isBreakpointReached }) =>
|
||||
!isBreakpointReached &&
|
||||
`
|
||||
&.${verticalNavClasses.toggled} {
|
||||
margin-inline-start: -${width}px;
|
||||
}
|
||||
`}
|
||||
|
||||
&.${horizontalNavClasses.root} .${menuClasses.root} > ul {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* User Styles */
|
||||
${({ customStyles }) => customStyles}
|
||||
`
|
||||
|
||||
export default StyledVerticalNav
|
||||
@@ -0,0 +1,20 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { VerticalNavProps } from '../../components/vertical-menu/VerticalNav'
|
||||
|
||||
type StyledVerticalNavBgColorContainerProps = Pick<VerticalNavProps, 'backgroundColor'>
|
||||
|
||||
const StyledVerticalNavBgColorContainer = styled.div<StyledVerticalNavBgColorContainerProps>`
|
||||
position: relative;
|
||||
block-size: 100%;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
${({ backgroundColor }) => backgroundColor && `background-color:${backgroundColor};`}
|
||||
`
|
||||
|
||||
export default StyledVerticalNavBgColorContainer
|
||||
@@ -0,0 +1,28 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { VerticalNavProps } from '../../components/vertical-menu/VerticalNav'
|
||||
|
||||
// Util Imports
|
||||
import { verticalNavClasses } from '../../utils/menuClasses'
|
||||
|
||||
type StyledVerticalNavContainerProps = Pick<VerticalNavProps, 'width' | 'transitionDuration'>
|
||||
|
||||
const StyledVerticalNavContainer = styled.div<StyledVerticalNavContainerProps>`
|
||||
position: relative;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
border-inline-end: 1px solid #efefef;
|
||||
.${verticalNavClasses.hovered} &,
|
||||
.${verticalNavClasses.expanding} & {
|
||||
inline-size: ${({ width }) => `${width}px`};
|
||||
}
|
||||
|
||||
/* Transition */
|
||||
transition-property: inline-size, inset-inline-start;
|
||||
transition-duration: ${({ transitionDuration }) => `${transitionDuration}ms`};
|
||||
transition-timing-function: ease-in-out;
|
||||
`
|
||||
|
||||
export default StyledVerticalNavContainer
|
||||
@@ -0,0 +1,34 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { RootStylesType } from '../../types'
|
||||
import type { VerticalMenuContextProps } from '../../components/vertical-menu/Menu'
|
||||
|
||||
type StyledVerticalNavExpandIconProps = {
|
||||
open?: boolean
|
||||
transitionDuration?: VerticalMenuContextProps['transitionDuration']
|
||||
}
|
||||
|
||||
export const StyledVerticalNavExpandIconWrapper = styled.span<RootStylesType>`
|
||||
display: flex;
|
||||
margin-inline-start: 5px;
|
||||
${({ rootStyles }) => rootStyles};
|
||||
`
|
||||
|
||||
const StyledVerticalNavExpandIcon = styled.span<StyledVerticalNavExpandIconProps>`
|
||||
display: flex;
|
||||
|
||||
& > i,
|
||||
& > svg {
|
||||
transition: ${({ transitionDuration }) => `transform ${transitionDuration}ms ease-in-out`};
|
||||
${({ open }) => open && 'transform: rotate(90deg);'}
|
||||
|
||||
[dir='rtl'] & {
|
||||
transform: rotate(180deg);
|
||||
${({ open }) => open && 'transform: rotate(90deg);'}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledVerticalNavExpandIcon
|
||||
@@ -0,0 +1,10 @@
|
||||
.root {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// React Imports
|
||||
import type { SVGAttributes } from 'react'
|
||||
|
||||
const ChevronRight = (props: SVGAttributes<SVGElement>) => {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' fontSize='1.5rem' viewBox='0 0 24 24' {...props}>
|
||||
<path fill='currentColor' d='M10.707 17.707 16.414 12l-5.707-5.707-1.414 1.414L13.586 12l-4.293 4.293z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChevronRight
|
||||
@@ -0,0 +1,15 @@
|
||||
// React Imports
|
||||
import type { SVGAttributes } from 'react'
|
||||
|
||||
const Close = (props: SVGAttributes<SVGElement>) => {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' fontSize='1.5rem' viewBox='0 0 24 24' {...props}>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='m16.192 6.344-4.243 4.242-4.242-4.242-1.414 1.414L10.535 12l-4.242 4.242 1.414 1.414 4.242-4.242 4.243 4.242 1.414-1.414L13.364 12l4.242-4.242z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Close
|
||||
@@ -0,0 +1,15 @@
|
||||
// React Imports
|
||||
import type { SVGAttributes } from 'react'
|
||||
|
||||
const RadioCircle = (props: SVGAttributes<SVGElement>) => {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' fontSize='1.5rem' viewBox='0 0 24 24' {...props}>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M5 12c0 3.859 3.14 7 7 7 3.859 0 7-3.141 7-7s-3.141-7-7-7c-3.86 0-7 3.141-7 7zm12 0c0 2.757-2.243 5-5 5s-5-2.243-5-5 2.243-5 5-5 5 2.243 5 5z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadioCircle
|
||||
@@ -0,0 +1,16 @@
|
||||
// React Imports
|
||||
import type { SVGAttributes } from 'react'
|
||||
|
||||
const RadioCircleMarked = (props: SVGAttributes<SVGElement>) => {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' fontSize='1.5rem' viewBox='0 0 24 24' {...props}>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M12 5c-3.859 0-7 3.141-7 7s3.141 7 7 7 7-3.141 7-7-3.141-7-7-7zm0 12c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z'
|
||||
/>
|
||||
<path fill='currentColor' d='M12 9c-1.627 0-3 1.373-3 3s1.373 3 3 3 3-1.373 3-3-1.373-3-3-3z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadioCircleMarked
|
||||
@@ -0,0 +1,95 @@
|
||||
// React Imports
|
||||
import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
export type ChildrenType = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
// Breakpoints
|
||||
export type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'always'
|
||||
|
||||
// Exact match for active URL in menu item
|
||||
export type MenuItemExactMatchUrlProps =
|
||||
| {
|
||||
exactMatch: true
|
||||
activeUrl?: never
|
||||
}
|
||||
| {
|
||||
exactMatch: false
|
||||
activeUrl: string
|
||||
}
|
||||
| {
|
||||
exactMatch?: never
|
||||
activeUrl?: never
|
||||
}
|
||||
|
||||
// Menu Item Elements for styling
|
||||
export type MenuItemElement = 'root' | 'button' | 'icon' | 'label' | 'prefix' | 'suffix'
|
||||
|
||||
// Sub Menu Item Elements for styling
|
||||
export type SubMenuItemElement =
|
||||
| 'root'
|
||||
| 'button'
|
||||
| 'label'
|
||||
| 'prefix'
|
||||
| 'suffix'
|
||||
| 'icon'
|
||||
| 'subMenuStyles'
|
||||
| 'subMenuContent'
|
||||
| 'subMenuExpandIcon'
|
||||
|
||||
// Menu Button Props
|
||||
export type MenuButtonProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
|
||||
Partial<ChildrenType> & {
|
||||
component?: string | ReactElement
|
||||
}
|
||||
|
||||
// Menu Item Styles Params Type
|
||||
export type MenuItemStylesParams = {
|
||||
level: number
|
||||
disabled: boolean
|
||||
active?: boolean
|
||||
isSubmenu: boolean
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
// Menu Item Style Elements Type
|
||||
export type ElementStyles = CSSObject | ((params: MenuItemStylesParams) => CSSObject | undefined)
|
||||
|
||||
// Menu Item Styles Type
|
||||
export type MenuItemStyles = {
|
||||
root?: ElementStyles
|
||||
button?: ElementStyles
|
||||
label?: ElementStyles
|
||||
prefix?: ElementStyles
|
||||
suffix?: ElementStyles
|
||||
icon?: ElementStyles
|
||||
subMenuStyles?: ElementStyles
|
||||
subMenuContent?: ElementStyles
|
||||
subMenuExpandIcon?: ElementStyles
|
||||
}
|
||||
|
||||
// Expand Icon
|
||||
export type RenderExpandIconParams = {
|
||||
open: boolean
|
||||
level: number
|
||||
active: boolean
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
// Icon for menu items in expanded submenu
|
||||
export type RenderExpandedMenuItemIcon = {
|
||||
icon:
|
||||
| ReactElement
|
||||
| ((params: { level?: number; active?: boolean; disabled?: boolean }) => ReactElement | null)
|
||||
| null
|
||||
level?: number
|
||||
}
|
||||
|
||||
// Root Styles
|
||||
export type RootStylesType = {
|
||||
rootStyles?: CSSObject
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Common classes for menu components
|
||||
export const menuClasses = {
|
||||
root: 'ts-menu-root',
|
||||
menuSectionRoot: 'ts-menusection-root',
|
||||
menuItemRoot: 'ts-menuitem-root',
|
||||
subMenuRoot: 'ts-submenu-root',
|
||||
button: 'ts-menu-button',
|
||||
prefix: 'ts-menu-prefix',
|
||||
suffix: 'ts-menu-suffix',
|
||||
label: 'ts-menu-label',
|
||||
icon: 'ts-menu-icon',
|
||||
menuSectionWrapper: 'ts-menu-section-wrapper',
|
||||
menuSectionContent: 'ts-menu-section-content',
|
||||
menuSectionLabel: 'ts-menu-section-label',
|
||||
subMenuContent: 'ts-submenu-content',
|
||||
subMenuExpandIcon: 'ts-submenu-expand-icon',
|
||||
disabled: 'ts-disabled',
|
||||
active: 'ts-active',
|
||||
open: 'ts-open'
|
||||
}
|
||||
|
||||
// Classes for vertical navigation menu
|
||||
export const verticalNavClasses = {
|
||||
root: 'ts-vertical-nav-root',
|
||||
container: 'ts-vertical-nav-container',
|
||||
bgColorContainer: 'ts-vertical-nav-bg-color-container',
|
||||
header: 'ts-vertical-nav-header',
|
||||
image: 'ts-vertical-nav-image',
|
||||
backdrop: 'ts-vertical-nav-backdrop',
|
||||
collapsed: 'ts-collapsed',
|
||||
toggled: 'ts-toggled',
|
||||
hovered: 'ts-hovered',
|
||||
scrollWithContent: 'ts-scroll-with-content',
|
||||
breakpointReached: 'ts-breakpoint-reached',
|
||||
collapsing: 'ts-collapsing',
|
||||
expanding: 'ts-expanding'
|
||||
}
|
||||
|
||||
// Classes for horizontal navigation menu
|
||||
export const horizontalNavClasses = {
|
||||
root: 'ts-horizontal-nav-root',
|
||||
scrollWithContent: 'ts-scroll-with-content',
|
||||
breakpointReached: 'ts-breakpoint-reached'
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// React Imports
|
||||
import { Children, isValidElement } from 'react'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType, RenderExpandedMenuItemIcon } from '../types'
|
||||
|
||||
// Component Imports
|
||||
import {
|
||||
SubMenu as HorizontalSubMenu,
|
||||
MenuItem as HorizontalMenuItem,
|
||||
Menu as HorizontalMenu
|
||||
} from '../horizontal-menu'
|
||||
import { SubMenu as VerticalSubMenu, MenuItem as VerticalMenuItem, Menu as VerticalMenu } from '../vertical-menu'
|
||||
import { GenerateVerticalMenu } from '@components/GenerateMenu'
|
||||
|
||||
// Util Imports
|
||||
import { menuClasses } from './menuClasses'
|
||||
|
||||
// Styled Component Imports
|
||||
import StyledMenuIcon from '../styles/StyledMenuIcon'
|
||||
|
||||
type RenderMenuIconParams = {
|
||||
level?: number
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
styles?: CSSObject
|
||||
icon?: ReactElement
|
||||
renderExpandedMenuItemIcon?: RenderExpandedMenuItemIcon
|
||||
isBreakpointReached?: boolean
|
||||
}
|
||||
|
||||
export const confirmUrlInChildren = (children: ChildrenType['children'], url: string): boolean => {
|
||||
if (!children) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
return children.some((child: ReactNode) => confirmUrlInChildren(child, url))
|
||||
}
|
||||
|
||||
if (isValidElement(children)) {
|
||||
const { component, href, exactMatch, activeUrl, children: subChildren } = children.props
|
||||
|
||||
if (component && component.props.href) {
|
||||
return exactMatch === true || exactMatch === undefined
|
||||
? component.props.href === url
|
||||
: activeUrl && url.includes(activeUrl)
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return exactMatch === true || exactMatch === undefined ? href === url : activeUrl && url.includes(activeUrl)
|
||||
}
|
||||
|
||||
if (subChildren) {
|
||||
return confirmUrlInChildren(subChildren, url)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
* Reason behind mapping the children of the horizontal-menu component to the vertical-menu component:
|
||||
* The Horizontal menu components will not work inside of Vertical menu on small screens.
|
||||
* So, we have to map the children of the horizontal-menu components to the vertical-menu components.
|
||||
* We also kept the same names and almost similar props for menuitem and submenu components for easy mapping.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processes children of a HorizontalMenu component to either generate a vertical menu directly
|
||||
* from menuData or apply a transformation function to each child.
|
||||
*
|
||||
* @param {ReactNode} children - The children of the HorizontalMenu component.
|
||||
* @param {Function} mapFunction - A function to transform each child that doesn't have menuData.
|
||||
* @returns {ReactNode} The processed children suitable for inclusion in a VerticalMenu.
|
||||
*/
|
||||
const processMenuChildren = (children: ReactNode, mapFunction: (child: ReactNode) => ReactNode): ReactNode => {
|
||||
return Children.map(children, child => {
|
||||
// Skip processing for non-React elements
|
||||
if (!isValidElement(child)) return child
|
||||
|
||||
// If child has menuData prop, create a GenerateVerticalMenu component
|
||||
// Otherwise, apply the transformation function to the child
|
||||
return child.props?.menuData ? <GenerateVerticalMenu menuData={child.props.menuData} /> : mapFunction(child)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a hierarchy of horizontal menu components (HorizontalMenuItem,
|
||||
* HorizontalSubMenu, and HorizontalMenu) into their vertical equivalents.
|
||||
*
|
||||
* @param {ReactNode} children - The children of the menu to be transformed.
|
||||
* @returns {ReactNode} The transformed menu as a hierarchy of vertical menu components.
|
||||
*/
|
||||
export const mapHorizontalToVerticalMenu = (children: ReactNode): ReactNode => {
|
||||
return Children.map(children, child => {
|
||||
// If the child is not a valid React element, exclude it from the output
|
||||
if (!isValidElement(child)) return null
|
||||
|
||||
// Destructure to separate specific props and rest props for further use
|
||||
const { children: childChildren, verticalMenuProps, ...rest } = child.props
|
||||
|
||||
// Use a switch statement to handle different types of menu items
|
||||
switch (child.type) {
|
||||
case HorizontalMenuItem:
|
||||
// Directly transform HorizontalMenuItem to VerticalMenuItem
|
||||
return <VerticalMenuItem {...rest}>{childChildren}</VerticalMenuItem>
|
||||
case HorizontalSubMenu:
|
||||
// Transform HorizontalSubMenu to VerticalSubMenu, recursively transforming its children
|
||||
return <VerticalSubMenu {...rest}>{mapHorizontalToVerticalMenu(childChildren)}</VerticalSubMenu>
|
||||
case HorizontalMenu:
|
||||
// For HorizontalMenu, process its children specifically, then wrap in VerticalMenu
|
||||
const transformedChildren = processMenuChildren(childChildren, mapHorizontalToVerticalMenu)
|
||||
|
||||
return <VerticalMenu {...verticalMenuProps}>{transformedChildren}</VerticalMenu>
|
||||
default:
|
||||
// For any other type of child, return it without modification
|
||||
return child
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Render all the icons for Menu Item and SubMenu components for all the levels more than 0
|
||||
*/
|
||||
export const renderMenuIcon = (params: RenderMenuIconParams) => {
|
||||
const { icon, level, active, disabled, styles, renderExpandedMenuItemIcon, isBreakpointReached } = params
|
||||
|
||||
if (icon && (level === 0 || (!isBreakpointReached && level && level > 0))) {
|
||||
return (
|
||||
<StyledMenuIcon className={menuClasses.icon} rootStyles={styles}>
|
||||
{icon}
|
||||
</StyledMenuIcon>
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
level &&
|
||||
level !== 0 &&
|
||||
renderExpandedMenuItemIcon &&
|
||||
renderExpandedMenuItemIcon.icon !== null &&
|
||||
(!renderExpandedMenuItemIcon.level || renderExpandedMenuItemIcon.level >= level)
|
||||
) {
|
||||
const iconToRender =
|
||||
typeof renderExpandedMenuItemIcon.icon === 'function'
|
||||
? renderExpandedMenuItemIcon.icon({ level, active, disabled })
|
||||
: renderExpandedMenuItemIcon.icon
|
||||
|
||||
if (iconToRender) {
|
||||
return (
|
||||
<StyledMenuIcon className={menuClasses.icon} rootStyles={styles}>
|
||||
{iconToRender}
|
||||
</StyledMenuIcon>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Import all Vertical Nav components and export them
|
||||
import Menu from '../components/vertical-menu/Menu'
|
||||
import SubMenu from '../components/vertical-menu/SubMenu'
|
||||
import MenuItem from '../components/vertical-menu/MenuItem'
|
||||
import NavHeader from '../components/vertical-menu/NavHeader'
|
||||
import VerticalNav from '../components/vertical-menu/VerticalNav'
|
||||
import MenuSection from '../components/vertical-menu/MenuSection'
|
||||
import NavCollapseIcons from '../components/vertical-menu/NavCollapseIcons'
|
||||
import type { MenuProps } from '../components/vertical-menu/Menu'
|
||||
import type { SubMenuProps } from '../components/vertical-menu/SubMenu'
|
||||
import type { MenuItemProps } from '../components/vertical-menu/MenuItem'
|
||||
import type { MenuSectionProps } from '../components/vertical-menu/MenuSection'
|
||||
import type { VerticalNavProps } from '../components/vertical-menu/VerticalNav'
|
||||
|
||||
export default VerticalNav
|
||||
export { Menu, MenuItem, SubMenu, MenuSection, NavHeader, NavCollapseIcons }
|
||||
export type { VerticalNavProps, MenuProps, MenuItemProps, SubMenuProps, MenuSectionProps }
|
||||
Reference in New Issue
Block a user