From 58c029ba2ed15b386f65e8b65be7dac58f912141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=B5=E3=81=BF?= Date: Tue, 9 Jan 2024 17:31:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(Table):=20=E3=83=AD=E3=83=BC=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0=20(#1487?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add loading props for table * Add docs * Add change log --- .changeset/perfect-ties-taste.md | 5 + packages/for-ui/src/table/ColumnDef.ts | 4 +- packages/for-ui/src/table/Table.stories.tsx | 20 +- packages/for-ui/src/table/Table.tsx | 236 ++++++++++++------ packages/for-ui/src/table/TableCell.tsx | 4 +- packages/for-ui/src/table/TablePagination.tsx | 6 + 6 files changed, 191 insertions(+), 84 deletions(-) create mode 100644 .changeset/perfect-ties-taste.md diff --git a/.changeset/perfect-ties-taste.md b/.changeset/perfect-ties-taste.md new file mode 100644 index 000000000..282824831 --- /dev/null +++ b/.changeset/perfect-ties-taste.md @@ -0,0 +1,5 @@ +--- +"@4design/for-ui": patch +--- + +feat(Table): ローディングを追加 diff --git a/packages/for-ui/src/table/ColumnDef.ts b/packages/for-ui/src/table/ColumnDef.ts index c4a6b5c7c..ad9fa4b42 100644 --- a/packages/for-ui/src/table/ColumnDef.ts +++ b/packages/for-ui/src/table/ColumnDef.ts @@ -1,3 +1 @@ -import type { ColumnDef } from '@tanstack/react-table'; - -export { ColumnDef }; +export type { ColumnDef } from '@tanstack/react-table'; diff --git a/packages/for-ui/src/table/Table.stories.tsx b/packages/for-ui/src/table/Table.stories.tsx index 71cb84f32..67e4d1438 100644 --- a/packages/for-ui/src/table/Table.stories.tsx +++ b/packages/for-ui/src/table/Table.stories.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { MdMoreVert, MdOutlineDelete, MdOutlineEdit } from 'react-icons/md'; import { Meta, Story } from '@storybook/react/types-6-0'; import { Badge } from '../badge'; @@ -18,11 +18,13 @@ export default { component: Table, } as Meta; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const columns: ColumnDef[] = [ +const columns: ColumnDef[] = [ { header: 'ID', accessorKey: 'id', + meta: { + width: '16px', + }, cell: (cell) => {cell.renderValue()}, }, { @@ -44,6 +46,18 @@ const columns: ColumnDef[] = [ export const Base: Story = () => columns={columns} data={StaticPersonData} />; +export const Loading: Story = () => loading loadingRows={20} columns={columns} />; + +export const LoadingWithSelect: Story = () => ( + + loading + loadingRows={10} + columns={columns} + getRowId={(row) => row.id.toString()} + onSelectRow={(row) => console.info('Selected row: ', row)} + /> +); + export const WithSelect: Story = () => ( columns={columns} diff --git a/packages/for-ui/src/table/Table.tsx b/packages/for-ui/src/table/Table.tsx index eaa93b3e1..2adcb2bb2 100644 --- a/packages/for-ui/src/table/Table.tsx +++ b/packages/for-ui/src/table/Table.tsx @@ -11,7 +11,6 @@ import { useState, } from 'react'; import { - ColumnDef, ColumnSort, flexRender, getCoreRowModel, @@ -28,12 +27,14 @@ import { } from '@tanstack/react-table'; import { Checkbox } from '../checkbox'; import { Radio } from '../radio'; +import { Skeleton } from '../skeleton'; import { fsx } from '../system/fsx'; import { Text } from '../text'; +import { ColumnDef } from './ColumnDef'; import { SortableTableCellHead, TableCell } from './TableCell'; import { TablePagination } from './TablePagination'; -export type TableProps = Pick, 'data' | 'columns' | 'getRowId'> & { +export type TableProps = Pick, 'columns' | 'getRowId'> & { disablePagination?: boolean | undefined; defaultSortColumn?: ColumnSort; /** onRowClick is called when each row is clicked regardless of the type of table (selectable or not) */ @@ -61,8 +62,126 @@ export type TableProps = Pick, 'data' | 'colu onSelectRows?: ((ids: string[]) => void) | undefined; defaultSelectedRows?: string[]; } + ) & + ( + | { + /** + * 読み込み中であることを示す時に指定 + * + * @default false + */ + loading?: false | undefined; + + /** + * 読み込み中であることを示す時にスケルトンローディングで表示する行数を指定 + * + * @default 10 + */ + loadingRows?: never; + data: TableOptions['data']; + } + | { + /** + * 読み込み中であることを示す時に指定 + * + * @default false + */ + loading: true; + + /** + * 読み込み中であることを示す時にスケルトンローディングで表示する行数を指定 + * + * @default 10 + */ + loadingRows: number; + data?: never; + } ); +const getSelectColumn = ({ + id, + multiple, + loading, + onSelectRow, +}: { + id: string; + multiple?: boolean; + loading?: boolean; + onSelectRow?: (row: RowType) => void; +}): ColumnDef => { + return { + id, + meta: { + minWidth: '20px', + width: '20px', + maxWidth: '20px', + }, + header: ({ table }) => ( + + {multiple && ( + + すべての行を選択 + + } + disabled={loading} + className={fsx(`flex`)} + checked={table.getIsAllRowsSelected()} + indeterminate={!table.getIsAllRowsSelected() && table.getIsSomeRowsSelected()} + onChange={table.getToggleAllRowsSelectedHandler()} + /> + )} + + ), + cell: ({ row }) => ( + + {multiple ? ( + + 行を選択 + + } + disabled={loading} + className={fsx(`flex`)} + checked={row.getIsSelected()} + onClick={(e) => { + onSelectRow?.(row); + e.stopPropagation(); + }} + /> + ) : ( + + 行を選択 + + } + disabled={loading} + className={fsx(`flex`)} + checked={row.getIsSelected()} + onClick={(e) => { + onSelectRow?.(row); + e.stopPropagation(); + }} + /> + )} + + ), + }; +}; + +const makeColumnsLoading = (columns: ColumnDef[]) => + columns.map((column) => ({ + ...column, + cell: () => ( + + + + ), + })); + export const Table = ({ data, disablePagination, @@ -74,13 +193,15 @@ export const Table = ({ onRowClick, rowRenderer, getRowId, - columns, + columns: passedColumns, pageCount, pageSize = 20, className, page, defaultPage = 1, onChangePage, + loading, + loadingRows = 10, }: TableProps) => { const tableId = useId(); const [sorting, setSorting] = useState(defaultSortColumn ? [defaultSortColumn] : []); @@ -91,6 +212,8 @@ export const Table = ({ const [rowSelection, setRowSelection] = useState(defaultRowSelection); const prevRowSelection = useRef({}); + const selectable = !!(onSelectRow || onSelectRows); + const onRowSelectionChange: OnChangeFn = useCallback( (updater) => { // updater is designed to be passed to setState like `setState((prev) => updater(prev))` @@ -120,91 +243,45 @@ export const Table = ({ const RowComponent: FC> = rowRenderer || Row; - const selectableColumns = useMemo(() => { - // Not selectable table - if (!(onSelectRow || onSelectRows)) { - return columns; - } + const loadingDummyData = Array(loadingRows).fill( + Object.fromEntries( + passedColumns.map((column) => [column.id || ('accessorKey' in column && column.accessorKey), '']), + ), + ); - const selectColumn: ColumnDef = { - // FIXME: use useId instead - id: 'select', - meta: { - minWidth: '20px', - width: '20px', - maxWidth: '20px', - }, - header: ({ table }) => ( - - {!!onSelectRows && ( - - すべての行を選択 - - } - className={fsx(`flex`)} - checked={table.getIsAllRowsSelected()} - indeterminate={!table.getIsAllRowsSelected() && table.getIsSomeRowsSelected()} - onChange={table.getToggleAllRowsSelectedHandler()} - /> - )} - - ), - cell: ({ row }) => ( - - {!!onSelectRows && ( - - 行を選択 - - } - className={fsx(`flex`)} - checked={row.getIsSelected()} - onClick={(e) => { - selectRow(row); - e.stopPropagation(); - }} - /> - )} - {!!onSelectRow && ( - - 行を選択 - - } - className={fsx(`flex`)} - checked={row.getIsSelected()} - onClick={(e) => { - selectRow(row); - e.stopPropagation(); - }} - /> - )} - - ), - }; - return [selectColumn, ...columns]; - }, [onSelectRow, onSelectRows, selectRow, columns]); + const columns = useMemo(() => { + if (!selectable && !loading) { + return passedColumns; + } + const cols = loading ? makeColumnsLoading(passedColumns) : passedColumns; + if (!selectable) { + return cols; + } + const selectColumn = getSelectColumn({ + id: `${tableId}-select`, + multiple: !!onSelectRows, + loading, + onSelectRow: selectRow, + }); + return [selectColumn, ...cols]; + }, [tableId, onSelectRows, loading, selectRow, passedColumns, selectable]); const table = useReactTable({ - data, - columns: selectableColumns, + data: loading ? loadingDummyData : data, + columns, pageCount: disablePagination ? undefined : pageCount, state: { sorting, rowSelection, }, getRowId, - onRowSelectionChange, + onRowSelectionChange: loading ? undefined : onRowSelectionChange, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: !disablePagination ? getPaginationRowModel() : undefined, - enableRowSelection: !!(onSelectRow || onSelectRows), + enableRowSelection: selectable, enableMultiRowSelection: !!onSelectRows, }); @@ -214,7 +291,7 @@ export const Table = ({ return (
- + {table.getHeaderGroups().map((headerGroup) => ( @@ -222,6 +299,7 @@ export const Table = ({ ({ { @@ -256,6 +334,7 @@ export const Table = ({ {!disablePagination && (
(
@@ -313,6 +394,7 @@ export const TableRow = forwardRef = { row: RowType; selectable: boolean; + clickable?: boolean; onClick?: (e: MouseEvent, row: RowType) => void; className?: string; }; diff --git a/packages/for-ui/src/table/TableCell.tsx b/packages/for-ui/src/table/TableCell.tsx index 34879ba15..c386c223b 100644 --- a/packages/for-ui/src/table/TableCell.tsx +++ b/packages/for-ui/src/table/TableCell.tsx @@ -44,9 +44,10 @@ export const SortableTableCellHead: FC< sorted: false | 'asc' | 'desc'; nextSortingOrder: false | 'asc' | 'desc'; children: ReactNode; + disabled?: boolean; onClick?: HTMLAttributes['onClick']; } -> = ({ sortable, sorted, nextSortingOrder, onClick, children, ...rest }) => ( +> = ({ sortable, sorted, nextSortingOrder, onClick, disabled, children, ...rest }) => ( {sortable ? (