feat(collapse): add component

This commit is contained in:
unix
2020-03-29 04:00:10 +08:00
parent d00007628d
commit 1c40cdbc12
8 changed files with 354 additions and 1 deletions

View File

@@ -0,0 +1,14 @@
import React from 'react'
export interface CollapseConfig {
values: Array<number>
updateValues?: Function
}
const defaultContext = {
values: [],
}
export const CollapseContext = React.createContext<CollapseConfig>(defaultContext)
export const useCollapseContext = (): CollapseConfig => React.useContext<CollapseConfig>(CollapseContext)

View File

@@ -0,0 +1,66 @@
import React, { useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import { CollapseContext, CollapseConfig } from './collapse-context'
import useCurrentState from '../utils/use-current-state'
import { setChildrenIndex } from '../utils/collections'
import Collapse from './collapse'
interface Props {
accordion?: boolean
className?: string
}
const defaultProps = {
accordion: true,
className: '',
}
export type CollapseGroupProps = Props & typeof defaultProps & React.HTMLAttributes<any>
const CollapseGroup: React.FC<React.PropsWithChildren<CollapseGroupProps>> = ({
children, accordion, className, ...props
}) => {
const theme = useTheme()
const [state, setState, stateRef] = useCurrentState<Array<number>>([])
const updateValues = (currentIndex: number, nextState: boolean) => {
const hasChild = stateRef.current.find(val => val === currentIndex)
if (accordion) {
if (nextState) return setState([currentIndex])
return setState([])
}
if (nextState) {
if (hasChild) return
return setState([...stateRef.current, currentIndex])
}
setState(stateRef.current.filter(item => item !== currentIndex))
}
const initialValue = useMemo<CollapseConfig>(() => ({
values: state,
updateValues,
}), [state.join(',')])
const hasIndexChildren = useMemo(() => setChildrenIndex(children, [Collapse]), [children])
return (
<CollapseContext.Provider value={initialValue}>
<div className={`collapse-group ${className}`} {...props}>
{hasIndexChildren}
<style jsx>{`
.collapse-group {
width: auto;
padding: 0 ${theme.layout.gapHalf};
}
.collapse-group > :global(div+div) {
border-top: none;
}
`}</style>
</div>
</CollapseContext.Provider>
)
}
export default withDefaults(CollapseGroup, defaultProps)

View File

@@ -0,0 +1,23 @@
import React from 'react'
interface Props {
active?: boolean
}
const CollapseIcon: React.FC<Props> = ({ active }) => {
return (
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
strokeLinejoin="round" fill="none" shapeRendering="geometricPrecision" style={{ color: 'currentColor' }}>
<path d="M6 9l6 6 6-6" />
<style jsx>{`
svg {
transition: transform 200ms ease;
transform: rotateZ(${active ? '-180deg' : '0'});
}
`}</style>
</svg>
)
}
export default CollapseIcon

View File

@@ -0,0 +1,125 @@
import React, { useEffect } from 'react'
import CollapseIcon from './collapse-icon'
import useTheme from '../styles/use-theme'
import Expand from '../shared/expand'
import { useCollapseContext } from './collapse-context'
import useCurrentState from '../utils/use-current-state'
import CollapseGroup from './collapse-group'
interface Props {
title: string
subtitle?: React.ReactNode | string
initialVisible?: boolean
shadow?: boolean
className?: string
index?: number
}
const defaultProps = {
className: '',
shadow: false,
initialVisible: false,
}
export type CollapseProps = Props & typeof defaultProps & React.HTMLAttributes<any>
const Collapse: React.FC<React.PropsWithChildren<CollapseProps>> = ({
children, title, subtitle, initialVisible, shadow, className,
index, ...props
}) => {
const theme = useTheme()
const [visible, setVisible, visibleRef] = useCurrentState<boolean>(initialVisible)
const { values, updateValues } = useCollapseContext()
useEffect(() => {
if (!values.length) return
const isActive = !!values.find(item => item === index)
setVisible(isActive)
}, [values.join(',')])
const clickHandler = () => {
const next = !visibleRef.current
setVisible(next)
updateValues && updateValues(index, next)
}
return (
<div className={`collapse ${shadow ? 'shadow' : ''} ${className}`} {...props}>
<div className="view" role="button" onClick={clickHandler}>
<div className="title">
<h3>{title}</h3> <CollapseIcon active={visible} />
</div>
{subtitle && <div className="subtitle">{subtitle}</div>}
</div>
<Expand isExpanded={visible}>
<div className="content">
{children}
</div>
</Expand>
<style jsx>{`
.collapse {
padding: ${theme.layout.gap} 0;
border-top: 1px solid ${theme.palette.border};
border-bottom: 1px solid ${theme.palette.border};
}
.shadow {
box-shadow: ${theme.expressiveness.shadowSmall};
border: none;
border-radius: ${theme.layout.radius};
padding: ${theme.layout.gap};
}
.view {
cursor: pointer;
outline: none;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
color: ${theme.palette.foreground};
}
.title h3 {
margin: 0;
}
.subtitle {
color: ${theme.palette.accents_5};
margin: 0;
}
.subtitle > :global(*) {
margin: 0;
}
.content {
font-size: 1rem;
line-height: 1.625rem;
padding: ${theme.layout.gap} 0;
}
.content > :global(*:first-child) {
margin-top: 0;
}
.content > :global(*:last-child) {
margin-bottom: 0;
}
`}</style>
</div>
)
}
Collapse.defaultProps = defaultProps
type CollapseComponent<P = {}> = React.FC<P> & {
Group: typeof CollapseGroup
}
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
export default Collapse as CollapseComponent<ComponentProps>

View File

@@ -0,0 +1,6 @@
import Collapse from './collapse'
import CollapseGroup from './collapse-group'
Collapse.Group = CollapseGroup
export default Collapse

View File

@@ -38,3 +38,4 @@ export { default as Progress } from './progress'
export { default as Tree } from './file-tree'
export { default as Badge } from './badge'
export { default as AutoComplete } from './auto-complete'
export { default as Collapse } from './collapse'

View File

@@ -65,7 +65,7 @@ export const pickChildrenFirst = (
export const setChildrenProps = (
children: ReactNode | undefined,
props: object = {},
targetComponents: Array<React.ElementType> = []
targetComponents: Array<React.ElementType> = [],
): ReactNode | undefined => {
if (React.Children.count(children) === 0) return []
const allowAll = targetComponents.length === 0
@@ -81,6 +81,27 @@ export const setChildrenProps = (
})
}
export const setChildrenIndex = (
children: ReactNode | undefined,
targetComponents: Array<React.ElementType> = [],
): ReactNode | undefined => {
if (React.Children.count(children) === 0) return []
const allowAll = targetComponents.length === 0
const clone = (child: React.ReactElement, props = {}) => React.cloneElement(child, props)
let index = 0
return React.Children.map(children, item => {
if (!React.isValidElement(item)) return item
index = index + 1
if (allowAll) return clone(item, { index })
const isAllowed = targetComponents.find(child => child === item.type)
if (isAllowed) return clone(item, { index })
index = index - 1
return item
})
}
export type ShapeType = {
width: number
height: number