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 <chihsuan.tw@gmail.com>

* Update packages/js/components/src/table/empty.tsx

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Remove unnecessary empty space and convert comment stlye

* Update packages/js/components/src/table/index.tsx

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Remove unnecessary type casting

* Type defaultOnQueryChange func correctly

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
This commit is contained in:
Moon 2023-01-19 13:05:12 -08:00 committed by GitHub
parent a7b1beaa05
commit 0d67a6aaf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1018 additions and 1041 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Migrate Table component to TS

View File

@ -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 (
<div
className="woocommerce-table is-empty"
style={ { '--number-of-rows': numberOfRows } }
style={
{ '--number-of-rows': numberOfRows } as React.CSSProperties
}
>
{ children }
</div>
);
};
EmptyTable.propTypes = {
/**
* An integer with the number of rows the box should occupy.
*/
numberOfRows: PropTypes.number,
};
EmptyTable.defaultProps = {
numberOfRows: 5,
};
export default EmptyTable;

View File

@ -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, `<Table />`, `<TableSummary />`, and `<Pagination />`.
* 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 (
<Card className={ classes }>
<CardHeader>
<Text size={ 16 } weight={ 600 } as="h2" color="#23282d">
{ title }
</Text>
<div className="woocommerce-table__actions">
{ actions }
</div>
{ showMenu && (
<EllipsisMenu
label={ __(
'Choose which values to display',
'woocommerce'
) }
renderContent={ () => (
<Fragment>
<MenuTitle>
{ __( 'Columns:', 'woocommerce' ) }
</MenuTitle>
{ allHeaders.map(
( { key, label, required } ) => {
if ( required ) {
return null;
}
return (
<MenuItem
checked={ showCols.includes(
key
) }
isCheckbox
isClickable
key={ key }
onInvoke={ this.onColumnToggle(
key
) }
>
{ label }
</MenuItem>
);
}
) }
</Fragment>
) }
/>
) }
</CardHeader>
<CardBody size={ null }>
{ isLoading ? (
<Fragment>
<span className="screen-reader-text">
{ __(
'Your requested data is loading',
'woocommerce'
) }
</span>
<TablePlaceholder
numberOfRows={ rowsPerPage }
headers={ headers }
rowHeader={ rowHeader }
caption={ title }
query={ query }
/>
</Fragment>
) : (
<Table
rows={ rows }
headers={ headers }
rowHeader={ rowHeader }
caption={ title }
query={ query }
onSort={ onSort || onQueryChange( 'sort' ) }
rowKey={ rowKey }
emptyMessage={ emptyMessage }
/>
) }
</CardBody>
<CardFooter justify="center">
{ isLoading ? (
<TableSummaryPlaceholder />
) : (
<Fragment>
<Pagination
key={ parseInt( query.paged, 10 ) || 1 }
page={ parseInt( query.paged, 10 ) || 1 }
perPage={ rowsPerPage }
total={ totalRows }
onPageChange={ this.onPageChange }
onPerPageChange={ onQueryChange( 'per_page' ) }
/>
{ summary && <TableSummary data={ summary } /> }
</Fragment>
) }
</CardFooter>
</Card>
);
}
}
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;

View File

@ -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, `<Table />`, `<TableSummary />`, and `<Pagination />`.
* 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 (
<Card className={ classes }>
<CardHeader>
<Text size={ 16 } weight={ 600 } as="h2" color="#23282d">
{ title }
</Text>
<div className="woocommerce-table__actions">{ actions }</div>
{ showMenu && (
<EllipsisMenu
label={ __(
'Choose which values to display',
'woocommerce'
) }
renderContent={ () => (
<Fragment>
{ /* @ts-expect-error: Ignoring the error until we migrate ellipsis-menu to TS*/ }
<MenuTitle>
{ /* @ts-expect-error: Allow string */ }
{ __( 'Columns:', 'woocommerce' ) }
</MenuTitle>
{ allHeaders.map(
( { key, label, required } ) => {
if ( required ) {
return null;
}
return (
<MenuItem
checked={ showCols.includes(
key
) }
isCheckbox
isClickable
key={ key }
onInvoke={
key !== undefined
? onColumnToggle( key )
: undefined
}
>
{ label }
</MenuItem>
);
}
) }
</Fragment>
) }
/>
) }
</CardHeader>
{ /* Ignoring the error to make it backward compatible for now. */ }
{ /* @ts-expect-error: size must be one of small, medium, largel, xSmall, extraSmall. */ }
<CardBody size={ null }>
{ isLoading ? (
<Fragment>
<span className="screen-reader-text">
{ __(
'Your requested data is loading',
'woocommerce'
) }
</span>
<TablePlaceholder
numberOfRows={ rowsPerPage }
headers={ visibleHeaders }
rowHeader={ rowHeader }
caption={ title }
query={ query }
/>
</Fragment>
) : (
<Table
rows={ visibleRows as TableCardProps[ 'rows' ] }
headers={
visibleHeaders as TableCardProps[ 'headers' ]
}
rowHeader={ rowHeader }
caption={ title }
query={ query }
onSort={
onSort ||
( onQueryChange( 'sort' ) as (
key: string,
direction: string
) => void )
}
rowKey={ rowKey }
emptyMessage={ emptyMessage }
/>
) }
</CardBody>
{ /* @ts-expect-error: justify is missing from the latest @types/wordpress__components */ }
<CardFooter justify="center">
{ isLoading ? (
<TableSummaryPlaceholder />
) : (
<Fragment>
<Pagination
key={ parseInt( query.paged as string, 10 ) || 1 }
page={ parseInt( query.paged as string, 10 ) || 1 }
perPage={ rowsPerPage }
total={ totalRows }
onPageChange={ onPageChange }
onPerPageChange={ onQueryChange( 'per_page' ) }
/>
{ summary && <TableSummary data={ summary } /> }
</Fragment>
) }
</CardFooter>
</Card>
);
};
export default TableCard;

