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

View File

@@ -0,0 +1,97 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Collapse, Spacer, Text } from 'components'
export const meta = {
title: 'collapse',
description: 'collapse',
}
## Collapse
Display large amounts of text in collapsible sections. Commonly referred to as an accordion.
<Playground
scope={{ Collapse, Text }}
code={`
<Collapse.Group>
<Collapse title="Question A">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
<Collapse title="Question B">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
</Collapse.Group>
`} />
<Playground
title="Expanded"
scope={{ Collapse, Text }}
code={`
<Collapse.Group>
<Collapse title="Question A" initialVisible>
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
<Collapse title="Question B">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
</Collapse.Group>
`} />
<Playground
title="Subtitle"
scope={{ Collapse, Text }}
code={`
<Collapse.Group>
<Collapse title="Question A" subtitle="More description about Question A">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
<Collapse title="Question B">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
</Collapse.Group>
`} />
<Playground
title="Shadow"
scope={{ Collapse, Text }}
code={`
<Collapse shadow title="Question A" subtitle="More description about Question A">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</Text>
</Collapse>
`} />
<Attributes edit="/pages/docs/components/collapse.mdx">
<Attributes.Title>Collapse.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **title**(required) | collapse title | `string` | - | - |
| **subtitle** | description | `string` | - | - |
| **initialVisible** | initial expanded | `boolean` | - | `false` |
| **shadow** | show shadow card | `boolean` | - | `false` |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
<Attributes.Title>Collapse.Group.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **accordion** | accordion mode | `boolean` | - | `true` |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>