import React, { ElementType, forwardRef, HTMLAttributes, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { CBackdrop } from '../backdrop'
import { isInViewport } from '../../utils'
import { useForkedRef } from '../../hooks'
import { PolymorphicRefForwardingComponent } from '../../helpers'
export interface CSidebarProps extends HTMLAttributes {
/**
* Component used for the root node. Either a string to use a HTML element or a component.
*/
as?: ElementType
/**
* A string of all className you want applied to the component.
*/
className?: string
/**
* Sets if the color of text should be colored for a light or dark dark background.
*
* @type 'dark' | 'light'
*/
colorScheme?: 'dark' | 'light'
/**
* Make sidebar narrow.
*/
narrow?: boolean
/**
* Callback fired when the component requests to be hidden.
*/
onHide?: () => void
/**
* Callback fired when the component requests to be shown.
*/
onShow?: () => void
/**
* Event emitted after visibility of component changed.
*/
onVisibleChange?: (visible: boolean) => void
/**
* Set sidebar to overlaid variant.
*/
overlaid?: boolean
/**
* Components placement, there’s no default placement.
* @type 'start' | 'end'
*/
placement?: 'start' | 'end'
/**
* Place sidebar in non-static positions.
*/
position?: 'fixed' | 'sticky'
/**
* Size the component small, large, or extra large.
*/
size?: 'sm' | 'lg' | 'xl'
/**
* Expand narrowed sidebar on hover.
*/
unfoldable?: boolean
/**
* Toggle the visibility of sidebar component.
*/
visible?: boolean
}
const isOnMobile = (element: HTMLDivElement) =>
Boolean(getComputedStyle(element).getPropertyValue('--cui-is-mobile'))
export const CSidebar: PolymorphicRefForwardingComponent<'div', CSidebarProps> = forwardRef<
HTMLDivElement,
CSidebarProps
>(
(
{
children,
as: Component = 'div',
className,
colorScheme,
narrow,
onHide,
onShow,
onVisibleChange,
overlaid,
placement,
position,
size,
unfoldable,
visible,
...rest
},
ref,
) => {
const sidebarRef = useRef(null)
const forkedRef = useForkedRef(ref, sidebarRef)
const [inViewport, setInViewport] = useState()
const [mobile, setMobile] = useState(false)
const [visibleMobile, setVisibleMobile] = useState(false)
const [visibleDesktop, setVisibleDesktop] = useState(
visible !== undefined ? visible : overlaid ? false : true,
)
useEffect(() => {
sidebarRef.current && setMobile(isOnMobile(sidebarRef.current))
visible !== undefined && handleVisibleChange(visible)
}, [visible])
useEffect(() => {
inViewport !== undefined && onVisibleChange && onVisibleChange(inViewport)
!inViewport && onHide && onHide()
inViewport && onShow && onShow()
}, [inViewport])
useEffect(() => {
mobile && setVisibleMobile(false)
}, [mobile])
useEffect(() => {
sidebarRef.current && setMobile(isOnMobile(sidebarRef.current))
sidebarRef.current && setInViewport(isInViewport(sidebarRef.current))
window.addEventListener('resize', handleResize)
window.addEventListener('mouseup', handleClickOutside)
window.addEventListener('keyup', handleKeyup)
sidebarRef.current?.addEventListener('mouseup', handleOnClick)
sidebarRef.current?.addEventListener('transitionend', () => {
sidebarRef.current && setInViewport(isInViewport(sidebarRef.current))
})
return () => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('mouseup', handleClickOutside)
window.removeEventListener('keyup', handleKeyup)
sidebarRef.current?.removeEventListener('mouseup', handleOnClick)
sidebarRef.current?.removeEventListener('transitionend', () => {
sidebarRef.current && setInViewport(isInViewport(sidebarRef.current))
})
}
})
const handleVisibleChange = (visible: boolean) => {
if (mobile) {
setVisibleMobile(visible)
return
}
setVisibleDesktop(visible)
}
const handleHide = () => {
handleVisibleChange(false)
}
const handleResize = () => {
sidebarRef.current && setMobile(isOnMobile(sidebarRef.current))
sidebarRef.current && setInViewport(isInViewport(sidebarRef.current))
}
const handleKeyup = (event: Event) => {
if (
mobile &&
sidebarRef.current &&
!sidebarRef.current.contains(event.target as HTMLElement)
) {
handleHide()
}
}
const handleClickOutside = (event: Event) => {
if (
mobile &&
sidebarRef.current &&
!sidebarRef.current.contains(event.target as HTMLElement)
) {
handleHide()
}
}
const handleOnClick = (event: Event) => {
const target = event.target as HTMLAnchorElement
target &&
target.classList.contains('nav-link') &&
!target.classList.contains('nav-group-toggle') &&
mobile &&
handleHide()
}
return (
<>
{children}
{typeof window !== 'undefined' &&
mobile &&
createPortal(
,
document.body,
)}
>
)
},
)
CSidebar.propTypes = {
as: PropTypes.elementType,
children: PropTypes.node,
className: PropTypes.string,
colorScheme: PropTypes.oneOf(['dark', 'light']),
narrow: PropTypes.bool,
onHide: PropTypes.func,
onShow: PropTypes.func,
onVisibleChange: PropTypes.func,
overlaid: PropTypes.bool,
placement: PropTypes.oneOf(['start', 'end']),
position: PropTypes.oneOf(['fixed', 'sticky']),
size: PropTypes.oneOf(['sm', 'lg', 'xl']),
unfoldable: PropTypes.bool,
visible: PropTypes.bool,
}
CSidebar.displayName = 'CSidebar'