From 5fbf40cd709c09a026de741e387e418b8e71d21b Mon Sep 17 00:00:00 2001 From: lerte smith Date: Mon, 4 Nov 2024 01:17:16 +0800 Subject: [PATCH] refactor: table refactor with react-aria --- apps/docs/src/usages/table.tsx | 134 ++++++--- .../actify/src/components/Table/Table.tsx | 270 +++++------------- .../actify/src/components/Table/TableCell.tsx | 35 +++ .../components/Table/TableCheckboxCell.tsx | 33 +++ .../components/Table/TableColumnHeader.tsx | 62 ++++ .../src/components/Table/TableHeaderRow.tsx | 26 ++ .../actify/src/components/Table/TableRow.tsx | 55 ++++ .../src/components/Table/TableRowGroup.tsx | 28 ++ .../components/Table/TableSelectAllCell.tsx | 26 ++ .../actify/src/components/Table/Tbody.tsx | 16 -- packages/actify/src/components/Table/Td.tsx | 14 - .../actify/src/components/Table/Tfoot.tsx | 16 -- packages/actify/src/components/Table/Th.tsx | 14 - .../actify/src/components/Table/Thead.tsx | 16 -- packages/actify/src/components/Table/Tr.tsx | 14 - packages/actify/src/components/Table/index.ts | 11 +- .../src/components/Table/table.module.css | 40 +++ 17 files changed, 474 insertions(+), 336 deletions(-) create mode 100644 packages/actify/src/components/Table/TableCell.tsx create mode 100644 packages/actify/src/components/Table/TableCheckboxCell.tsx create mode 100644 packages/actify/src/components/Table/TableColumnHeader.tsx create mode 100644 packages/actify/src/components/Table/TableHeaderRow.tsx create mode 100644 packages/actify/src/components/Table/TableRow.tsx create mode 100644 packages/actify/src/components/Table/TableRowGroup.tsx create mode 100644 packages/actify/src/components/Table/TableSelectAllCell.tsx delete mode 100644 packages/actify/src/components/Table/Tbody.tsx delete mode 100644 packages/actify/src/components/Table/Td.tsx delete mode 100644 packages/actify/src/components/Table/Tfoot.tsx delete mode 100644 packages/actify/src/components/Table/Th.tsx delete mode 100644 packages/actify/src/components/Table/Thead.tsx delete mode 100644 packages/actify/src/components/Table/Tr.tsx create mode 100644 packages/actify/src/components/Table/table.module.css diff --git a/apps/docs/src/usages/table.tsx b/apps/docs/src/usages/table.tsx index d925cea6..68cc5e04 100644 --- a/apps/docs/src/usages/table.tsx +++ b/apps/docs/src/usages/table.tsx @@ -1,48 +1,100 @@ -import { Table } from 'actify' +import { + Cell, + Column, + Pagination, + Row, + Table, + TableBody, + TableHeader, + useAsyncList +} from 'actify' + +import { useState } from 'react' + +interface StarWarsChar { + name: string + url: string + [x: string]: string +} export default () => { - const headers = [ - { - text: 'Name', - value: 'name' - }, - { - text: 'Job', - value: 'job' - }, - { - text: 'Employed', - value: 'date' - } - ] + const totalPages = 20 + const [currentPage, setCurrentPage] = useState(1) - const items = [ - { - name: 'John Michael', - job: 'Manager', - date: '23/04/18' - }, - { - name: 'Alexa Liras', - job: 'Developer', - date: '23/04/18' - }, - { - name: 'Laurent Perrier', - job: 'Executive', - date: '19/09/17' - }, - { - name: 'Michael Levi', - job: 'Developer', - date: '24/12/08' + const handlePageChange = (page: number) => { + setCurrentPage(page) + } + + const list = useAsyncList({ + async load({ signal }) { + let res = await fetch('https://swapi.py4e.com/api/people/?search', { + signal + }) + let json = await res.json() + return { + items: json.results + } }, - { - name: 'Richard Gran', - job: 'Manager', - date: '04/10/21' + async sort({ items, sortDescriptor }) { + return { + items: items.sort((a, b) => { + // @ts-ignore + const first = a[sortDescriptor.column] + // @ts-ignore + const second = b[sortDescriptor.column] + + let cmp = + (parseInt(first, 10) || first) < (parseInt(second, 10) || second) + ? -1 + : 1 + if (sortDescriptor.direction === 'descending') { + cmp *= -1 + } + return cmp + }) + } } - ] + }) - return
+ return ( + <> + + } + > + + + Name + + + Height + + + Mass + + + Birth year + + + + {(item) => ( + + {(columnKey) => {item[columnKey]}} + + )} + +
+ + ) } diff --git a/packages/actify/src/components/Table/Table.tsx b/packages/actify/src/components/Table/Table.tsx index 09c5be6e..d1ec7629 100644 --- a/packages/actify/src/components/Table/Table.tsx +++ b/packages/actify/src/components/Table/Table.tsx @@ -1,217 +1,79 @@ 'use client' -import { - PopoverMenu as Menu, - PopoverMenuItem as MenuItem -} from '../PopoverMenu' -import React, { Children, ComponentProps, useState } from 'react' +import { AriaTableProps, useTable } from 'react-aria' +import { SelectionBehavior, useTableState } from 'react-stately' -import { Button } from '../Button' -import { Elevation } from '../Elevation' -import { Icon } from './../Icon' -import { IconButton } from './../Button/IconButton' -import { Tbody } from './Tbody' -import { Td } from './Td' -import { Tfoot } from './Tfoot' -import { Th } from './Th' -import { Thead } from './Thead' -import { Tr } from './Tr' -import _ from 'lodash' -import { tv } from 'tailwind-variants' +import { TableCell } from './TableCell' +import { TableCheckboxCell } from './TableCheckboxCell' +import { TableColumnHeader } from './TableColumnHeader' +import { TableHeaderRow } from './TableHeaderRow' +import type { TableProps } from '@react-types/table' +import { TableRow } from './TableRow' +import { TableRowGroup } from './TableRowGroup' +import { TableSelectAllCell } from './TableSelectAllCell' +import styles from './table.module.css' +import { useRef } from 'react' -const root = tv({ - base: [ - 'w-full', - 'text-sm', - 'text-left', - 'rtl:text-right', - 'bg-surface', - 'overflow-hidden' - ] -}) - -type Headers = Record[] -type Items = Record[] -interface InitialPaginationState { - page: number - pageSize: number -} -interface TableProps extends ComponentProps<'table'> { - headers?: Headers - items?: Items - actions?: boolean - onItemEdit?: (item: any) => void - onItemDelete?: (item: any) => void - pageSizes?: number[] - initialPaginationState?: InitialPaginationState - children?: React.JSX.Element | React.JSX.Element[] +interface TableComponentProps extends AriaTableProps, TableProps { + children: any + paginator?: React.ReactNode + style?: React.CSSProperties + selectionBehavior?: SelectionBehavior } -const Table = (props: TableProps) => { - const { - className, - headers, - items, - actions, - onItemEdit, - onItemDelete, - pageSizes, - initialPaginationState, - children, - ...rest - } = props - const [selectedPageSize, setSelectedPageSize] = useState( - initialPaginationState?.pageSize ?? pageSizes?.[0] ?? 10 - ) - const [pageSizeOpen, setPageSizeOpen] = useState(false) - const [currentPage, setCurrentPage] = useState( - initialPaginationState?.page ?? 0 - ) - - const thead = Children.map(children, (child) => - child?.type?.displayName === 'Thead' ? child : null - ) - const tbody = Children.map(children, (child) => - child?.type?.displayName === 'Tbody' ? child : null - ) - const tfoot = Children.map(children, (child) => - child?.type?.displayName === 'Tfoot' ? child : null - ) - - const hasThead = thead ? thead.length > 0 : false - const hasToby = tbody ? tbody.length > 0 : false - const hasTfoot = tfoot ? tfoot.length > 0 : false +const Table = (props: TableComponentProps) => { + const { paginator, selectionMode, selectionBehavior } = props + const state = useTableState({ + ...props, + showSelectionCheckboxes: + selectionMode === 'multiple' && selectionBehavior !== 'replace' + }) - const paginatedGroups = pageSizes ? _.chunk(items, selectedPageSize) : [items] + const ref = useRef(null) + const { collection } = state + const { gridProps } = useTable(props, state, ref) return ( -
- - - {hasThead ? ( - thead - ) : ( - - - {headers?.map((head) => )} - - {actions && } - - - )} - - {hasToby ? ( - tbody - ) : ( - - {paginatedGroups[currentPage]?.map((item, index) => ( - - {headers?.map(({ value }) => ( - - ))} - {actions && ( - - )} - - ))} - - )} - {hasTfoot ? tfoot : <>} + <> +
{head.text}Actions
-

{item[value]}

-
- onItemDelete?.(item)} - > - delete - - onItemEdit?.(item)} - > - edit - -
+ + {collection.headerRows.map((headerRow) => ( + + {[...headerRow.childNodes].map((column) => + column.props.isSelectionCell ? ( + + ) : ( + + ) + )} + + ))} + + + {[...collection.body.childNodes].map((row) => ( + + {[...row.childNodes].map((cell) => + cell.props.isSelectionCell ? ( + + ) : ( + + ) + )} + + ))} +
- {pageSizes ? ( -
-
-

Rows per page:

-
- - - {pageSizes?.map((size) => ( - { - setSelectedPageSize(size) - setCurrentPage(0) - }} - > - {size} - - ))} - -
- - {currentPage * selectedPageSize + 1}- - {Math.min( - (currentPage + 1) * selectedPageSize, - items?.length ?? 0 - )}{' '} - of {items?.length ?? 0} - - setCurrentPage(currentPage - 1)} - > - chevron_left - - = paginatedGroups.length - 1} - onClick={() => setCurrentPage(currentPage + 1)} - > - chevron_right - -
-
- ) : ( - <> - )} -
+ {paginator} + ) } Table.displayName = 'Actify.Table' - -export default Object.assign(Table, { - Thead, - Tbody, - Tfoot, - Th, - Tr, - Td -}) +export { Table } diff --git a/packages/actify/src/components/Table/TableCell.tsx b/packages/actify/src/components/Table/TableCell.tsx new file mode 100644 index 00000000..04b7cd6e --- /dev/null +++ b/packages/actify/src/components/Table/TableCell.tsx @@ -0,0 +1,35 @@ +import { + AriaTableCellProps, + mergeProps, + useFocusRing, + useTableCell +} from 'react-aria' + +import { FocusRing } from '../FocusRing/FocusRing' +import { GridNode } from '@react-types/grid' +import React from 'react' +import { TableState } from 'react-stately' +import styles from './table.module.css' + +interface TableCellProps extends AriaTableCellProps { + state: TableState +} + +const TableCell = ({ node, state }: TableCellProps) => { + const ref = React.useRef(null) + const { gridCellProps } = useTableCell({ node }, state, ref) + const { isFocusVisible, focusProps } = useFocusRing() + + return ( + + {node.rendered} + {isFocusVisible && } + + ) +} + +export { TableCell } diff --git a/packages/actify/src/components/Table/TableCheckboxCell.tsx b/packages/actify/src/components/Table/TableCheckboxCell.tsx new file mode 100644 index 00000000..ba0f10fb --- /dev/null +++ b/packages/actify/src/components/Table/TableCheckboxCell.tsx @@ -0,0 +1,33 @@ +import { + AriaTableCellProps, + Key, + useTableCell, + useTableSelectionCheckbox +} from 'react-aria' + +import { Checkbox } from './../Checkbox' +import React from 'react' +import { TableState } from 'react-stately' + +interface TableCheckboxCellProps extends AriaTableCellProps { + state: TableState +} +const TableCheckboxCell = ({ + node, + state +}: TableCheckboxCellProps) => { + const ref = React.useRef(null) + const { gridCellProps } = useTableCell({ node }, state, ref) + const { checkboxProps } = useTableSelectionCheckbox( + { key: node.parentKey as Key }, + state + ) + + return ( + + + + ) +} + +export { TableCheckboxCell } diff --git a/packages/actify/src/components/Table/TableColumnHeader.tsx b/packages/actify/src/components/Table/TableColumnHeader.tsx new file mode 100644 index 00000000..9a818547 --- /dev/null +++ b/packages/actify/src/components/Table/TableColumnHeader.tsx @@ -0,0 +1,62 @@ +import { mergeProps, useFocusRing, useTableColumnHeader } from 'react-aria' + +import { FocusRing } from '../FocusRing' +import { GridNode } from '@react-types/grid' +import { Icon } from '../Icon' +import React from 'react' +import { TableState } from 'react-stately' +import styles from './table.module.css' + +interface TableColumnHeaderProps extends React.ComponentProps<'th'> { + column: GridNode + state: TableState +} +const TableColumnHeader = ({ + column, + state +}: TableColumnHeaderProps) => { + const ref = React.useRef(null) + const { columnHeaderProps } = useTableColumnHeader( + { node: column }, + state, + ref + ) + const isSortVisible = state.sortDescriptor?.column === column.key + const { isFocusVisible, focusProps } = useFocusRing() + + return ( + 1 ? 'center' : 'left', + cursor: column.props.allowsSorting ? 'pointer' : 'default' + }} + {...mergeProps(columnHeaderProps, focusProps)} + > + {isFocusVisible && } + {column.rendered} + {column.props.allowsSorting && ( + + )} + + ) +} + +export { TableColumnHeader } diff --git a/packages/actify/src/components/Table/TableHeaderRow.tsx b/packages/actify/src/components/Table/TableHeaderRow.tsx new file mode 100644 index 00000000..8efefe1c --- /dev/null +++ b/packages/actify/src/components/Table/TableHeaderRow.tsx @@ -0,0 +1,26 @@ +import { GridNode } from '@react-types/grid' +import React from 'react' +import { TableState } from 'react-stately' +import { useTableHeaderRow } from 'react-aria' + +interface TableHeaderRowProps extends React.ComponentProps<'tr'> { + item: GridNode + state: TableState +} + +const TableHeaderRow = ({ + item, + state, + children +}: TableHeaderRowProps) => { + const ref = React.useRef(null) + const { rowProps } = useTableHeaderRow({ node: item }, state, ref) + + return ( + + {children} + + ) +} + +export { TableHeaderRow } diff --git a/packages/actify/src/components/Table/TableRow.tsx b/packages/actify/src/components/Table/TableRow.tsx new file mode 100644 index 00000000..39f5580b --- /dev/null +++ b/packages/actify/src/components/Table/TableRow.tsx @@ -0,0 +1,55 @@ +import { + GridRowProps, + mergeProps, + useFocusRing, + useHover, + useTableRow +} from 'react-aria' + +import { FocusRing } from '../FocusRing' +import React from 'react' +import { TableState } from 'react-stately' +import clsx from 'clsx' +import styles from './table.module.css' + +interface TableRowProps extends GridRowProps { + state: TableState + children: React.ReactNode +} + +const TableRow = (props: TableRowProps) => { + const { node, children, state } = props + const ref = React.useRef(null) + const isDisabled = state.selectionManager.isDisabled(node.key) + const isSelected = state.selectionManager.isSelected(node.key) + + const { rowProps, isPressed } = useTableRow( + { + node + }, + state, + ref + ) + const { isHovered, hoverProps } = useHover({ isDisabled }) + const { isFocusVisible, focusProps } = useFocusRing() + + return ( + + {children} + {isFocusVisible && } + + ) +} + +export { TableRow } diff --git a/packages/actify/src/components/Table/TableRowGroup.tsx b/packages/actify/src/components/Table/TableRowGroup.tsx new file mode 100644 index 00000000..750abcb1 --- /dev/null +++ b/packages/actify/src/components/Table/TableRowGroup.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { useTableRowGroup } from 'react-aria' + +type TableRowGroupProps = { + className?: string + style?: React.CSSProperties + type?: keyof HTMLElementTagNameMap + children: React.ReactNode +} +const TableRowGroup = ({ type = 'thead', children }: TableRowGroupProps) => { + const { rowGroupProps } = useTableRowGroup() + const Element = type + return ( + + {children} + + ) +} + +export { TableRowGroup } diff --git a/packages/actify/src/components/Table/TableSelectAllCell.tsx b/packages/actify/src/components/Table/TableSelectAllCell.tsx new file mode 100644 index 00000000..7cc4e06e --- /dev/null +++ b/packages/actify/src/components/Table/TableSelectAllCell.tsx @@ -0,0 +1,26 @@ +import { + VisuallyHidden, + useTableColumnHeader, + useTableSelectAllCheckbox +} from 'react-aria' + +import { Checkbox } from '../Checkbox' +import React from 'react' + +const TableSelectAllCell = ({ column, state }) => { + let ref = React.useRef(null) + let { columnHeaderProps } = useTableColumnHeader({ node: column }, state, ref) + let { checkboxProps } = useTableSelectAllCheckbox(state) + + return ( + + {state.selectionManager.selectionMode === 'single' ? ( + {checkboxProps['aria-label']} + ) : ( + + )} + + ) +} + +export { TableSelectAllCell } diff --git a/packages/actify/src/components/Table/Tbody.tsx b/packages/actify/src/components/Table/Tbody.tsx deleted file mode 100644 index 0ade1742..00000000 --- a/packages/actify/src/components/Table/Tbody.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import React from 'react' -import { tv } from 'tailwind-variants' - -const root = tv({ - base: '' -}) - -const Tbody = ({ className, children }: React.ComponentProps<'tbody'>) => { - return {children} -} - -Tbody.displayName = 'Tbody' - -export { Tbody } diff --git a/packages/actify/src/components/Table/Td.tsx b/packages/actify/src/components/Table/Td.tsx deleted file mode 100644 index 24ed865e..00000000 --- a/packages/actify/src/components/Table/Td.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' - -import React from 'react' -import { tv } from 'tailwind-variants' - -const root = tv({ - base: 'px-4' -}) - -const Td = ({ className, children }: React.ComponentProps<'td'>) => { - return {children} -} - -export { Td } diff --git a/packages/actify/src/components/Table/Tfoot.tsx b/packages/actify/src/components/Table/Tfoot.tsx deleted file mode 100644 index 8a984190..00000000 --- a/packages/actify/src/components/Table/Tfoot.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import React from 'react' -import { tv } from 'tailwind-variants' - -const root = tv({ - base: 'border-t border-outline-variant text-xs bg-inverse-surface/25' -}) - -const Tfoot = ({ className, children }: React.ComponentProps<'tfoot'>) => { - return {children} -} - -Tfoot.displayName = 'Tfoot' - -export { Tfoot } diff --git a/packages/actify/src/components/Table/Th.tsx b/packages/actify/src/components/Table/Th.tsx deleted file mode 100644 index c815d79e..00000000 --- a/packages/actify/src/components/Table/Th.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' - -import React from 'react' -import { tv } from 'tailwind-variants' - -const root = tv({ - base: 'px-4' -}) - -const Th = ({ className, children }: React.ComponentProps<'th'>) => { - return {children} -} - -export { Th } diff --git a/packages/actify/src/components/Table/Thead.tsx b/packages/actify/src/components/Table/Thead.tsx deleted file mode 100644 index 8dcc44e7..00000000 --- a/packages/actify/src/components/Table/Thead.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import React from 'react' -import { tv } from 'tailwind-variants' - -const root = tv({ - base: 'border-b text-xs bg-surface' -}) - -const Thead = ({ className, children }: React.ComponentProps<'thead'>) => { - return {children} -} - -Thead.displayName = 'Thead' - -export { Thead } diff --git a/packages/actify/src/components/Table/Tr.tsx b/packages/actify/src/components/Table/Tr.tsx deleted file mode 100644 index 9fa1f1a3..00000000 --- a/packages/actify/src/components/Table/Tr.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' - -import React from 'react' -import { tv } from 'tailwind-variants' - -const root = tv({ - base: '' -}) - -const Tr = ({ className, children }: React.ComponentProps<'tr'>) => { - return {children} -} - -export { Tr } diff --git a/packages/actify/src/components/Table/index.ts b/packages/actify/src/components/Table/index.ts index 48b14b36..8659367c 100644 --- a/packages/actify/src/components/Table/index.ts +++ b/packages/actify/src/components/Table/index.ts @@ -1 +1,10 @@ -export { default as Table } from './Table' +export { Table } from './Table' + +export { + Cell, + Column, + Row, + TableBody, + TableHeader, + useAsyncList +} from 'react-stately' diff --git a/packages/actify/src/components/Table/table.module.css b/packages/actify/src/components/Table/table.module.css new file mode 100644 index 00000000..f29b35c7 --- /dev/null +++ b/packages/actify/src/components/Table/table.module.css @@ -0,0 +1,40 @@ +.table { + width: 100%; + position: relative; + border-collapse: collapse; + background-color: rgb(var(--md-sys-color-surface)); + box-shadow: + rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, + rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, + rgba(0, 0, 0, 0.12) 0px 3px 14px 2px; +} +.th:focus-visible, +.tr:focus-visible, +.td:focus-visible { + outline: none; +} + +.th { + position: relative; + padding: 0 1rem; +} +.tr { + position: relative; + height: 52px; + border-style: solid; + border-top-width: 1px; + border-color: rgb( + var(--md-sys-color-outline-variant) / var(--tw-border-opacity) + ); +} +.tr.hovered { + background-color: rgb(var(--md-sys-color-inverse-surface) / 0.1); +} +.tr.selected { + color: rgb(var(--md-sys-color-on-secondary-container)); + background-color: rgb(var(--md-sys-color-secondary-container)); +} +.td { + position: relative; + padding: 0 1rem; +}