mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-26 13:25:46 +08:00
feat(table): add component
This commit is contained in:
@@ -42,3 +42,4 @@ export { default as AutoComplete } from './auto-complete'
|
||||
export { default as Collapse } from './collapse'
|
||||
export { default as Loading } from './loading'
|
||||
export { default as Textarea } from './textarea'
|
||||
export { default as Table } from './table'
|
||||
|
||||
6
components/table/index.ts
Normal file
6
components/table/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Table from './table'
|
||||
import TableColumn from './table-column'
|
||||
|
||||
Table.Column = TableColumn
|
||||
|
||||
export default Table
|
||||
85
components/table/table-body.tsx
Normal file
85
components/table/table-body.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import TableCell from './table-cell'
|
||||
import { useTableContext } from './table-context'
|
||||
|
||||
interface Props {
|
||||
hover: boolean
|
||||
emptyText: string
|
||||
onRow: (row: any, index: number) => void
|
||||
onCell: (cell: any, index: number, colunm: number) => void
|
||||
data: Array<any>
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
}
|
||||
|
||||
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
|
||||
export type TableBodyProps = Props & typeof defaultProps & NativeAttrs
|
||||
|
||||
export type cellActions = {
|
||||
remove: Function
|
||||
}
|
||||
|
||||
export type cellData = {
|
||||
row: number,
|
||||
column: number,
|
||||
value: any,
|
||||
}
|
||||
|
||||
const TableBody: React.FC<TableBodyProps> = React.memo(({
|
||||
data, hover, emptyText, onRow, onCell
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const { columns } = useTableContext()
|
||||
const rowClickHandler = (row: any, index: number) => {
|
||||
onRow(row, index)
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{data.map((row, index) => {
|
||||
return (
|
||||
<tr key={`tbody-row-${index}`} className={hover ? 'hover' : ''}
|
||||
onClick={() => rowClickHandler(row, index)}>
|
||||
<TableCell columns={columns}
|
||||
row={row}
|
||||
rowIndex={index}
|
||||
emptyText={emptyText}
|
||||
onCellClick={onCell} />
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<style jsx>{`
|
||||
tr {
|
||||
transition: background-color .25s ease;
|
||||
}
|
||||
|
||||
tr.hover:hover {
|
||||
background-color: ${theme.palette.accents_1};
|
||||
}
|
||||
|
||||
tr :global(td) {
|
||||
padding: 0 ${theme.layout.gapHalf};
|
||||
border-bottom: 1px solid ${theme.palette.border};
|
||||
color: ${theme.palette.accents_6};
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr :global(.cell) {
|
||||
min-height: 3.125rem;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
`}</style>
|
||||
</tbody>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(TableBody, defaultProps)
|
||||
55
components/table/table-cell.tsx
Normal file
55
components/table/table-cell.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
import { TableColumnItem, useTableContext } from './table-context'
|
||||
|
||||
interface Props {
|
||||
columns: Array<TableColumnItem>,
|
||||
row: any,
|
||||
rowIndex: number,
|
||||
emptyText: string,
|
||||
onCellClick: (cell: any, rowIndex: number, colunmIndex: number) => void
|
||||
}
|
||||
|
||||
export type cellActions = {
|
||||
remove: Function
|
||||
}
|
||||
|
||||
export type cellData = {
|
||||
row: number,
|
||||
column: number,
|
||||
rowValue: any,
|
||||
}
|
||||
|
||||
const TableCell: React.FC<Props> = React.memo(({
|
||||
columns, row, rowIndex, emptyText, onCellClick,
|
||||
}) => {
|
||||
const { removeRow } = useTableContext()
|
||||
const actions: cellActions = {
|
||||
remove: () => {
|
||||
removeRow && removeRow(rowIndex)
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{columns.map((column, index) => {
|
||||
const data: cellData = {
|
||||
row: rowIndex,
|
||||
column: index,
|
||||
rowValue: row,
|
||||
}
|
||||
const rowLabel = row[column.value]
|
||||
const cellValue = !rowLabel ? emptyText
|
||||
: (typeof rowLabel === 'function' ? rowLabel(actions, data) : rowLabel)
|
||||
|
||||
return (
|
||||
<td key={`row-td-${index}-${column.value}`}
|
||||
onClick={() => onCellClick(cellValue, rowIndex, index)}>
|
||||
<div className="cell">{cellValue}</div>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default TableCell
|
||||
32
components/table/table-column.tsx
Normal file
32
components/table/table-column.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTableContext } from './table-context'
|
||||
import useWarning from '../utils/use-warning'
|
||||
|
||||
interface Props {
|
||||
prop: string
|
||||
label?: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
export type TableColumnProps = Props
|
||||
|
||||
const TableColumn: React.FC<React.PropsWithChildren<TableColumnProps>> = ({
|
||||
children, prop, label, width,
|
||||
}) => {
|
||||
const { appendColumn } = useTableContext()
|
||||
if (!prop || prop.trim() === '') {
|
||||
useWarning('The props "prop" is required.', 'Table.Column')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
appendColumn && appendColumn({
|
||||
label: children || label,
|
||||
value: `${prop}`.trim(),
|
||||
width,
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default TableColumn
|
||||
21
components/table/table-context.ts
Normal file
21
components/table/table-context.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
export type TableColumnItem = {
|
||||
value: string
|
||||
label: React.ReactNode | string
|
||||
width?: number
|
||||
}
|
||||
|
||||
export interface TableConfig {
|
||||
columns: Array<TableColumnItem>
|
||||
appendColumn?: (column: TableColumnItem) => void
|
||||
removeRow?: (rowIndex: number) => void
|
||||
}
|
||||
|
||||
const defaultContext = {
|
||||
columns: [],
|
||||
}
|
||||
|
||||
export const TableContext = React.createContext<TableConfig>(defaultContext)
|
||||
|
||||
export const useTableContext = (): TableConfig => React.useContext<TableConfig>(TableContext)
|
||||
106
components/table/table-head.tsx
Normal file
106
components/table/table-head.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import { TableColumnItem } from './table-context'
|
||||
|
||||
interface Props {
|
||||
width: number
|
||||
columns: Array<TableColumnItem>
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
}
|
||||
|
||||
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
|
||||
export type TableHeadProps = Props & typeof defaultProps & NativeAttrs
|
||||
|
||||
const makeColgroup = (width: number, columns: Array<TableColumnItem>) => {
|
||||
const unsetWidthCount = columns.filter(c => !c.width).length
|
||||
const customWidthTotal = columns.reduce((pre, current) => {
|
||||
return current.width ? pre + current.width : pre
|
||||
}, 0)
|
||||
const averageWidth = (width - customWidthTotal) / unsetWidthCount
|
||||
if (averageWidth <= 0) return <colgroup />
|
||||
return (
|
||||
<colgroup>
|
||||
{columns.map((column, index) => (
|
||||
<col key={`colgroup-${index}`} width={column.width || averageWidth} />
|
||||
))}
|
||||
</colgroup>
|
||||
)
|
||||
}
|
||||
|
||||
const TableHead: React.FC<TableHeadProps> = React.memo(({
|
||||
columns, width,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const isScalableWidth = useMemo(() => columns.find(item => !!item.width), [columns])
|
||||
const colgroup = useMemo(() => {
|
||||
if (!isScalableWidth) return <colgroup />
|
||||
return makeColgroup(width, columns)
|
||||
}, [isScalableWidth, width])
|
||||
|
||||
return (
|
||||
<>
|
||||
{colgroup}
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th key={`table-th-${column.value}-${index}`}>
|
||||
<div className="thead-box">{column.label}</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<style jsx>{`
|
||||
thead {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 ${theme.layout.gapHalf};
|
||||
font-size: .75rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
letter-spacing: 0;
|
||||
vertical-align: center;
|
||||
min-height: 2.5rem;
|
||||
color: ${theme.palette.accents_5};
|
||||
background: ${theme.palette.accents_1};
|
||||
border-bottom: 1px solid ${theme.palette.border};
|
||||
border-top: 1px solid ${theme.palette.border};
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
th:nth-child(1) {
|
||||
border-bottom: 1px solid ${theme.palette.border};
|
||||
border-left: 1px solid ${theme.palette.border};
|
||||
border-top: 1px solid ${theme.palette.border};
|
||||
border-top-left-radius: ${theme.layout.radius};
|
||||
border-bottom-left-radius: ${theme.layout.radius};
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
border-bottom: 1px solid ${theme.palette.border};
|
||||
border-right: 1px solid ${theme.palette.border};
|
||||
border-top: 1px solid ${theme.palette.border};
|
||||
border-top-right-radius: ${theme.layout.radius};
|
||||
border-bottom-right-radius: ${theme.layout.radius};
|
||||
}
|
||||
|
||||
.thead-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-box-align: center;
|
||||
min-height: 2.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(TableHead, defaultProps)
|
||||
89
components/table/table.tsx
Normal file
89
components/table/table.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import TableColumn from './table-column'
|
||||
import TableHead from './table-head'
|
||||
import TableBody from './table-body'
|
||||
import useRealShape from '../utils/use-real-shape'
|
||||
import useResize from '../utils/use-resize'
|
||||
import { TableContext, TableColumnItem, TableConfig } from './table-context'
|
||||
import useCurrentState from '../utils/use-current-state'
|
||||
|
||||
interface Props {
|
||||
data?: Array<any>
|
||||
emptyText?: string
|
||||
hover?: boolean
|
||||
onRow: (row: any, index: number) => void
|
||||
onCell: (cell: any, index: number, colunm: number) => void
|
||||
onChange: (data: any) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
hover: true,
|
||||
emptyText: '',
|
||||
onRow: () => {},
|
||||
onCell: () => {},
|
||||
onChange: () => {},
|
||||
className: '',
|
||||
}
|
||||
|
||||
type NativeAttrs = Omit<React.TableHTMLAttributes<any>, keyof Props>
|
||||
export type TableProps = Props & typeof defaultProps & NativeAttrs
|
||||
|
||||
const Table: React.FC<React.PropsWithChildren<TableProps>> = ({
|
||||
children, data, hover, emptyText, onRow, onCell, onChange,
|
||||
className, ...props
|
||||
}) => {
|
||||
const ref = useRef<HTMLTableElement>(null)
|
||||
const [{ width }, updateShape] = useRealShape<HTMLTableElement>(ref)
|
||||
const [columns, setColumns, columnsRef] = useCurrentState<Array<TableColumnItem>>([])
|
||||
const [selfData, setSelfData, dataRef] = useCurrentState<Array<TableColumnItem>>([])
|
||||
const appendColumn = (column: TableColumnItem) => {
|
||||
const pureCurrent = columnsRef.current.filter(item => item.value !== column.value)
|
||||
setColumns([...pureCurrent, column])
|
||||
}
|
||||
const removeRow = (rowIndex: number) => {
|
||||
const next = dataRef.current.filter((_, index) => index !== rowIndex)
|
||||
onChange(next)
|
||||
setSelfData([...next])
|
||||
}
|
||||
|
||||
const initialValue = useMemo<TableConfig>(() => ({
|
||||
columns,
|
||||
appendColumn,
|
||||
removeRow,
|
||||
}), [columns])
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
setSelfData(data)
|
||||
}, [data])
|
||||
useResize(() => updateShape())
|
||||
|
||||
return (
|
||||
<TableContext.Provider value={initialValue}>
|
||||
<table ref={ref} className={className} {...props}>
|
||||
<TableHead columns={columns} width={width} />
|
||||
<TableBody data={selfData} hover={hover} emptyText={emptyText}
|
||||
onRow={onRow} onCell={onCell} />
|
||||
{children}
|
||||
|
||||
<style jsx>{`
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`}</style>
|
||||
</table>
|
||||
</TableContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type TableComponent<P = {}> = React.FC<P> & {
|
||||
Column: typeof TableColumn
|
||||
}
|
||||
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
|
||||
|
||||
(Table as TableComponent<ComponentProps>).defaultProps = defaultProps
|
||||
|
||||
export default Table as TableComponent<ComponentProps>
|
||||
147
pages/docs/components/table.mdx
Normal file
147
pages/docs/components/table.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Layout, Playground, Attributes } from 'lib/components'
|
||||
import { Table, Spacer, Code, Text, Button } from 'components'
|
||||
|
||||
export const meta = {
|
||||
title: 'Table',
|
||||
description: 'Table',
|
||||
}
|
||||
|
||||
## Table
|
||||
|
||||
Display tabular data in format.
|
||||
|
||||
<Playground
|
||||
scope={{ Table }}
|
||||
code={`
|
||||
() => {
|
||||
const data = [
|
||||
{ property: 'type', description: 'Content type', type: 'secondary | warning', default: '-' },
|
||||
{ property: 'Component', description: 'DOM element to use', type: 'string', default: '-' },
|
||||
{ property: 'bold', description: 'Bold style', type: 'boolean', default: 'true' },
|
||||
]
|
||||
return (
|
||||
<Table data={data}>
|
||||
<Table.Column prop="property" label="property" />
|
||||
<Table.Column prop="description" label="description" />
|
||||
<Table.Column prop="type" label="type" />
|
||||
<Table.Column prop="default" label="default" />
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
`} />
|
||||
|
||||
<Playground
|
||||
title="compose"
|
||||
scope={{ Table, Code, Text }}
|
||||
code={`
|
||||
() => {
|
||||
const data = [
|
||||
{ property: 'type', description: 'Content type', type: 'secondary | warning', default: '-' },
|
||||
{ property: 'Component', description: 'DOM element to use', type: <Code>string</Code>, default: '-' },
|
||||
{ property: <Text b>bold</Text>, description: 'Bold style', type: <Code>boolean</Code>, default: <Code>true</Code> },
|
||||
]
|
||||
return (
|
||||
<Table data={data}>
|
||||
<Table.Column prop="property" label="property" />
|
||||
<Table.Column prop="description" label="description" />
|
||||
<Table.Column prop="type" label="type" />
|
||||
<Table.Column prop="default" label="default" />
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
`} />
|
||||
|
||||
<Playground
|
||||
title="width"
|
||||
desc="Specifies the `width` of all or part of the columns."
|
||||
scope={{ Table, Code, Text }}
|
||||
code={`
|
||||
() => {
|
||||
const data = [
|
||||
{ property: 'type', description: 'Content type', type: 'secondary | warning', default: '-' },
|
||||
{ property: 'Component', description: 'DOM element to use', type: <Code>string</Code>, default: '-' },
|
||||
{ property: <Text b>bold</Text>, description: 'Bold style', type: <Code>boolean</Code>, default: <Code>true</Code> },
|
||||
]
|
||||
return (
|
||||
<Table data={data}>
|
||||
<Table.Column prop="property" label="property" width={50} />
|
||||
<Table.Column prop="description" label="description" />
|
||||
<Table.Column prop="type" label="type" />
|
||||
<Table.Column prop="default" label="default" />
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
`} />
|
||||
|
||||
<Playground
|
||||
title="actions"
|
||||
scope={{ Table, Text, Button }}
|
||||
code={`
|
||||
() => {
|
||||
const operation = (actions, rowData) => {
|
||||
return <Button type="error" auto size="mini" onClick={() => actions.remove()}>Remove</Button>
|
||||
}
|
||||
const data = [
|
||||
{ property: 'type', description: 'Content type', operation },
|
||||
{ property: 'Component', description: 'DOM element to use', operation },
|
||||
{ property: <Text b>bold</Text>, description: 'Bold style', operation },
|
||||
]
|
||||
return (
|
||||
<Table data={data}>
|
||||
<Table.Column prop="property" label="property" />
|
||||
<Table.Column prop="description" label="description" />
|
||||
<Table.Column prop="operation" label="operation" width={150} />
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
`} />
|
||||
|
||||
<Playground
|
||||
title="custom head"
|
||||
scope={{ Table, Code, Text }}
|
||||
code={`
|
||||
() => {
|
||||
const data = [
|
||||
{ property: 'type', description: 'Content type', type: 'secondary | warning', default: '-' },
|
||||
{ property: 'Component', description: 'DOM element to use', type: <Code>string</Code>, default: '-' },
|
||||
{ property: <Text b>bold</Text>, description: 'Bold style', type: <Code>boolean</Code>, default: <Code>true</Code> },
|
||||
]
|
||||
return (
|
||||
<Table data={data}>
|
||||
<Table.Column prop="property" label="property" />
|
||||
<Table.Column prop="description" label="description" />
|
||||
<Table.Column prop="type">
|
||||
<Code>type</Code>
|
||||
</Table.Column>
|
||||
<Table.Column prop="default">
|
||||
<Text b>default</Text>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
`} />
|
||||
|
||||
<Attributes edit="/pages/docs/components/table.mdx">
|
||||
<Attributes.Title>Table.Props</Attributes.Title>
|
||||
|
||||
| Attribute | Description | Type | Accepted values | Default
|
||||
| ---------- | ---------- | ---- | -------------- | ------ |
|
||||
| **data** | data source | `Array<any>` | - | - |
|
||||
| **emptyText** | displayed text when table's content is empty | `string` | - | - |
|
||||
| **hover** | table's hover effect | `boolean` | - | `true` |
|
||||
| **onRow** | callback row's content by click | `(row: any, index: number) => void` | - | - |
|
||||
| **onCell** | callback cell's content by click | `(cell: any, index: number, colunm: number) => void` | - | - |
|
||||
| **onChange** | data change event | `(data: any) => void` | - | - |
|
||||
| ... | native props | `TableHTMLAttributes` | `'id', 'name', 'className', ...` | - |
|
||||
|
||||
<Attributes.Title>Table.Column.Props</Attributes.Title>
|
||||
|
||||
| Attribute | Description | Type | Accepted values | Default
|
||||
| ---------- | ---------- | ---- | -------------- | ------ |
|
||||
| **prop**(required) | table-column's prop | `string` | - | - |
|
||||
| **label** | table-column's label | `string` | - | - |
|
||||
| **width** | width number (px) | `number` | - | - |
|
||||
|
||||
</Attributes>
|
||||
|
||||
export default ({ children }) => <Layout meta={meta}>{children}</Layout>
|
||||
Reference in New Issue
Block a user