From ab058a36fa55129d8dd4ec350880859e28f06919 Mon Sep 17 00:00:00 2001 From: David Ragsdale Date: Mon, 10 Jul 2023 15:41:31 -0400 Subject: [PATCH] Add types for DataTable (#14149) * feat(react): add DataTable types Add TypeScript types for the DataTable component for better developer experience Closes #12515 * fix: format & lint --------- Co-authored-by: Francine Lucca <40550942+francinelucca@users.noreply.github.com> Co-authored-by: Francine Lucca Co-authored-by: Andrea N. Cardona --- .all-contributorsrc | 27 +- README.md | 3 + .../DataTable/{DataTable.js => DataTable.tsx} | 273 ++++++++++++++++-- .../DataTable/__tests__/exports-test.js | 2 +- .../react/src/components/DataTable/index.ts | 15 +- .../state/{sortStates.js => sortStates.ts} | 4 +- 6 files changed, 291 insertions(+), 33 deletions(-) rename packages/react/src/components/DataTable/{DataTable.js => DataTable.tsx} (75%) rename packages/react/src/components/DataTable/state/{sortStates.js => sortStates.ts} (76%) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3e3668a03cd1..872429a9c3cf 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -165,15 +165,15 @@ "a11y" ] }, - { - "login": "erifsx", - "name": "Eric Marcoux", - "avatar_url": "https://avatars3.githubusercontent.com/u/997572?v=4", - "profile": "https://github.com/erifsx", - "contributions": [ - "code" - ] - }, + { + "login": "erifsx", + "name": "Eric Marcoux", + "avatar_url": "https://avatars3.githubusercontent.com/u/997572?v=4", + "profile": "https://github.com/erifsx", + "contributions": [ + "code" + ] + }, { "login": "vpicone", "name": "Vince Picone", @@ -1223,6 +1223,15 @@ "contributions": [ "code" ] + }, + { + "login": "djragsdale", + "name": "David Ragsdale", + "avatar_url": "https://avatars.githubusercontent.com/u/4396766?v=4", + "profile": "https://github.com/djragsdale", + "contributions": [ + "code" + ] } ], "commitConvention": "none" diff --git a/README.md b/README.md index 237a14006242..e32c8674b9bf 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,9 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Tony ZL

💻
Petr Kadlec

💻 + +
David Ragsdale

💻 + diff --git a/packages/react/src/components/DataTable/DataTable.js b/packages/react/src/components/DataTable/DataTable.tsx similarity index 75% rename from packages/react/src/components/DataTable/DataTable.js rename to packages/react/src/components/DataTable/DataTable.tsx index 104baf46ccd4..b4d40a0e8291 100644 --- a/packages/react/src/components/DataTable/DataTable.js +++ b/packages/react/src/components/DataTable/DataTable.tsx @@ -10,6 +10,7 @@ import React from 'react'; import isEqual from 'lodash.isequal'; import getDerivedStateFromProps from './state/getDerivedStateFromProps'; import { getNextSortState } from './state/sorting'; +import type { DataTableSortState } from './state/sortStates'; import { getCellId } from './tools/cells'; import denormalize from './tools/denormalize'; import { composeEventHandlers } from '../../tools/events'; @@ -63,6 +64,185 @@ const defaultTranslations = { const translateWithId = (id) => defaultTranslations[id]; +const dataTableDefaultProps = { + filterRows: defaultFilterRows, + locale: 'en', + overflowMenuOnHover: true, + size: 'lg', + sortRow: defaultSortRow, + translateWithId, +}; + +export type DataTableSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export interface DataTableCell { + id: string; + value: unknown; + isEditable: boolean; + isEditing: boolean; + isValid: boolean; + errors: null | Array; + info: { + header: string; + }; +} + +export interface DataTableRow { + id: string; + cells: Array; + disabled?: boolean; + isExpanded?: boolean; + isSelected?: boolean; +} + +export interface DataTableHeader { + key: string; + header: React.ReactNode; +} + +export interface DataTableRenderProps { + headers: Array; + rows: Array; + selectedRows: Array; + + // Prop accessors/getters + getHeaderProps: (getHeaderPropsArgs: { + header: DataTableHeader; + isSortable?: boolean; + onClick?: ( + e: MouseEvent, + sortState: { sortHeaderKey: string; sortDirection: DataTableSortState } + ) => void; + [key: string]: unknown; + }) => { + isSortable: boolean | undefined; + isSortHeader: boolean; + key: string; + onClick: (e: MouseEvent) => void; + sortDirection: DataTableSortState; + [key: string]: unknown; + }; + getExpandHeaderProps: (getExpandHeaderPropsArgs?: { + onClick?: (e: MouseEvent, expandState: { isExpanded?: boolean }) => void; + onExpand?: (e: MouseEvent) => void; + [key: string]: unknown; + }) => { + ariaLabel: string; + isExpanded: boolean; + onExpand: (e: MouseEvent) => void; + [key: string]: unknown; + }; + getRowProps: (getRowPropsArgs: { + onClick?: (e: MouseEvent) => void; + row: DataTableRow; + [key: string]: unknown; + }) => { + ariaLabel: string; + disabled: boolean | undefined; + isExpanded?: boolean; + isSelected?: boolean; + key: string; + onExpand: (e: MouseEvent) => void; + [key: string]: unknown; + }; + getSelectionProps: (getSelectionPropsArgs: { + onClick?: (e: MouseEvent) => void; + row: DataTableRow; + [key: string]: unknown; + }) => { + ariaLabel: string; + checked: boolean | undefined; + disabled?: boolean | undefined; + id: string; + indeterminate?: boolean; + name: string; + onSelect: (e: MouseEvent) => void; + radio?: boolean | null; + [key: string]: unknown; + }; + getToolbarProps: (getToolbarPropsArgs?: { [key: string]: unknown }) => { + size: 'sm' | undefined; + [key: string]: unknown; + }; + getBatchActionProps: (getBatchActionPropsArgs?: { + [key: string]: unknown; + }) => { + onCancel: () => void; + shouldShowBatchActions: boolean; + totalSelected: number; + [key: string]: unknown; + }; + getTableProps: () => { + experimentalAutoAlign?: boolean; + isSortable?: boolean; + overflowMenuOnHover: boolean; + size: DataTableSize; + stickyHeader?: boolean; + useStaticWidth?: boolean; + useZebraStyles?: boolean; + }; + getTableContainerProps: () => { + stickyHeader?: boolean; + useStaticWidth?: boolean; + }; + + // Custom event handlers + onInputChange: (e: Event, defaultValue?: string) => void; + + // Expose internal state change actions + sortBy: (headerKey: string) => void; + selectAll: () => void; + selectRow: (rowId: string) => void; + expandRow: (rowId: string) => void; + expandAll: () => void; + radio: boolean | undefined; +} + +export interface DataTableProps { + children?: (renderProps: DataTableRenderProps) => React.ReactElement; + experimentalAutoAlign?: boolean; + filterRows?: (filterRowsArgs: { + cellsById: Record; + getCellId: (rowId: string, header: string) => string; + headers: Array; + inputValue: string; + rowIds: Array; + }) => Array; + headers: Array; + isSortable?: boolean; + locale?: string; + overflowMenuOnHover?: boolean; + radio?: boolean; + render?: (renderProps: DataTableRenderProps) => React.ReactElement; + rows: Array>; + size?: DataTableSize; + sortRow?: ( + cellA: DataTableCell, + cellB: DataTableCell, + sortRowOptions: { + sortDirection: DataTableSortState; + sortStates: Record; + locale: string; + } + ) => number; + stickyHeader?: boolean; + translateWithId?: (id: string) => string; + useStaticWidth?: boolean; + useZebraStyles?: boolean; +} + +interface DataTableState { + cellsById: Record; + filterInputValue: string | null; + initialRowOrder: Array; + isExpandedAll: boolean; + rowIds: Array; + rowsById: Record; + shouldShowBatchActions: boolean; + sortDirection: DataTableSortState; + sortHeaderKey: string | null; +} + /** * Data Tables are used to represent a collection of resources, displaying a * subset of their fields in columns, or headers. We prioritize direct updates @@ -73,7 +253,13 @@ const translateWithId = (id) => defaultTranslations[id]; * and updating the state of the single entity will cascade updates to the * consumer. */ -class DataTable extends React.Component { +class DataTable extends React.Component< + DataTableProps & typeof dataTableDefaultProps, + DataTableState +> { + instanceId: number; + + static defaultProps = dataTableDefaultProps; static propTypes = { /** * Experimental property. Allows table to align cell contents to the top if there is text wrapping in the content. Might have performance issues, intended for smaller tables @@ -168,17 +354,30 @@ class DataTable extends React.Component { useZebraStyles: PropTypes.bool, }; - static defaultProps = { - sortRow: defaultSortRow, - filterRows: defaultFilterRows, - locale: 'en', - size: 'lg', - overflowMenuOnHover: true, - translateWithId, - }; - static translationKeys = Object.values(translationKeys); + // Static properties for sub-components + static Table: typeof Table; + static TableActionList: typeof TableActionList; + static TableBatchAction: typeof TableBatchAction; + static TableBatchActions: typeof TableBatchActions; + static TableBody: typeof TableBody; + static TableCell: typeof TableCell; + static TableContainer: typeof TableContainer; + static TableExpandHeader: typeof TableExpandHeader; + static TableExpandRow: typeof TableExpandRow; + static TableExpandedRow: typeof TableExpandedRow; + static TableHead: typeof TableHead; + static TableHeader: typeof TableHeader; + static TableRow: typeof TableRow; + static TableSelectAll: typeof TableSelectAll; + static TableSelectRow: typeof TableSelectRow; + static TableToolbar: typeof TableToolbar; + static TableToolbarAction: typeof TableToolbarAction; + static TableToolbarContent: typeof TableToolbarContent; + static TableToolbarSearch: typeof TableToolbarSearch; + static TableToolbarMenu: typeof TableToolbarMenu; + constructor(props) { super(props); this.state = { @@ -230,6 +429,14 @@ class DataTable extends React.Component { onClick, isSortable = this.props.isSortable, ...rest + }: { + header: DataTableHeader; + onClick?: ( + e: MouseEvent, + sortState: { sortHeaderKey: string; sortDirection: DataTableSortState } + ) => void; + isSortable?: boolean; + [key: string]: unknown; }) => { const { sortDirection, sortHeaderKey } = this.state; return { @@ -261,7 +468,13 @@ class DataTable extends React.Component { * @param {Function} config.onExpand a custom click handler called when header is expanded * @returns {object} */ - getExpandHeaderProps = ({ onClick, onExpand, ...rest } = {}) => { + getExpandHeaderProps = ( + { onClick, onExpand, ...rest } = {} as { + onClick?: (e: MouseEvent, expandState: { isExpanded: boolean }) => void; + onExpand?: (e: MouseEvent) => void; + [key: string]: unknown; + } + ) => { const { translateWithId: t } = this.props; const { isExpandedAll, rowIds, rowsById } = this.state; const isExpanded = @@ -278,11 +491,10 @@ class DataTable extends React.Component { onExpand: composeEventHandlers([ this.handleOnExpandAll, onExpand, - onClick - ? this.handleOnExpandHeaderClick(onClick, { - isExpanded, - }) - : null, + onClick && + this.handleOnExpandHeaderClick(onClick, { + isExpanded, + }), ]), }; }; @@ -317,7 +529,15 @@ class DataTable extends React.Component { * @param {Function} config.onClick a custom click handler for the header * @returns {object} */ - getRowProps = ({ row, onClick, ...rest }) => { + getRowProps = ({ + row, + onClick, + ...rest + }: { + onClick?: (e: MouseEvent) => void; + row: DataTableRow; + [key: string]: unknown; + }) => { const { translateWithId: t } = this.props; const translationKey = row.isExpanded ? translationKeys.collapseRow @@ -345,7 +565,13 @@ class DataTable extends React.Component { * @param {object} row.row * @returns {object} */ - getSelectionProps = ({ onClick, row, ...rest } = {}) => { + getSelectionProps = ( + { onClick, row, ...rest } = {} as { + onClick?: (e: MouseEvent) => void; + row: DataTableRow; + [key: string]: unknown; + } + ) => { const { translateWithId: t } = this.props; // If we're given a row, return the selection state values for that row @@ -391,9 +617,14 @@ class DataTable extends React.Component { }; }; - getToolbarProps = (props = {}) => { + getToolbarProps = ( + props = {} + ): { + size: 'sm' | undefined; + [key: string]: unknown; + } => { const { size } = this.props; - let isSmall = size === 'xs' || size === 'sm'; + const isSmall = size === 'xs' || size === 'sm'; return { ...props, size: isSmall ? 'sm' : undefined, @@ -676,7 +907,7 @@ class DataTable extends React.Component { getCellId, }) : rowIds; - const renderProps = { + const renderProps: DataTableRenderProps = { // Data derived from state rows: denormalize(filteredRowIds, rowsById, cellsById), headers: this.props.headers, diff --git a/packages/react/src/components/DataTable/__tests__/exports-test.js b/packages/react/src/components/DataTable/__tests__/exports-test.js index 5b6567125dbd..9db710f77936 100644 --- a/packages/react/src/components/DataTable/__tests__/exports-test.js +++ b/packages/react/src/components/DataTable/__tests__/exports-test.js @@ -21,7 +21,7 @@ const blocklist = new Set([ 'DataTable-story.js', '__tests__', '__mocks__', - 'DataTable.js', + 'DataTable.tsx', 'DataTable.mdx', 'stories', 'next', diff --git a/packages/react/src/components/DataTable/index.ts b/packages/react/src/components/DataTable/index.ts index 77ae0a9d2ed9..335a1b5126d2 100644 --- a/packages/react/src/components/DataTable/index.ts +++ b/packages/react/src/components/DataTable/index.ts @@ -5,7 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import DataTable from './DataTable'; +import DataTable, { + type DataTableCell, + type DataTableHeader, + type DataTableRow, + type DataTableProps, + type DataTableRenderProps, + type DataTableSize, +} from './DataTable'; import Table from './Table'; import TableActionList from './TableActionList'; import TableBatchAction from './TableBatchAction'; @@ -50,6 +57,12 @@ DataTable.TableToolbarMenu = TableToolbarMenu; export { DataTable, + type DataTableCell, + type DataTableHeader, + type DataTableProps, + type DataTableRenderProps, + type DataTableRow, + type DataTableSize, Table, TableActionList, TableBatchAction, diff --git a/packages/react/src/components/DataTable/state/sortStates.js b/packages/react/src/components/DataTable/state/sortStates.ts similarity index 76% rename from packages/react/src/components/DataTable/state/sortStates.js rename to packages/react/src/components/DataTable/state/sortStates.ts index c82e2fc8d7af..bf2f0ba15d7c 100644 --- a/packages/react/src/components/DataTable/state/sortStates.js +++ b/packages/react/src/components/DataTable/state/sortStates.ts @@ -5,12 +5,14 @@ * LICENSE file in the root directory of this source tree. */ +export type DataTableSortState = 'NONE' | 'DESC' | 'ASC'; + /** * We currently support the following sorting states for DataTable headers, * namely: `NONE` for no sorting being applied, and then `DESC` and `ASC` for * the corresponding direction of the sorting order. */ -export const sortStates = { +export const sortStates: Record = { NONE: 'NONE', DESC: 'DESC', ASC: 'ASC',