add front-urban

This commit is contained in:
Valentin Romero
2026-06-30 22:08:11 -03:00
parent 25d4ba6cc0
commit 03294769a4
266 changed files with 38239 additions and 0 deletions
@@ -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