From 0d67a6aaf1293ab0d0315de7a4d203e22e71e931 Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 19 Jan 2023 13:05:12 -0800 Subject: [PATCH] Migrate Table component to TS (#36370) * Migrate table component to TS * Revert pnpm-lock to orig * Revert pnpm-lock to orig * Fix eslint errors * Update packages/js/components/src/table/empty.tsx Co-authored-by: Chi-Hsuan Huang * Update packages/js/components/src/table/empty.tsx Co-authored-by: Chi-Hsuan Huang * Remove unnecessary empty space and convert comment stlye * Update packages/js/components/src/table/index.tsx Co-authored-by: Chi-Hsuan Huang * Remove unnecessary type casting * Type defaultOnQueryChange func correctly Co-authored-by: Chi-Hsuan Huang --- .../dev-migrate-table-component-to-ts | 4 + .../src/table/{empty.js => empty.tsx} | 31 +- packages/js/components/src/table/index.js | 384 -------------- packages/js/components/src/table/index.tsx | 248 +++++++++ .../js/components/src/table/placeholder.js | 68 --- .../js/components/src/table/placeholder.tsx | 55 ++ .../{empty-table.js => empty-table.tsx} | 1 + .../src/table/stories/{index.js => index.ts} | 0 .../src/table/stories/table-card.js | 42 -- .../src/table/stories/table-card.tsx | 93 ++++ ...e-placeholder.js => table-placeholder.tsx} | 14 +- ...older.js => table-summary-placeholder.tsx} | 18 +- .../{table-summary.js => table-summary.jsx} | 0 .../src/table/stories/{table.js => table.tsx} | 26 +- .../src/table/{summary.js => summary.tsx} | 21 +- packages/js/components/src/table/table.js | 491 ------------------ packages/js/components/src/table/table.tsx | 374 +++++++++++++ packages/js/components/src/table/types.ts | 189 +++++++ 18 files changed, 1018 insertions(+), 1041 deletions(-) create mode 100644 packages/js/components/changelog/dev-migrate-table-component-to-ts rename packages/js/components/src/table/{empty.js => empty.tsx} (50%) delete mode 100644 packages/js/components/src/table/index.js create mode 100644 packages/js/components/src/table/index.tsx delete mode 100644 packages/js/components/src/table/placeholder.js create mode 100644 packages/js/components/src/table/placeholder.tsx rename packages/js/components/src/table/stories/{empty-table.js => empty-table.tsx} (83%) rename packages/js/components/src/table/stories/{index.js => index.ts} (100%) delete mode 100644 packages/js/components/src/table/stories/table-card.js create mode 100644 packages/js/components/src/table/stories/table-card.tsx rename packages/js/components/src/table/stories/{table-placeholder.js => table-placeholder.tsx} (52%) rename packages/js/components/src/table/stories/{table-summary-placeholder.js => table-summary-placeholder.tsx} (50%) rename packages/js/components/src/table/stories/{table-summary.js => table-summary.jsx} (100%) rename packages/js/components/src/table/stories/{table.js => table.tsx} (54%) rename packages/js/components/src/table/{summary.js => summary.tsx} (76%) delete mode 100644 packages/js/components/src/table/table.js create mode 100644 packages/js/components/src/table/table.tsx create mode 100644 packages/js/components/src/table/types.ts diff --git a/packages/js/components/changelog/dev-migrate-table-component-to-ts b/packages/js/components/changelog/dev-migrate-table-component-to-ts new file mode 100644 index 00000000000..0f3389fca67 --- /dev/null +++ b/packages/js/components/changelog/dev-migrate-table-component-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate Table component to TS \ No newline at end of file diff --git a/packages/js/components/src/table/empty.js b/packages/js/components/src/table/empty.tsx similarity index 50% rename from packages/js/components/src/table/empty.js rename to packages/js/components/src/table/empty.tsx index 503ab1fc9a2..80550ced137 100644 --- a/packages/js/components/src/table/empty.js +++ b/packages/js/components/src/table/empty.tsx @@ -1,39 +1,32 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { createElement } from '@wordpress/element'; +import React from 'react'; + +type EmptyTableProps = { + children: React.ReactNode; + + /** An integer with the number of rows the box should occupy. */ + numberOfRows?: number; +}; /** * `EmptyTable` displays a blank space with an optional message passed as a children node * with the purpose of replacing a table with no rows. * It mimics the same height a table would have according to the `numberOfRows` prop. - * - * @param {Object} props - * @param {Node} props.children - * @param {number} props.numberOfRows - * @return {Object} - */ -const EmptyTable = ( { children, numberOfRows } ) => { +const EmptyTable = ( { children, numberOfRows = 5 }: EmptyTableProps ) => { return (
{ children }
); }; -EmptyTable.propTypes = { - /** - * An integer with the number of rows the box should occupy. - */ - numberOfRows: PropTypes.number, -}; - -EmptyTable.defaultProps = { - numberOfRows: 5, -}; - export default EmptyTable; diff --git a/packages/js/components/src/table/index.js b/packages/js/components/src/table/index.js deleted file mode 100644 index 2723ceb6c32..00000000000 --- a/packages/js/components/src/table/index.js +++ /dev/null @@ -1,384 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import classnames from 'classnames'; -import { - Card, - CardBody, - CardFooter, - CardHeader, - __experimentalText as Text, -} from '@wordpress/components'; -import { createElement, Component, Fragment } from '@wordpress/element'; -import { find, first, isEqual, without } from 'lodash'; -import PropTypes from 'prop-types'; - -/** - * Internal dependencies - */ -import EllipsisMenu from '../ellipsis-menu'; -import MenuItem from '../ellipsis-menu/menu-item'; -import MenuTitle from '../ellipsis-menu/menu-title'; -import Pagination from '../pagination'; -import Table from './table'; -import TablePlaceholder from './placeholder'; -import TableSummary, { TableSummaryPlaceholder } from './summary'; - -/** - * This is an accessible, sortable, and scrollable table for displaying tabular data (like revenue and other analytics data). - * It accepts `headers` for column headers, and `rows` for the table content. - * `rowHeader` can be used to define the index of the row header (or false if no header). - * - * `TableCard` serves as Card wrapper & contains a card header, ``, ``, and ``. - * This includes filtering and comparison functionality for report pages. - */ -class TableCard extends Component { - constructor( props ) { - super( props ); - const showCols = this.getShowCols( props.headers ); - - this.state = { showCols }; - this.onColumnToggle = this.onColumnToggle.bind( this ); - this.onPageChange = this.onPageChange.bind( this ); - } - - componentDidUpdate( { headers: prevHeaders, query: prevQuery } ) { - const { headers, onColumnsChange, query } = this.props; - const { showCols } = this.state; - - if ( ! isEqual( headers, prevHeaders ) ) { - /* eslint-disable react/no-did-update-set-state */ - this.setState( { - showCols: this.getShowCols( headers ), - } ); - /* eslint-enable react/no-did-update-set-state */ - } - if ( - query.orderby !== prevQuery.orderby && - ! showCols.includes( query.orderby ) - ) { - const newShowCols = showCols.concat( query.orderby ); - /* eslint-disable react/no-did-update-set-state */ - this.setState( { - showCols: newShowCols, - } ); - /* eslint-enable react/no-did-update-set-state */ - onColumnsChange( newShowCols ); - } - } - - getShowCols( headers ) { - return headers - .map( ( { key, visible } ) => { - if ( typeof visible === 'undefined' || visible ) { - return key; - } - return false; - } ) - .filter( Boolean ); - } - - getVisibleHeaders() { - const { headers } = this.props; - const { showCols } = this.state; - return headers.filter( ( { key } ) => showCols.includes( key ) ); - } - - getVisibleRows() { - const { headers, rows } = this.props; - const { showCols } = this.state; - - return rows.map( ( row ) => { - return headers - .map( ( { key }, i ) => { - return showCols.includes( key ) && row[ i ]; - } ) - .filter( Boolean ); - } ); - } - - onColumnToggle( key ) { - const { headers, query, onQueryChange, onColumnsChange } = this.props; - - return () => { - this.setState( ( prevState ) => { - const hasKey = prevState.showCols.includes( key ); - - if ( hasKey ) { - // Handle hiding a sorted column - if ( query.orderby === key ) { - const defaultSort = - find( headers, { defaultSort: true } ) || - first( headers ) || - {}; - onQueryChange( 'sort' )( defaultSort.key, 'desc' ); - } - - const showCols = without( prevState.showCols, key ); - onColumnsChange( showCols, key ); - return { showCols }; - } - - const showCols = [ ...prevState.showCols, key ]; - onColumnsChange( showCols, key ); - return { showCols }; - } ); - }; - } - - onPageChange( ...params ) { - const { onPageChange, onQueryChange } = this.props; - if ( onPageChange ) { - onPageChange( ...params ); - } - if ( onQueryChange ) { - onQueryChange( 'paged' )( ...params ); - } - } - - render() { - const { - actions, - className, - hasSearch, - isLoading, - onQueryChange, - onSort, - query, - rowHeader, - rowsPerPage, - showMenu, - summary, - title, - totalRows, - rowKey, - emptyMessage, - } = this.props; - const { showCols } = this.state; - const allHeaders = this.props.headers; - const headers = this.getVisibleHeaders(); - const rows = this.getVisibleRows(); - const classes = classnames( 'woocommerce-table', className, { - 'has-actions': !! actions, - 'has-menu': showMenu, - 'has-search': hasSearch, - } ); - - return ( - - - - { title } - -
- { actions } -
- { showMenu && ( - ( - - - { __( 'Columns:', 'woocommerce' ) } - - { allHeaders.map( - ( { key, label, required } ) => { - if ( required ) { - return null; - } - return ( - - { label } - - ); - } - ) } - - ) } - /> - ) } -
- - { isLoading ? ( - - - { __( - 'Your requested data is loading', - 'woocommerce' - ) } - - - - ) : ( -
- ) } - - - - { isLoading ? ( - - ) : ( - - - - { summary && } - - ) } - - - ); - } -} - -TableCard.propTypes = { - /** - * If a search is provided in actions and should reorder actions on mobile. - */ - hasSearch: PropTypes.bool, - /** - * An array of column headers (see `Table` props). - */ - headers: PropTypes.arrayOf( - PropTypes.shape( { - hiddenByDefault: PropTypes.bool, - defaultSort: PropTypes.bool, - isSortable: PropTypes.bool, - key: PropTypes.string, - label: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), - required: PropTypes.bool, - } ) - ), - /** - * A list of IDs, matching to the row list so that ids[ 0 ] contains the object ID for the object displayed in row[ 0 ]. - */ - ids: PropTypes.arrayOf( PropTypes.number ), - /** - * Defines if the table contents are loading. - * It will display `TablePlaceholder` component instead of `Table` if that's the case. - */ - isLoading: PropTypes.bool, - /** - * A function which returns a callback function to update the query string for a given `param`. - */ - onQueryChange: PropTypes.func, - /** - * A function which returns a callback function which is called upon the user changing the visiblity of columns. - */ - onColumnsChange: PropTypes.func, - /** - * A function which is called upon the user changing the sorting of the table. - */ - onSort: PropTypes.func, - /** - * An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }`. - */ - query: PropTypes.object, - /** - * Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col - * is checkboxes, for example). Set to false to disable row headers. - */ - rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ), - /** - * An array of arrays of display/value object pairs (see `Table` props). - */ - rows: PropTypes.arrayOf( - PropTypes.arrayOf( - PropTypes.shape( { - display: PropTypes.node, - value: PropTypes.oneOfType( [ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ] ), - } ) - ) - ).isRequired, - /** - * The total number of rows to display per page. - */ - rowsPerPage: PropTypes.number.isRequired, - /** - * Boolean to determine whether or not ellipsis menu is shown. - */ - showMenu: PropTypes.bool, - /** - * An array of objects with `label` & `value` properties, which display in a line under the table. - * Optional, can be left off to show no summary. - */ - summary: PropTypes.arrayOf( - PropTypes.shape( { - label: PropTypes.node, - value: PropTypes.oneOfType( [ - PropTypes.string, - PropTypes.number, - ] ), - } ) - ), - /** - * The title used in the card header, also used as the caption for the content in this table. - */ - title: PropTypes.string.isRequired, - /** - * The total number of rows (across all pages). - */ - totalRows: PropTypes.number.isRequired, - /** - * The rowKey used for the key value on each row, this can be a string of the key or a function that returns the value. - * This uses the index if not defined. - */ - rowKey: PropTypes.func, - /** - * Customize the message to show when there are no rows in the table. - */ - emptyMessage: PropTypes.string, -}; - -TableCard.defaultProps = { - isLoading: false, - onQueryChange: () => () => {}, - onColumnsChange: () => {}, - onSort: undefined, - query: {}, - rowHeader: 0, - rows: [], - showMenu: true, - emptyMessage: undefined, -}; - -export default TableCard; diff --git a/packages/js/components/src/table/index.tsx b/packages/js/components/src/table/index.tsx new file mode 100644 index 00000000000..6c12b11f810 --- /dev/null +++ b/packages/js/components/src/table/index.tsx @@ -0,0 +1,248 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { createElement, Fragment, useState } from '@wordpress/element'; +import { find, first, without } from 'lodash'; +import React from 'react'; +import { + Card, + CardBody, + CardFooter, + CardHeader, + // @ts-expect-error: Suppressing Module '"@wordpress/components"' has no exported member '__experimentalText' + __experimentalText as Text, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import EllipsisMenu from '../ellipsis-menu'; +import MenuItem from '../ellipsis-menu/menu-item'; +import MenuTitle from '../ellipsis-menu/menu-title'; +import Pagination from '../pagination'; +import Table from './table'; +import TablePlaceholder from './placeholder'; +import TableSummary, { TableSummaryPlaceholder } from './summary'; +import { TableCardProps } from './types'; + +const defaultOnQueryChange = + ( param: string ) => ( path?: string, direction?: string ) => {}; + +const defaultOnColumnsChange = ( + showCols: Array< string >, + key?: string +) => {}; +/** + * This is an accessible, sortable, and scrollable table for displaying tabular data (like revenue and other analytics data). + * It accepts `headers` for column headers, and `rows` for the table content. + * `rowHeader` can be used to define the index of the row header (or false if no header). + * + * `TableCard` serves as Card wrapper & contains a card header, `
`, ``, and ``. + * This includes filtering and comparison functionality for report pages. + */ +const TableCard: React.VFC< TableCardProps > = ( { + actions, + className, + hasSearch, + headers = [], + ids, + isLoading = false, + onQueryChange = defaultOnQueryChange, + onColumnsChange = defaultOnColumnsChange, + onSort, + query = {}, + rowHeader = 0, + rows = [], + rowsPerPage, + showMenu = true, + summary, + title, + totalRows, + rowKey, + emptyMessage = undefined, + ...props +} ) => { + // eslint-disable-next-line no-console + const getShowCols = ( _headers: TableCardProps[ 'headers' ] = [] ) => { + return _headers + .map( ( { key, visible } ) => { + if ( typeof visible === 'undefined' || visible ) { + return key; + } + return false; + } ) + .filter( Boolean ) as string[]; + }; + + const [ showCols, setShowCols ] = useState( getShowCols( headers ) ); + + const onColumnToggle = ( key: string ) => { + return () => { + const hasKey = showCols.includes( key ); + + if ( hasKey ) { + // Handle hiding a sorted column + if ( query.orderby === key ) { + const defaultSort = find( headers, { + defaultSort: true, + } ) || + first( headers ) || { key: undefined }; + onQueryChange( 'sort' )( defaultSort.key, 'desc' ); + } + + const newShowCols = without( showCols, key ); + onColumnsChange( newShowCols, key ); + setShowCols( newShowCols ); + } else { + const newShowCols = [ ...showCols, key ] as string[]; + onColumnsChange( newShowCols, key ); + setShowCols( newShowCols ); + } + }; + }; + + const onPageChange = ( + newPage: string, + direction?: 'previous' | 'next' + ) => { + if ( props.onPageChange ) { + props.onPageChange( parseInt( newPage, 10 ), direction ); + } + if ( onQueryChange ) { + onQueryChange( 'paged' )( newPage, direction ); + } + }; + + const allHeaders = headers; + const visibleHeaders = headers.filter( ( { key } ) => + showCols.includes( key ) + ); + const visibleRows = rows.map( ( row ) => { + return headers + .map( ( { key }, i ) => { + return showCols.includes( key ) && row[ i ]; + } ) + .filter( Boolean ); + } ); + const classes = classnames( 'woocommerce-table', className, { + 'has-actions': !! actions, + 'has-menu': showMenu, + 'has-search': hasSearch, + } ); + + return ( + + + + { title } + +
{ actions }
+ { showMenu && ( + ( + + { /* @ts-expect-error: Ignoring the error until we migrate ellipsis-menu to TS*/ } + + { /* @ts-expect-error: Allow string */ } + { __( 'Columns:', 'woocommerce' ) } + + { allHeaders.map( + ( { key, label, required } ) => { + if ( required ) { + return null; + } + return ( + + { label } + + ); + } + ) } + + ) } + /> + ) } +
+ { /* Ignoring the error to make it backward compatible for now. */ } + { /* @ts-expect-error: size must be one of small, medium, largel, xSmall, extraSmall. */ } + + { isLoading ? ( + + + { __( + 'Your requested data is loading', + 'woocommerce' + ) } + + + + ) : ( +
void ) + } + rowKey={ rowKey } + emptyMessage={ emptyMessage } + /> + ) } + + + { /* @ts-expect-error: justify is missing from the latest @types/wordpress__components */ } + + { isLoading ? ( + + ) : ( + + + + { summary && } + + ) } + + + ); +}; + +export default TableCard; diff --git a/packages/js/components/src/table/placeholder.js b/packages/js/components/src/table/placeholder.js deleted file mode 100644 index cd6e2cbf999..00000000000 --- a/packages/js/components/src/table/placeholder.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * External dependencies - */ -import { createElement, Component } from '@wordpress/element'; -import { range } from 'lodash'; -import PropTypes from 'prop-types'; - -/** - * Internal dependencies - */ -import Table from './table'; - -/** - * `TablePlaceholder` behaves like `Table` but displays placeholder boxes instead of data. This can be used while loading. - */ -class TablePlaceholder extends Component { - render() { - const { numberOfRows, ...tableProps } = this.props; - const rows = range( numberOfRows ).map( () => - this.props.headers.map( () => ( { - display: , - } ) ) - ); - - return ( -
- ); - } -} - -TablePlaceholder.propTypes = { - /** - * An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }`. - */ - query: PropTypes.object, - /** - * A label for the content in this table. - */ - caption: PropTypes.string.isRequired, - /** - * An array of column headers (see `Table` props). - */ - headers: PropTypes.arrayOf( - PropTypes.shape( { - hiddenByDefault: PropTypes.bool, - defaultSort: PropTypes.bool, - isSortable: PropTypes.bool, - key: PropTypes.string, - label: PropTypes.node, - required: PropTypes.bool, - } ) - ), - /** - * An integer with the number of rows to display. - */ - numberOfRows: PropTypes.number, -}; - -TablePlaceholder.defaultProps = { - numberOfRows: 5, -}; - -export default TablePlaceholder; diff --git a/packages/js/components/src/table/placeholder.tsx b/packages/js/components/src/table/placeholder.tsx new file mode 100644 index 00000000000..fbc8ee916f1 --- /dev/null +++ b/packages/js/components/src/table/placeholder.tsx @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { range } from 'lodash'; + +/** + * Internal dependencies + */ +import Table from './table'; +import { QueryProps, TableHeader } from './types'; + +type TablePlaceholderProps = { + /** An object of the query parameters passed to the page */ + query?: QueryProps; + /** A label for the content in this table. */ + caption: string; + /** An integer with the number of rows to display. */ + numberOfRows?: number; + /** + * Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col + * is checkboxes, for example). Set to false to disable row headers. + */ + rowHeader?: number | false; + /** An array of column headers (see `Table` props). */ + headers: Array< TableHeader >; +}; + +/** + * `TablePlaceholder` behaves like `Table` but displays placeholder boxes instead of data. This can be used while loading. + */ +const TablePlaceholder: React.VFC< TablePlaceholderProps > = ( { + query, + caption, + headers, + numberOfRows = 5, + ...props +} ) => { + const rows = range( numberOfRows ).map( () => + headers.map( () => ( { + display: , + } ) ) + ); + const tableProps = { query, caption, headers, numberOfRows, ...props }; + return ( +
+ ); +}; + +export default TablePlaceholder; diff --git a/packages/js/components/src/table/stories/empty-table.js b/packages/js/components/src/table/stories/empty-table.tsx similarity index 83% rename from packages/js/components/src/table/stories/empty-table.js rename to packages/js/components/src/table/stories/empty-table.tsx index d7688261b34..cc2674a85f4 100644 --- a/packages/js/components/src/table/stories/empty-table.js +++ b/packages/js/components/src/table/stories/empty-table.tsx @@ -2,6 +2,7 @@ * External dependencies */ import { EmptyTable } from '@woocommerce/components'; +import { createElement } from '@wordpress/element'; export const Basic = () => There are no entries.; diff --git a/packages/js/components/src/table/stories/index.js b/packages/js/components/src/table/stories/index.ts similarity index 100% rename from packages/js/components/src/table/stories/index.js rename to packages/js/components/src/table/stories/index.ts diff --git a/packages/js/components/src/table/stories/table-card.js b/packages/js/components/src/table/stories/table-card.js deleted file mode 100644 index cc485146080..00000000000 --- a/packages/js/components/src/table/stories/table-card.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { TableCard } from '@woocommerce/components'; -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { headers, rows, summary } from './index'; - -const TableCardExample = () => { - const [ { query }, setState ] = useState( { - query: { - paged: 1, - }, - } ); - return ( - ( value ) => - setState( { - query: { - [ param ]: value, - }, - } ) } - query={ query } - rowsPerPage={ 7 } - totalRows={ 10 } - summary={ summary } - /> - ); -}; - -export const Basic = () => ; - -export default { - title: 'WooCommerce Admin/components/TableCard', - component: TableCard, -}; diff --git a/packages/js/components/src/table/stories/table-card.tsx b/packages/js/components/src/table/stories/table-card.tsx new file mode 100644 index 00000000000..86aa3202426 --- /dev/null +++ b/packages/js/components/src/table/stories/table-card.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { TableCard } from '@woocommerce/components'; +import { useState, createElement } from '@wordpress/element'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { headers, rows, summary } from './index'; + +const TableCardExample = () => { + const [ { query }, setState ] = useState( { + query: { + paged: 1, + }, + } ); + return ( + ( value ) => + setState( { + // @ts-expect-error: ignore for storybook + query: { + [ param ]: value, + }, + } ) } + query={ query } + rowsPerPage={ 7 } + totalRows={ 10 } + summary={ summary } + /> + ); +}; + +const TableCardWithActionsExample = () => { + const [ { query }, setState ] = useState( { + query: { + paged: 1, + }, + } ); + + const [ action1Text, setAction1Text ] = useState( 'Action 1' ); + const [ action2Text, setAction2Text ] = useState( 'Action 2' ); + + return ( + { + setAction1Text( 'Action 1 Clicked' ); + } } + > + { action1Text } + , + , + ] } + title="Revenue last week" + rows={ rows } + headers={ headers } + onQueryChange={ ( param ) => ( value ) => + setState( { + // @ts-expect-error: ignore for storybook + query: { + [ param ]: value, + }, + } ) } + query={ query } + rowsPerPage={ 7 } + totalRows={ 10 } + summary={ summary } + /> + ); +}; + +export const Basic = () => ; +export const Actions = () => ; + +export default { + title: 'WooCommerce Admin/components/TableCard', + component: TableCard, +}; diff --git a/packages/js/components/src/table/stories/table-placeholder.js b/packages/js/components/src/table/stories/table-placeholder.tsx similarity index 52% rename from packages/js/components/src/table/stories/table-placeholder.js rename to packages/js/components/src/table/stories/table-placeholder.tsx index ea947bb0125..ea1dd40ac65 100644 --- a/packages/js/components/src/table/stories/table-placeholder.js +++ b/packages/js/components/src/table/stories/table-placeholder.tsx @@ -3,17 +3,21 @@ */ import { Card } from '@wordpress/components'; import { TablePlaceholder } from '@woocommerce/components'; +import { createElement } from '@wordpress/element'; /** * Internal dependencies */ import { headers } from './index'; -export const Basic = () => ( - - - -); +export const Basic = () => { + return ( + /* @ts-expect-error: size must be one of small, medium, largel, xSmall, extraSmall. */ + + + + ); +}; export default { title: 'WooCommerce Admin/components/TablePlaceholder', diff --git a/packages/js/components/src/table/stories/table-summary-placeholder.js b/packages/js/components/src/table/stories/table-summary-placeholder.tsx similarity index 50% rename from packages/js/components/src/table/stories/table-summary-placeholder.js rename to packages/js/components/src/table/stories/table-summary-placeholder.tsx index 6c5e15ed11d..6343c44aa73 100644 --- a/packages/js/components/src/table/stories/table-summary-placeholder.js +++ b/packages/js/components/src/table/stories/table-summary-placeholder.tsx @@ -3,14 +3,18 @@ */ import { Card, CardFooter } from '@wordpress/components'; import { TableSummaryPlaceholder } from '@woocommerce/components'; +import { createElement } from '@wordpress/element'; -export const Basic = () => ( - - - - - -); +export const Basic = () => { + return ( + + { /* @ts-expect-error: justify is missing from the latest type def. */ } + + + + + ); +}; export default { title: 'WooCommerce Admin/components/TableSummaryPlaceholder', diff --git a/packages/js/components/src/table/stories/table-summary.js b/packages/js/components/src/table/stories/table-summary.jsx similarity index 100% rename from packages/js/components/src/table/stories/table-summary.js rename to packages/js/components/src/table/stories/table-summary.jsx diff --git a/packages/js/components/src/table/stories/table.js b/packages/js/components/src/table/stories/table.tsx similarity index 54% rename from packages/js/components/src/table/stories/table.js rename to packages/js/components/src/table/stories/table.tsx index 1ea3ffb97c9..a7a68b6bce7 100644 --- a/packages/js/components/src/table/stories/table.js +++ b/packages/js/components/src/table/stories/table.tsx @@ -3,6 +3,7 @@ */ import { Card } from '@wordpress/components'; import { Table } from '@woocommerce/components'; +import { createElement } from '@wordpress/element'; /** * Internal dependencies @@ -20,17 +21,20 @@ export const Basic = () => ( ); -export const NoDataCustomMessage = () => ( - -
row[ 0 ].value } - emptyMessage="Custom empty message" - /> - -); +export const NoDataCustomMessage = () => { + return ( + /* @ts-expect-error: size must be one of small, medium, largel, xSmall, extraSmall. */ + +
row[ 0 ].value } + emptyMessage="Custom empty message" + /> + + ); +}; export default { title: 'WooCommerce Admin/components/Table', diff --git a/packages/js/components/src/table/summary.js b/packages/js/components/src/table/summary.tsx similarity index 76% rename from packages/js/components/src/table/summary.js rename to packages/js/components/src/table/summary.tsx index e3e85ca724f..784d47632da 100644 --- a/packages/js/components/src/table/summary.js +++ b/packages/js/components/src/table/summary.tsx @@ -1,17 +1,17 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { createElement } from '@wordpress/element'; /** - * A component to display summarized table data - the list of data passed in on a single line. - * - * @param {Object} props - * @param {Array} props.data - * @return {Object} - + * Internal dependencies */ -const TableSummary = ( { data } ) => { +import { TableSummaryProps } from './types'; + +/** + * A component to display summarized table data - the list of data passed in on a single line. + */ +const TableSummary = ( { data }: TableSummaryProps ) => { return (
    { data.map( ( { label, value }, i ) => ( @@ -28,13 +28,6 @@ const TableSummary = ( { data } ) => { ); }; -TableSummary.propTypes = { - /** - * An array of objects with `label` & `value` properties, which display on a single line. - */ - data: PropTypes.array, -}; - export default TableSummary; /** diff --git a/packages/js/components/src/table/table.js b/packages/js/components/src/table/table.js deleted file mode 100644 index 0f484027b69..00000000000 --- a/packages/js/components/src/table/table.js +++ /dev/null @@ -1,491 +0,0 @@ -/** - * External dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { - createElement, - Component, - createRef, - Fragment, -} from '@wordpress/element'; -import classnames from 'classnames'; -import { Button } from '@wordpress/components'; -import { find, get, noop } from 'lodash'; -import PropTypes from 'prop-types'; -import { withInstanceId } from '@wordpress/compose'; -import { Icon, chevronUp, chevronDown } from '@wordpress/icons'; -import deprecated from '@wordpress/deprecated'; - -const ASC = 'asc'; -const DESC = 'desc'; - -const getDisplay = ( cell ) => cell.display || null; - -/** - * A table component, without the Card wrapper. This is a basic table display, sortable, but no default filtering. - * - * Row data should be passed to the component as a list of arrays, where each array is a row in the table. - * Headers are passed in separately as an array of objects with column-related properties. For example, - * this data would render the following table. - * - * ```js - * const headers = [ { label: 'Month' }, { label: 'Orders' }, { label: 'Revenue' } ]; - * const rows = [ - * [ - * { display: 'January', value: 1 }, - * { display: 10, value: 10 }, - * { display: '$530.00', value: 530 }, - * ], - * [ - * { display: 'February', value: 2 }, - * { display: 13, value: 13 }, - * { display: '$675.00', value: 675 }, - * ], - * [ - * { display: 'March', value: 3 }, - * { display: 9, value: 9 }, - * { display: '$460.00', value: 460 }, - * ], - * ] - * ``` - * - * | Month | Orders | Revenue | - * | ---------|--------|---------| - * | January | 10 | $530.00 | - * | February | 13 | $675.00 | - * | March | 9 | $460.00 | - */ -class Table extends Component { - constructor( props ) { - super( props ); - this.state = { - tabIndex: null, - isScrollableRight: false, - isScrollableLeft: false, - }; - this.container = createRef(); - this.sortBy = this.sortBy.bind( this ); - this.updateTableShadow = this.updateTableShadow.bind( this ); - this.getRowKey = this.getRowKey.bind( this ); - } - - componentDidMount() { - const { scrollWidth, clientWidth } = this.container.current; - const scrollable = scrollWidth > clientWidth; - /* eslint-disable react/no-did-mount-set-state */ - this.setState( { - tabIndex: scrollable ? '0' : null, - } ); - /* eslint-enable react/no-did-mount-set-state */ - this.updateTableShadow(); - window.addEventListener( 'resize', this.updateTableShadow ); - } - - componentDidUpdate() { - this.updateTableShadow(); - } - - componentWillUnmount() { - window.removeEventListener( 'resize', this.updateTableShadow ); - } - - sortBy( key ) { - const { headers, query } = this.props; - return () => { - const currentKey = - query.orderby || - get( find( headers, { defaultSort: true } ), 'key', false ); - const currentDir = - query.order || - get( - find( headers, { key: currentKey } ), - 'defaultOrder', - DESC - ); - let dir = DESC; - if ( key === currentKey ) { - dir = DESC === currentDir ? ASC : DESC; - } - this.props.onSort( key, dir ); - }; - } - - updateTableShadow() { - const table = this.container.current; - const { isScrollableRight, isScrollableLeft } = this.state; - - const scrolledToEnd = - table.scrollWidth - table.scrollLeft <= table.offsetWidth; - if ( scrolledToEnd && isScrollableRight ) { - this.setState( { isScrollableRight: false } ); - } else if ( ! scrolledToEnd && ! this.state.isScrollableRight ) { - this.setState( { isScrollableRight: true } ); - } - - const scrolledToStart = table.scrollLeft <= 0; - if ( scrolledToStart && isScrollableLeft ) { - this.setState( { isScrollableLeft: false } ); - } else if ( ! scrolledToStart && ! isScrollableLeft ) { - this.setState( { isScrollableLeft: true } ); - } - } - - getRowKey( row, index ) { - if ( this.props.rowKey && typeof this.props.rowKey === 'function' ) { - return this.props.rowKey( row, index ); - } - return index; - } - - render() { - const { - ariaHidden, - caption, - className, - classNames, - headers, - instanceId, - query, - rowHeader, - rows, - emptyMessage, - } = this.props; - const { isScrollableRight, isScrollableLeft, tabIndex } = this.state; - - if ( classNames ) { - deprecated( `Table component's classNames prop`, { - since: '11.1.0', - version: '12.0.0', - alternative: 'className', - plugin: '@woocommerce/components', - } ); - } - - const classes = classnames( - 'woocommerce-table__table', - classNames, - className, - { - 'is-scrollable-right': isScrollableRight, - 'is-scrollable-left': isScrollableLeft, - } - ); - const sortedBy = - query.orderby || - get( find( headers, { defaultSort: true } ), 'key', false ); - const sortDir = - query.order || - get( find( headers, { key: sortedBy } ), 'defaultOrder', DESC ); - const hasData = !! rows.length; - - return ( -
    -
- - - - { headers.map( ( header, i ) => { - const { - cellClassName, - isLeftAligned, - isSortable, - isNumeric, - key, - label, - screenReaderLabel, - } = header; - const labelId = `header-${ instanceId }-${ i }`; - const thProps = { - className: classnames( - 'woocommerce-table__header', - cellClassName, - { - 'is-left-aligned': - isLeftAligned || ! isNumeric, - 'is-sortable': isSortable, - 'is-sorted': sortedBy === key, - 'is-numeric': isNumeric, - } - ), - }; - if ( isSortable ) { - thProps[ 'aria-sort' ] = 'none'; - if ( sortedBy === key ) { - thProps[ 'aria-sort' ] = - sortDir === ASC - ? 'ascending' - : 'descending'; - } - } - // We only sort by ascending if the col is already sorted descending - const iconLabel = - sortedBy === key && sortDir !== ASC - ? sprintf( - __( - 'Sort by %s in ascending order', - 'woocommerce' - ), - screenReaderLabel || label - ) - : sprintf( - __( - 'Sort by %s in descending order', - 'woocommerce' - ), - screenReaderLabel || label - ); - - const textLabel = ( - - - { label } - - { screenReaderLabel && ( - - { screenReaderLabel } - - ) } - - ); - - return ( - - ); - } ) } - - { hasData ? ( - rows.map( ( row, i ) => ( - - { row.map( ( cell, j ) => { - const { - cellClassName, - isLeftAligned, - isNumeric, - } = headers[ j ]; - const isHeader = rowHeader === j; - const Cell = isHeader ? 'th' : 'td'; - const cellClasses = classnames( - 'woocommerce-table__item', - cellClassName, - { - 'is-left-aligned': - isLeftAligned || - ! isNumeric, - 'is-numeric': isNumeric, - 'is-sorted': - sortedBy === - headers[ j ].key, - } - ); - const cellKey = - this.getRowKey( - row, - i - ).toString() + j; - return ( - - { getDisplay( cell ) } - - ); - } ) } - - ) ) - ) : ( - - - - ) } - -
- { caption } - { tabIndex === '0' && ( - - { __( '(scroll to see more)', 'woocommerce' ) } - - ) } -
- { isSortable ? ( - - - - { iconLabel } - - - ) : ( - textLabel - ) } -
- { emptyMessage ?? - __( - 'No data to display', - 'woocommerce' - ) } -
- - ); - } -} - -Table.propTypes = { - /** - * Controls whether this component is hidden from screen readers. Used by the loading state, before there is data to read. - * Don't use this on real tables unless the table data is loaded elsewhere on the page. - */ - ariaHidden: PropTypes.bool, - /** - * A label for the content in this table - */ - caption: PropTypes.string.isRequired, - /** - * Additional CSS classes. - */ - className: PropTypes.string, - /** - * An array of column headers, as objects. - */ - headers: PropTypes.arrayOf( - PropTypes.shape( { - /** - * Boolean, true if this column is the default for sorting. Only one column should have this set. - */ - defaultSort: PropTypes.bool, - /** - * String, asc|desc if this column is the default for sorting. Only one column should have this set. - */ - defaultOrder: PropTypes.string, - /** - * Boolean, true if this column should be aligned to the left. - */ - isLeftAligned: PropTypes.bool, - /** - * Boolean, true if this column is a number value. - */ - isNumeric: PropTypes.bool, - /** - * Boolean, true if this column is sortable. - */ - isSortable: PropTypes.bool, - /** - * The API parameter name for this column, passed to `orderby` when sorting via API. - */ - key: PropTypes.string, - /** - * The display label for this column. - */ - label: PropTypes.node, - /** - * Boolean, true if this column should always display in the table (not shown in toggle-able list). - */ - required: PropTypes.bool, - /** - * The label used for screen readers for this column. - */ - screenReaderLabel: PropTypes.string, - } ) - ), - /** - * A function called when sortable table headers are clicked, gets the `header.key` as argument. - */ - onSort: PropTypes.func, - /** - * The query string represented in object form - */ - query: PropTypes.object, - /** - * An array of arrays of display/value object pairs. - */ - rows: PropTypes.arrayOf( - PropTypes.arrayOf( - PropTypes.shape( { - /** - * Display value, used for rendering- strings or elements are best here. - */ - display: PropTypes.node, - /** - * "Real" value used for sorting, and should be a string or number. A column with `false` value will not be sortable. - */ - value: PropTypes.oneOfType( [ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ] ), - } ) - ) - ).isRequired, - /** - * Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col - * is checkboxes, for example). Set to false to disable row headers. - */ - rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ), - /** - * The rowKey used for the key value on each row, a function that returns the key. - * Defaults to index. - */ - rowKey: PropTypes.func, - /** - * Customize the message to show when there are no rows in the table. - */ - emptyMessage: PropTypes.string, -}; - -Table.defaultProps = { - ariaHidden: false, - headers: [], - onSort: noop, - query: {}, - rowHeader: 0, - emptyMessage: undefined, -}; - -export default withInstanceId( Table ); diff --git a/packages/js/components/src/table/table.tsx b/packages/js/components/src/table/table.tsx new file mode 100644 index 00000000000..6de75954e85 --- /dev/null +++ b/packages/js/components/src/table/table.tsx @@ -0,0 +1,374 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + createElement, + useRef, + Fragment, + useState, + useEffect, +} from '@wordpress/element'; +import classnames from 'classnames'; +import { Button } from '@wordpress/components'; +import { find, get, noop } from 'lodash'; +import { withInstanceId } from '@wordpress/compose'; +import { Icon, chevronUp, chevronDown } from '@wordpress/icons'; +import deprecated from '@wordpress/deprecated'; +import React from 'react'; + +/** + * Internal dependencies + */ +import { TableRow, TableProps } from './types'; + +const ASC = 'asc'; +const DESC = 'desc'; + +const getDisplay = ( cell: { display?: React.ReactNode } ) => + cell.display || null; + +/** + * A table component, without the Card wrapper. This is a basic table display, sortable, but no default filtering. + * + * Row data should be passed to the component as a list of arrays, where each array is a row in the table. + * Headers are passed in separately as an array of objects with column-related properties. For example, + * this data would render the following table. + * + * ```js + * const headers = [ { label: 'Month' }, { label: 'Orders' }, { label: 'Revenue' } ]; + * const rows = [ + * [ + * { display: 'January', value: 1 }, + * { display: 10, value: 10 }, + * { display: '$530.00', value: 530 }, + * ], + * [ + * { display: 'February', value: 2 }, + * { display: 13, value: 13 }, + * { display: '$675.00', value: 675 }, + * ], + * [ + * { display: 'March', value: 3 }, + * { display: 9, value: 9 }, + * { display: '$460.00', value: 460 }, + * ], + * ] + * ``` + * + * | Month | Orders | Revenue | + * | ---------|--------|---------| + * | January | 10 | $530.00 | + * | February | 13 | $675.00 | + * | March | 9 | $460.00 | + */ + +const Table: React.VFC< TableProps > = ( { + instanceId, + headers = [], + rows = [], + ariaHidden, + caption, + className, + onSort = ( f ) => f, + query = {}, + rowHeader, + rowKey, + emptyMessage, + ...props +} ) => { + const { classNames } = props; + const [ tabIndex, setTabIndex ] = useState< number | undefined >( + undefined + ); + const [ isScrollableRight, setIsScrollableRight ] = useState( false ); + const [ isScrollableLeft, setIsScrollableLeft ] = useState( false ); + + const container = useRef< HTMLDivElement >( null ); + + if ( classNames ) { + deprecated( `Table component's classNames prop`, { + since: '11.1.0', + version: '12.0.0', + alternative: 'className', + plugin: '@woocommerce/components', + } ); + } + + const classes = classnames( + 'woocommerce-table__table', + classNames, + className, + { + 'is-scrollable-right': isScrollableRight, + 'is-scrollable-left': isScrollableLeft, + } + ); + + const sortBy = ( key: string ) => { + return () => { + const currentKey = + query.orderby || + get( find( headers, { defaultSort: true } ), 'key', false ); + const currentDir = + query.order || + get( + find( headers, { key: currentKey } ), + 'defaultOrder', + DESC + ); + let dir = DESC; + if ( key === currentKey ) { + dir = DESC === currentDir ? ASC : DESC; + } + onSort( key, dir ); + }; + }; + + const getRowKey = ( row: TableRow[], index: number ) => { + if ( rowKey && typeof rowKey === 'function' ) { + return rowKey( row, index ); + } + return index; + }; + + const updateTableShadow = () => { + const table = container.current; + + if ( table?.scrollWidth && table?.scrollHeight && table?.offsetWidth ) { + const scrolledToEnd = + table?.scrollWidth - table?.scrollLeft <= table?.offsetWidth; + if ( scrolledToEnd && isScrollableRight ) { + setIsScrollableRight( false ); + } else if ( ! scrolledToEnd && ! isScrollableRight ) { + setIsScrollableRight( true ); + } + } + + if ( table?.scrollLeft ) { + const scrolledToStart = table?.scrollLeft <= 0; + if ( scrolledToStart && isScrollableLeft ) { + setIsScrollableLeft( false ); + } else if ( ! scrolledToStart && ! isScrollableLeft ) { + setIsScrollableLeft( true ); + } + } + }; + + const sortedBy = + query.orderby || + get( find( headers, { defaultSort: true } ), 'key', false ); + const sortDir = + query.order || + get( find( headers, { key: sortedBy } ), 'defaultOrder', DESC ); + const hasData = !! rows.length; + + useEffect( () => { + const scrollWidth = container.current?.scrollWidth; + const clientWidth = container.current?.clientWidth; + + if ( scrollWidth === undefined || clientWidth === undefined ) { + return; + } + + const scrollable = scrollWidth > clientWidth; + setTabIndex( scrollable ? 0 : undefined ); + updateTableShadow(); + window.addEventListener( 'resize', updateTableShadow ); + + return () => { + window.removeEventListener( 'resize', updateTableShadow ); + }; + }, [] ); + + useEffect( updateTableShadow, [ headers, rows, emptyMessage ] ); + + return ( +
+ + + + + { headers.map( ( header, i ) => { + const { + cellClassName, + isLeftAligned, + isSortable, + isNumeric, + key, + label, + screenReaderLabel, + } = header; + const labelId = `header-${ instanceId }-${ i }`; + const thProps: { [ key: string ]: string } = { + className: classnames( + 'woocommerce-table__header', + cellClassName, + { + 'is-left-aligned': + isLeftAligned || ! isNumeric, + 'is-sortable': isSortable, + 'is-sorted': sortedBy === key, + 'is-numeric': isNumeric, + } + ), + }; + if ( isSortable ) { + thProps[ 'aria-sort' ] = 'none'; + if ( sortedBy === key ) { + thProps[ 'aria-sort' ] = + sortDir === ASC + ? 'ascending' + : 'descending'; + } + } + // We only sort by ascending if the col is already sorted descending + const iconLabel = + sortedBy === key && sortDir !== ASC + ? sprintf( + __( + 'Sort by %s in ascending order', + 'woocommerce' + ), + screenReaderLabel || label + ) + : sprintf( + __( + 'Sort by %s in descending order', + 'woocommerce' + ), + screenReaderLabel || label + ); + + const textLabel = ( + + + { label } + + { screenReaderLabel && ( + + { screenReaderLabel } + + ) } + + ); + + return ( + + ); + } ) } + + { hasData ? ( + rows.map( ( row, i ) => ( + + { row.map( ( cell, j ) => { + const { + cellClassName, + isLeftAligned, + isNumeric, + } = headers[ j ]; + const isHeader = rowHeader === j; + const Cell = isHeader ? 'th' : 'td'; + const cellClasses = classnames( + 'woocommerce-table__item', + cellClassName, + { + 'is-left-aligned': + isLeftAligned || ! isNumeric, + 'is-numeric': isNumeric, + 'is-sorted': + sortedBy === headers[ j ].key, + } + ); + const cellKey = + getRowKey( row, i ).toString() + j; + return ( + + { getDisplay( cell ) } + + ); + } ) } + + ) ) + ) : ( + + + + ) } + +
+ { caption } + { tabIndex === 0 && ( + + { __( '(scroll to see more)', 'woocommerce' ) } + + ) } +
+ { isSortable ? ( + + + + { iconLabel } + + + ) : ( + textLabel + ) } +
+ { emptyMessage ?? + __( 'No data to display', 'woocommerce' ) } +
+
+ ); +}; + +export default withInstanceId( Table ); diff --git a/packages/js/components/src/table/types.ts b/packages/js/components/src/table/types.ts new file mode 100644 index 00000000000..79b61e92a3d --- /dev/null +++ b/packages/js/components/src/table/types.ts @@ -0,0 +1,189 @@ +export type QueryProps = { + orderby?: string; + order?: string; + page?: number; + per_page?: number; + /** + * Allowing string for backward compatibility + */ + paged?: number | string; +}; + +export type TableHeader = { + /** + * Boolean, true if this column is the default for sorting. Only one column should have this set. + */ + defaultSort?: boolean; + /** + * String, asc|desc if this column is the default for sorting. Only one column should have this set. + */ + defaultOrder?: string; + /** + * Boolean, true if this column should be aligned to the left. + */ + isLeftAligned?: boolean; + /** + * Boolean, true if this column is a number value. + */ + isNumeric?: boolean; + /** + * Boolean, true if this column is sortable. + */ + isSortable?: boolean; + /** + * The API parameter name for this column, passed to `orderby` when sorting via API. + */ + key: string; + /** + * The display label for this column. + */ + label?: React.ReactNode; + /** + * Boolean, true if this column should always display in the table (not shown in toggle-able list). + */ + required?: boolean; + /** + * The label used for screen readers for this column. + */ + screenReaderLabel?: string; + /** + * Additional classname for the header cell + */ + cellClassName?: string; + /** + * Boolean value to control visibility of a header + */ + visible?: boolean; +}; + +export type TableRow = { + /** + * Display value, used for rendering- strings or elements are best here. + */ + display?: React.ReactNode; + /** + * "Real" value used for sorting, and should be a string or number. A column with `false` value will not be sortable. + */ + value?: string | number | boolean; +}; + +/** + * Props shared between TableProps and TableCardProps. + */ +type CommonTableProps = { + /** + * The rowKey used for the key value on each row, a function that returns the key. + * Defaults to index. + */ + rowKey?: ( row: TableRow[], index: number ) => number; + /** + * Customize the message to show when there are no rows in the table. + */ + emptyMessage?: string; + /** + * The query string represented in object form + */ + query?: QueryProps; + /** + * Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col + * is checkboxes, for example). Set to false to disable row headers. + */ + rowHeader?: number | false; + /** + * An array of column headers (see `Table` props). + */ + headers?: Array< TableHeader >; + /** + * An array of arrays of display/value object pairs (see `Table` props). + */ + rows?: Array< Array< TableRow > >; + /** + * Additional CSS classes. + */ + className?: string; + /** + * A function called when sortable table headers are clicked, gets the `header.key` as argument. + */ + onSort?: ( key: string, direction: string ) => void; +}; + +export type TableProps = CommonTableProps & { + /** A unique ID for this instance of the component. This is automatically generated by withInstanceId. */ + instanceId: number | string; + /** + * Controls whether this component is hidden from screen readers. Used by the loading state, before there is data to read. + * Don't use this on real tables unless the table data is loaded elsewhere on the page. + */ + ariaHidden?: boolean; + /** + * A label for the content in this table + */ + caption?: string; + /** + * Additional classnames + */ + classNames?: string | Record< string, string >; +}; + +export type TableSummaryProps = { + // An array of objects with `label` & `value` properties, which display on a single line. + data: Array< { + label: string; + value: boolean | number | string | React.ReactNode; + } >; +}; + +export type TableCardProps = CommonTableProps & { + /** + * An array of custom React nodes that is placed at the top right corner. + */ + actions?: Array< React.ReactNode >; + /** + * If a search is provided in actions and should reorder actions on mobile. + */ + hasSearch?: boolean; + /** + * A list of IDs, matching to the row list so that ids[ 0 ] contains the object ID for the object displayed in row[ 0 ]. + */ + ids?: Array< number >; + /** + * Defines if the table contents are loading. + * It will display `TablePlaceholder` component instead of `Table` if that's the case. + */ + isLoading?: boolean; + /** + * A function which returns a callback function to update the query string for a given `param`. + */ + // Allowing any for backward compatibitlity + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onQueryChange?: ( param: string ) => ( ...props: any ) => void; + /** + * A function which returns a callback function which is called upon the user changing the visiblity of columns. + */ + onColumnsChange?: ( showCols: Array< string >, key?: string ) => void; + /** + * A callback function that is invoked when the current page is changed. + */ + onPageChange?: ( newPage: number, direction?: 'previous' | 'next' ) => void; + /** + * The total number of rows to display per page. + */ + rowsPerPage: number; + /** + * Boolean to determine whether or not ellipsis menu is shown. + */ + showMenu?: boolean; + /** + * An array of objects with `label` & `value` properties, which display in a line under the table. + * Optional, can be left off to show no summary. + */ + summary?: TableSummaryProps[ 'data' ]; + /** + * The title used in the card header, also used as the caption for the content in this table. + */ + title: string; + /** + * The total number of rows (across all pages). + */ + totalRows: number; +};