packages/react/src/panels/CompositePanel.tsx (182 lines of code) (raw):
import React, { useEffect, useRef, useState } from 'react'
import { isValid } from '@designable/shared'
import cls from 'classnames'
import { IconWidget, TextWidget } from '../widgets'
import { usePrefix } from '../hooks'
export interface ICompositePanelProps {
direction?: 'left' | 'right'
showNavTitle?: boolean
defaultOpen?: boolean
defaultPinning?: boolean
defaultActiveKey?: number
activeKey?: number | string
onChange?: (activeKey: number | string) => void
}
export interface ICompositePanelItemProps {
shape?: 'tab' | 'button' | 'link'
title?: React.ReactNode
icon?: React.ReactNode
key?: number | string
href?: string
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
extra?: React.ReactNode
}
const parseItems = (
children: React.ReactNode
): React.PropsWithChildren<ICompositePanelItemProps>[] => {
const items = []
React.Children.forEach(children, (child, index) => {
if (child?.['type'] === CompositePanel.Item) {
items.push({ key: child['key'] ?? index, ...child['props'] })
}
})
return items
}
const findItem = (
items: React.PropsWithChildren<ICompositePanelItemProps>[],
key: string | number
) => {
for (let index = 0; index < items.length; index++) {
const item = items[index]
if (key === index) return item
if (key === item.key) return item
}
}
const getDefaultKey = (children: React.ReactNode) => {
const items = parseItems(children)
return items?.[0].key
}
export const CompositePanel: React.FC<ICompositePanelProps> & {
Item: React.FC<ICompositePanelItemProps>
} = (props) => {
const prefix = usePrefix('composite-panel')
const [activeKey, setActiveKey] = useState<string | number>(
props.defaultActiveKey ?? getDefaultKey(props.children)
)
const activeKeyRef = useRef(null)
const [pinning, setPinning] = useState(props.defaultPinning ?? false)
const [visible, setVisible] = useState(props.defaultOpen ?? true)
const items = parseItems(props.children)
const currentItem = findItem(items, activeKey)
const content = currentItem?.children
activeKeyRef.current = activeKey
useEffect(() => {
if (isValid(props.activeKey)) {
if (props.activeKey !== activeKeyRef.current) {
setActiveKey(props.activeKey)
}
}
}, [props.activeKey])
const renderContent = () => {
if (!content || !visible) return
return (
<div
className={cls(prefix + '-tabs-content', {
pinning,
})}
>
<div className={prefix + '-tabs-header'}>
<div className={prefix + '-tabs-header-title'}>
<TextWidget>{currentItem.title}</TextWidget>
</div>
<div className={prefix + '-tabs-header-actions'}>
<div className={prefix + '-tabs-header-extra'}>
{currentItem.extra}
</div>
{!pinning && (
<IconWidget
infer="PushPinOutlined"
className={prefix + '-tabs-header-pin'}
onClick={() => {
setPinning(!pinning)
}}
/>
)}
{pinning && (
<IconWidget
infer="PushPinFilled"
className={prefix + '-tabs-header-pin-filled'}
onClick={() => {
setPinning(!pinning)
}}
/>
)}
<IconWidget
infer="Close"
className={prefix + '-tabs-header-close'}
onClick={() => {
setVisible(false)
}}
/>
</div>
</div>
<div className={prefix + '-tabs-body'}>{content}</div>
</div>
)
}
return (
<div
className={cls(prefix, {
[`direction-${props.direction}`]: !!props.direction,
})}
>
<div className={prefix + '-tabs'}>
{items.map((item, index) => {
const takeTab = () => {
if (item.href) {
return <a href={item.href}>{item.icon}</a>
}
return (
<IconWidget
tooltip={
props.showNavTitle
? null
: {
title: <TextWidget>{item.title}</TextWidget>,
placement:
props.direction === 'right' ? 'left' : 'right',
}
}
infer={item.icon}
/>
)
}
const shape = item.shape ?? 'tab'
const Comp = shape === 'link' ? 'a' : 'div'
return (
<Comp
className={cls(prefix + '-tabs-pane', {
active: activeKey === item.key,
})}
key={index}
href={item.href}
onClick={(e: any) => {
if (shape === 'tab') {
if (activeKey === item.key) {
setVisible(!visible)
} else {
setVisible(true)
}
if (!props?.activeKey || !props?.onChange)
setActiveKey(item.key)
}
item.onClick?.(e)
props.onChange?.(item.key)
}}
>
{takeTab()}
{props.showNavTitle && item.title ? (
<div className={prefix + '-tabs-pane-title'}>
<TextWidget>{item.title}</TextWidget>
</div>
) : null}
</Comp>
)
})}
</div>
{renderContent()}
</div>
)
}
CompositePanel.Item = () => {
return <React.Fragment />
}