View File

@ -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: <span className="is-placeholder" />,
} ) )
);
return (
<Table
ariaHidden={ true }
className="is-loading"
rows={ rows }
{ ...tableProps }
/>
);
}
}
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;

View File

@ -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: <span className="is-placeholder" />,
} ) )
);
const tableProps = { query, caption, headers, numberOfRows, ...props };
return (
<Table
ariaHidden={ true }
className="is-loading"
rows={ rows }
{ ...tableProps }
/>
);
};
export default TablePlaceholder;

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { EmptyTable } from '@woocommerce/components';
import { createElement } from '@wordpress/element';
export const Basic = () => <EmptyTable>There are no entries.</EmptyTable>;

View File

@ -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 (
<TableCard
title="Revenue last week"
rows={ rows }
headers={ headers }
onQueryChange={ ( param ) => ( value ) =>
setState( {
query: {
[ param ]: value,
},
} ) }
query={ query }
rowsPerPage={ 7 }
totalRows={ 10 }
summary={ summary }
/>
);
};
export const Basic = () => <TableCardExample />;
export default {
title: 'WooCommerce Admin/components/TableCard',
component: TableCard,
};

View File

@ -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 (
<TableCard
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 }
/>
);
};
const TableCardWithActionsExample = () => {
const [ { query }, setState ] = useState( {
query: {
paged: 1,
},
} );
const [ action1Text, setAction1Text ] = useState( 'Action 1' );
const [ action2Text, setAction2Text ] = useState( 'Action 2' );
return (
<TableCard
actions={ [
<Button
key={ 0 }
onClick={ () => {
setAction1Text( 'Action 1 Clicked' );
} }
>
{ action1Text }
</Button>,
<Button
key={ 0 }
onClick={ () => {
setAction2Text( 'Action 2 Clicked' );
} }
>
{ action2Text }
</Button>,
] }
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 = () => <TableCardExample />;
export const Actions = () => <TableCardWithActionsExample />;
export default {
title: 'WooCommerce Admin/components/TableCard',
component: TableCard,
};

View File

@ -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 = () => (
<Card size={ null }>
<TablePlaceholder caption="Revenue last week" headers={ headers } />
</Card>
);
export const Basic = () => {
return (
/* @ts-expect-error: size must be one of small, medium, largel, xSmall, extraSmall. */
<Card size={ null }>
<TablePlaceholder caption="Revenue last week" headers={ headers } />
</Card>
);
};
export default {
title: 'WooCommerce Admin/components/TablePlaceholder',

View File

@ -3,14 +3,18 @@
*/
import { Card, CardFooter } from '@wordpress/components';
import { TableSummaryPlaceholder } from '@woocommerce/components';
import { createElement } from '@wordpress/element';
export const Basic = () => (
<Card>
<CardFooter justify="center">
<TableSummaryPlaceholder />
</CardFooter>
</Card>
);
export const Basic = () => {
return (
<Card>
{ /* @ts-expect-error: justify is missing from the latest type def. */ }
<CardFooter justify="center">
<TableSummaryPlaceholder />
</CardFooter>
</Card>
);
};
export default {
title: 'WooCommerce Admin/components/TableSummaryPlaceholder',

View File

@ -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 = () => (
</Card>
);
export const NoDataCustomMessage = () => (
<Card size={ null }>
<Table
caption="Revenue last week"
rows={ [] }
headers={ headers }
rowKey={ ( row ) => row[ 0 ].value }
emptyMessage="Custom empty message"
/>
</Card>
);
export const NoDataCustomMessage = () => {
return (
/* @ts-expect-error: size must be one of small, medium, largel, xSmall, extraSmall. */
<Card size={ null }>
<Table
caption="Revenue last week"
rows={ [] }
headers={ headers }
rowKey={ ( row ) => row[ 0 ].value }
emptyMessage="Custom empty message"
/>
</Card>
);
};
export default {
title: 'WooCommerce Admin/components/Table',

View File

@ -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 (
<ul className="woocommerce-table__summary" role="complementary">
{ 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;
/**

View File

@ -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 (
<div
className={ classes }
ref={ this.container }
tabIndex={ tabIndex }
aria-hidden={ ariaHidden }
aria-labelledby={ `caption-${ instanceId }` }
role="group"
onScroll={ this.updateTableShadow }
>
<table>
<caption
id={ `caption-${ instanceId }` }
className="woocommerce-table__caption screen-reader-text"
>
{ caption }
{ tabIndex === '0' && (
<small>
{ __( '(scroll to see more)', 'woocommerce' ) }
</small>
) }
</caption>
<tbody>
<tr>
{ 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 = (
<Fragment>
<span
aria-hidden={ Boolean(
screenReaderLabel
) }
>
{ label }
</span>
{ screenReaderLabel && (
<span className="screen-reader-text">
{ screenReaderLabel }
</span>
) }
</Fragment>
);
return (
<th
role="columnheader"
scope="col"
key={ header.key || i }
{ ...thProps }
>
{ isSortable ? (
<Fragment>
<Button
aria-describedby={ labelId }
onClick={
hasData
? this.sortBy( key )
: noop
}
>
{ sortedBy === key &&
sortDir === ASC ? (
<Icon
icon={ chevronUp }
/>
) : (
<Icon
icon={ chevronDown }
/>
) }
{ textLabel }
</Button>
<span
className="screen-reader-text"
id={ labelId }
>
{ iconLabel }
</span>
</Fragment>
) : (
textLabel
) }
</th>
);
} ) }
</tr>
{ hasData ? (
rows.map( ( row, i ) => (
<tr key={ this.getRowKey( 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 (
<Cell
scope={
isHeader ? 'row' : null
}
key={ cellKey }
className={ cellClasses }
>
{ getDisplay( cell ) }
</Cell>
);
} ) }
</tr>
) )
) : (
<tr>
<td
className="woocommerce-table__empty-item"
colSpan={ headers.length }
>
{ emptyMessage ??
__(
'No data to display',
'woocommerce'
) }
</td>
</tr>
) }
</tbody>
</table>
</div>
);
}
}
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 );

View File

@ -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 (
<div
className={ classes }
ref={ container }
tabIndex={ tabIndex }
aria-hidden={ ariaHidden }
aria-labelledby={ `caption-${ instanceId }` }
role="group"
onScroll={ updateTableShadow }
>
<table>
<caption
id={ `caption-${ instanceId }` }
className="woocommerce-table__caption screen-reader-text"
>
{ caption }
{ tabIndex === 0 && (
<small>
{ __( '(scroll to see more)', 'woocommerce' ) }
</small>
) }
</caption>
<tbody>
<tr>
{ 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 = (
<Fragment>
<span
aria-hidden={ Boolean(
screenReaderLabel
) }
>
{ label }
</span>
{ screenReaderLabel && (
<span className="screen-reader-text">
{ screenReaderLabel }
</span>
) }
</Fragment>
);
return (
<th
role="columnheader"
scope="col"
key={ header.key || i }
{ ...thProps }
>
{ isSortable ? (
<Fragment>
<Button
aria-describedby={ labelId }
onClick={
hasData
? sortBy( key )
: noop
}
>
{ sortedBy === key &&
sortDir === ASC ? (
<Icon icon={ chevronUp } />
) : (
<Icon
icon={ chevronDown }
/>
) }
{ textLabel }
</Button>
<span
className="screen-reader-text"
id={ labelId }
>
{ iconLabel }
</span>
</Fragment>
) : (
textLabel
) }
</th>
);
} ) }
</tr>
{ hasData ? (
rows.map( ( row, i ) => (
<tr key={ getRowKey( 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 (
<Cell
scope={
isHeader ? 'row' : undefined
}
key={ cellKey }
className={ cellClasses }
>
{ getDisplay( cell ) }
</Cell>
);
} ) }
</tr>
) )
) : (
<tr>
<td
className="woocommerce-table__empty-item"
colSpan={ headers.length }
>
{ emptyMessage ??
__( 'No data to display', 'woocommerce' ) }
</td>
</tr>
) }
</tbody>
</table>
</div>
);
};
export default withInstanceId( Table );

View File

@ -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;
};