mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-24 04:15:54 +08:00
feat(table): add component
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user