mirror of
https://github.com/zhigang1992/react.git
synced 2026-03-26 06:55:07 +08:00
feat(collapse): add component
This commit is contained in:
14
components/collapse/collapse-context.ts
Normal file
14
components/collapse/collapse-context.ts
Normal 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)
|
||||
66
components/collapse/collapse-group.tsx
Normal file
66
components/collapse/collapse-group.tsx
Normal 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)
|
||||
23
components/collapse/collapse-icon.tsx
Normal file
23
components/collapse/collapse-icon.tsx
Normal 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
|
||||
125
components/collapse/collapse.tsx
Normal file
125
components/collapse/collapse.tsx
Normal 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>
|
||||
6
components/collapse/index.ts
Normal file
6
components/collapse/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Collapse from './collapse'
|
||||
import CollapseGroup from './collapse-group'
|
||||
|
||||
Collapse.Group = CollapseGroup
|
||||
|
||||
export default Collapse
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
97
pages/docs/components/collapse.mdx
Normal file
97
pages/docs/components/collapse.mdx
Normal 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>
|
||||
Reference in New Issue
Block a user