initial commit

This commit is contained in:
ferdiansyah783
2025-08-05 12:35:40 +07:00
commit fffa2ead5c
1069 changed files with 118056 additions and 0 deletions
+27
View File
@@ -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
+229
View File
@@ -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
+84
View File
@@ -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
+16
View File
@@ -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
+22
View File
@@ -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
+19
View File
@@ -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
+29
View File
@@ -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
+22
View File
@@ -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
+19
View File
@@ -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
+13
View File
@@ -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 }
+20
View File
@@ -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
+15
View File
@@ -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
+23
View File
@@ -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
+19
View File
@@ -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
+19
View File
@@ -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
+46
View File
@@ -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;
}
}
+5
View File
@@ -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;
}
+12
View File
@@ -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
+15
View File
@@ -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
+15
View File
@@ -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
+16
View File
@@ -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
+95
View File
@@ -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
}
+44
View File
@@ -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'
}
+163
View File
@@ -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
}
+17
View File
@@ -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 }