add front-urban
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType } from '../types'
|
||||
|
||||
type RouterLinkProps = LinkProps &
|
||||
Partial<ChildrenType> & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const RouterLink = forwardRef((props: RouterLinkProps, ref: any) => {
|
||||
// Props
|
||||
const { href, className, ...other } = props
|
||||
|
||||
return (
|
||||
<Link ref={ref} href={href.toString() || '/'} className={className} {...other}>
|
||||
{props.children}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
'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 type { CSSObject } from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type {
|
||||
ChildrenType,
|
||||
MenuItemStyles,
|
||||
RootStylesType,
|
||||
RenderExpandIconParams,
|
||||
RenderExpandedMenuItemIcon
|
||||
} from '../../types'
|
||||
|
||||
// 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 = {
|
||||
transitionDuration?: number
|
||||
menuSectionStyles?: MenuSectionStyles
|
||||
menuItemStyles?: MenuItemStyles
|
||||
subMenuOpenBehavior?: 'accordion' | 'collapse'
|
||||
renderExpandIcon?: (params: RenderExpandIconParams) => ReactElement
|
||||
renderExpandedMenuItemIcon?: RenderExpandedMenuItemIcon
|
||||
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>
|
||||
|
||||
export const VerticalMenuContext = createContext({} as VerticalMenuContextProps)
|
||||
|
||||
const Menu: ForwardRefRenderFunction<HTMLMenuElement, MenuProps> = (props, ref) => {
|
||||
// Props
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
rootStyles,
|
||||
menuItemStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
menuSectionStyles,
|
||||
subMenuOpenBehavior = 'accordion', // accordion, collapse
|
||||
transitionDuration = verticalSubMenuToggleDuration,
|
||||
textTruncate = true,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [openSubmenu, setOpenSubmenu] = useState<OpenSubmenu[]>([])
|
||||
|
||||
// Refs
|
||||
const openSubmenusRef = useRef<OpenSubmenu[]>([])
|
||||
|
||||
// Hooks
|
||||
const pathname = usePathname()
|
||||
|
||||
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])
|
||||
|
||||
const providerValue = useMemo(
|
||||
() => ({
|
||||
transitionDuration,
|
||||
menuItemStyles,
|
||||
menuSectionStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
openSubmenu,
|
||||
openSubmenusRef,
|
||||
toggleOpenSubmenu,
|
||||
subMenuOpenBehavior,
|
||||
textTruncate
|
||||
}),
|
||||
[
|
||||
transitionDuration,
|
||||
menuItemStyles,
|
||||
menuSectionStyles,
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
openSubmenu,
|
||||
openSubmenusRef,
|
||||
toggleOpenSubmenu,
|
||||
subMenuOpenBehavior,
|
||||
textTruncate
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<VerticalMenuContext.Provider value={providerValue}>
|
||||
<StyledVerticalMenu
|
||||
ref={ref}
|
||||
className={classnames(menuClasses.root, className)}
|
||||
rootStyles={rootStyles}
|
||||
{...rest}
|
||||
>
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
</StyledVerticalMenu>
|
||||
</VerticalMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(Menu)
|
||||
@@ -0,0 +1,76 @@
|
||||
// React Imports
|
||||
import { forwardRef } from 'react'
|
||||
import type { ForwardRefRenderFunction } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
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
|
||||
}
|
||||
|
||||
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',
|
||||
paddingInlineEnd: '20px',
|
||||
paddingInlineStart: `${level === 0 ? 20 : (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, children, ...rest },
|
||||
ref
|
||||
) => {
|
||||
return rest.href ? (
|
||||
<RouterLink ref={ref} className={className} href={rest.href} {...rest}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
) : (
|
||||
<a ref={ref} className={className} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(MenuButton)
|
||||
@@ -0,0 +1,186 @@
|
||||
'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
|
||||
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,
|
||||
onActiveChange,
|
||||
rootStyles,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const pathname = usePathname()
|
||||
const { menuItemStyles, renderExpandedMenuItemIcon, textTruncate } = useVerticalMenu()
|
||||
|
||||
const { 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
|
||||
|
||||
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}
|
||||
disabled={disabled}
|
||||
buttonStyles={getMenuItemStyles('button')}
|
||||
menuItemStyles={getMenuItemStyles('root')}
|
||||
rootStyles={rootStyles}
|
||||
>
|
||||
<MenuButton
|
||||
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
|
||||
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 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 className={menuClasses.suffix} rootStyles={getMenuItemStyles('suffix')}>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
</MenuButton>
|
||||
</StyledVerticalMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(MenuItem)
|
||||
@@ -0,0 +1,121 @@
|
||||
'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 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 { menuSectionStyles, 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 className={menuClasses.prefix} rootStyles={getMenuSectionStyles('prefix')}>
|
||||
{prefix}
|
||||
</StyledMenuPrefix>
|
||||
)}
|
||||
{label && (
|
||||
<StyledMenuSectionLabel
|
||||
className={menuClasses.menuSectionLabel}
|
||||
rootStyles={getMenuSectionStyles('label')}
|
||||
textTruncate={textTruncate}
|
||||
>
|
||||
{label}
|
||||
</StyledMenuSectionLabel>
|
||||
)}
|
||||
{suffix && (
|
||||
<StyledMenuSuffix className={menuClasses.suffix} rootStyles={getMenuSectionStyles('suffix')}>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
</li>
|
||||
{/* Render Child */}
|
||||
{children}
|
||||
</ul>
|
||||
</StyledVerticalMenuSection>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLLIElement, MenuSectionProps>(MenuSection)
|
||||
@@ -0,0 +1,22 @@
|
||||
// Third-party Imports
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
// Type Imports
|
||||
import type { ChildrenType } from '../../types'
|
||||
|
||||
// Util Imports
|
||||
import { verticalNavClasses } from '../../utils/menuClasses'
|
||||
|
||||
const StyledNavHeader = styled.div`
|
||||
padding: 15px;
|
||||
padding-inline-start: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const NavHeader = ({ children }: ChildrenType) => {
|
||||
return <StyledNavHeader className={verticalNavClasses.header}>{children}</StyledNavHeader>
|
||||
}
|
||||
|
||||
export default NavHeader
|
||||
@@ -0,0 +1,332 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { Children, cloneElement, forwardRef, useEffect, useId, 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 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
|
||||
contentClassName?: string
|
||||
onOpenChange?: (open: boolean) => void
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
level?: number
|
||||
}
|
||||
|
||||
type StyledSubMenuProps = Pick<SubMenuProps, 'rootStyles' | 'disabled'> & {
|
||||
level: number
|
||||
active?: boolean
|
||||
menuItemStyles?: CSSObject
|
||||
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 }) =>
|
||||
menuButtonStyles({
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
children
|
||||
})};
|
||||
${({ 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,
|
||||
onOpenChange,
|
||||
onClick,
|
||||
onKeyUp,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [active, setActive] = useState<boolean>(false)
|
||||
|
||||
// Refs
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Hooks
|
||||
const id = useId()
|
||||
const pathname = usePathname()
|
||||
const { isBreakpointReached } = useVerticalNav()
|
||||
|
||||
const {
|
||||
renderExpandIcon,
|
||||
renderExpandedMenuItemIcon,
|
||||
menuItemStyles,
|
||||
openSubmenu,
|
||||
toggleOpenSubmenu,
|
||||
transitionDuration,
|
||||
openSubmenusRef,
|
||||
textTruncate
|
||||
} = useVerticalMenu()
|
||||
|
||||
// Vars
|
||||
// Filter out falsy values from children
|
||||
const childNodes = Children.toArray(children).filter(Boolean) as [ReactElement<SubMenuProps | MenuItemProps>]
|
||||
|
||||
const isSubMenuOpen = openSubmenu?.some((item: OpenSubmenu) => item.id === id) ?? false
|
||||
|
||||
const handleSlideToggle = (): void => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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={contentRef}
|
||||
transitionDuration={transitionDuration}
|
||||
open={isSubMenuOpen}
|
||||
level={level}
|
||||
className={classnames(menuClasses.subMenuContent, contentClassName)}
|
||||
rootStyles={{
|
||||
...getSubMenuItemStyles('subMenuContent')
|
||||
}}
|
||||
>
|
||||
{childNodes.map(node =>
|
||||
cloneElement(node, {
|
||||
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}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
buttonStyles={getSubMenuItemStyles('button')}
|
||||
rootStyles={rootStyles}
|
||||
>
|
||||
{/* Menu Item */}
|
||||
<MenuButton
|
||||
ref={null}
|
||||
onClick={handleOnClick}
|
||||
onKeyUp={handleOnKeyUp}
|
||||
title={title}
|
||||
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
{...rest}
|
||||
>
|
||||
{/* Sub Menu Icon */}
|
||||
{renderMenuIcon({
|
||||
icon,
|
||||
level,
|
||||
active,
|
||||
disabled,
|
||||
renderExpandedMenuItemIcon,
|
||||
styles: getSubMenuItemStyles('icon'),
|
||||
isBreakpointReached
|
||||
})}
|
||||
|
||||
{/* Sub Menu Prefix */}
|
||||
{prefix && (
|
||||
<StyledMenuPrefix 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 className={menuClasses.suffix} rootStyles={getSubMenuItemStyles('suffix')}>
|
||||
{suffix}
|
||||
</StyledMenuSuffix>
|
||||
)}
|
||||
|
||||
{/* Sub Menu Toggle Icon Wrapper */}
|
||||
{
|
||||
<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 */}
|
||||
{submenuContent}
|
||||
</StyledSubMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLLIElement, SubMenuProps>(SubMenu)
|
||||
@@ -0,0 +1,84 @@
|
||||
// React Imports
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import type { ForwardRefRenderFunction, HTMLAttributes, MutableRefObject } from 'react'
|
||||
|
||||
// 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
|
||||
transitionDuration?: VerticalMenuContextProps['transitionDuration']
|
||||
level?: number
|
||||
}
|
||||
|
||||
const SubMenuContent: ForwardRefRenderFunction<HTMLDivElement, SubMenuContentProps> = (props, ref) => {
|
||||
// Props
|
||||
const { children, open, level, transitionDuration, ...rest } = props
|
||||
|
||||
// States
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Refs
|
||||
const SubMenuContentRef = ref as MutableRefObject<HTMLDivElement>
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
if (open) {
|
||||
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)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StyledSubMenuContent ref={ref} level={level} open={open} transitionDuration={transitionDuration} {...rest}>
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
</StyledSubMenuContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(SubMenuContent)
|
||||
@@ -0,0 +1,138 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect } 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'
|
||||
|
||||
// Default Config Imports
|
||||
import { defaultBreakpoints, verticalNavToggleDuration } from '../../defaultConfigs'
|
||||
|
||||
export type VerticalNavProps = HTMLAttributes<HTMLHtmlElement> & {
|
||||
width?: VerticalNavState['width']
|
||||
breakpoint?: BreakpointType
|
||||
customBreakpoint?: string
|
||||
breakpoints?: Partial<typeof defaultBreakpoints>
|
||||
transitionDuration?: VerticalNavState['transitionDuration']
|
||||
backdropColor?: string
|
||||
customStyles?: CSSObject
|
||||
}
|
||||
|
||||
const VerticalNav = (props: VerticalNavProps) => {
|
||||
// Props
|
||||
const {
|
||||
width = 260,
|
||||
breakpoint = 'lg',
|
||||
customBreakpoint,
|
||||
breakpoints,
|
||||
transitionDuration = verticalNavToggleDuration,
|
||||
backdropColor,
|
||||
className,
|
||||
customStyles,
|
||||
children,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
// Vars
|
||||
const mergedBreakpoints = { ...defaultBreakpoints, ...breakpoints }
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
updateVerticalNavState,
|
||||
width: widthContext,
|
||||
isBreakpointReached: isBreakpointReachedContext,
|
||||
isToggled: isToggledContext,
|
||||
transitionDuration: transitionDurationContext
|
||||
} = 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,
|
||||
transitionDuration,
|
||||
isBreakpointReached: breakpointReached
|
||||
})
|
||||
|
||||
if (!breakpointReached) {
|
||||
updateVerticalNavState({ isToggled: false })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [width, breakpointReached, updateVerticalNavState])
|
||||
|
||||
// Handle Backdrop(Content Overlay) Click
|
||||
const handleBackdropClick = () => {
|
||||
// Close the verticalNav
|
||||
updateVerticalNavState({ isToggled: false })
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledVerticalNav
|
||||
width={width}
|
||||
isBreakpointReached={isBreakpointReachedContext}
|
||||
customStyles={customStyles}
|
||||
transitionDuration={transitionDurationContext}
|
||||
className={classnames(
|
||||
verticalNavClasses.root,
|
||||
{
|
||||
[verticalNavClasses.toggled]: isToggledContext,
|
||||
[verticalNavClasses.breakpointReached]: isBreakpointReachedContext
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<StyledVerticalNavContainer
|
||||
width={widthContext}
|
||||
className={verticalNavClasses.container}
|
||||
transitionDuration={transitionDurationContext}
|
||||
>
|
||||
{/* VerticalNav Container to apply styling like background */}
|
||||
<StyledVerticalNavBgColorContainer className={verticalNavClasses.bgColorContainer}>
|
||||
{children}
|
||||
</StyledVerticalNavBgColorContainer>
|
||||
</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
|
||||
Reference in New Issue
Block a user