diff --git a/.changeset/silver-wasps-greet.md b/.changeset/silver-wasps-greet.md new file mode 100644 index 00000000000..c81d001e472 --- /dev/null +++ b/.changeset/silver-wasps-greet.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Updated IndexTable, ResourceList, and DataTable to have built-in pagination props diff --git a/polaris-react/src/components/DataTable/DataTable.stories.tsx b/polaris-react/src/components/DataTable/DataTable.stories.tsx index 0f0558d20cf..0f111a97ce5 100644 --- a/polaris-react/src/components/DataTable/DataTable.stories.tsx +++ b/polaris-react/src/components/DataTable/DataTable.stories.tsx @@ -874,3 +874,46 @@ export function WithStickyHeaderEnabled() { ); } + +export function WithPagination() { + const rows = [ + ['Emerald Silk Gown', '$875.00', 124689, 140, '$122,500.00'], + ['Mauve Cashmere Scarf', '$230.00', 124533, 83, '$19,090.00'], + [ + 'Navy Merino Wool Blazer with khaki chinos and yellow belt', + '$445.00', + 124518, + 32, + '$14,240.00', + ], + ]; + + return ( + + + {}, + }} + /> + + + ); +} diff --git a/polaris-react/src/components/DataTable/DataTable.tsx b/polaris-react/src/components/DataTable/DataTable.tsx index 5dbff6297e4..fb6bd624a40 100644 --- a/polaris-react/src/components/DataTable/DataTable.tsx +++ b/polaris-react/src/components/DataTable/DataTable.tsx @@ -10,6 +10,8 @@ import {headerCell} from '../shared'; import {EventListener} from '../EventListener'; import {AfterInitialMount} from '../AfterInitialMount'; import {Sticky} from '../Sticky'; +import {Pagination} from '../Pagination'; +import type {PaginationProps} from '../Pagination'; import {Cell, Navigation} from './components'; import type {CellProps} from './components'; @@ -28,6 +30,8 @@ export type TableData = string | number | React.ReactNode; export type ColumnContentType = 'text' | 'numeric'; +export type DataTablePaginationProps = Omit; + const getRowClientHeights = (rows: NodeList | undefined) => { const heights: number[] = []; if (!rows) { @@ -97,6 +101,8 @@ export interface DataTableProps { fixedFirstColumns?: number; /** Specify a min width for the first column if neccessary */ firstColumnMinWidth?: string; + /** Properties to enable pagination at the bottom of the table. */ + pagination?: DataTablePaginationProps; } type CombinedProps = DataTableProps & { @@ -177,6 +183,7 @@ class DataTableInner extends PureComponent { hasZebraStripingOnData = false, stickyHeader = false, hasFixedFirstColumn: fixedFirstColumn = false, + pagination, } = this.props; const { condensed, @@ -300,6 +307,10 @@ class DataTableInner extends PureComponent {
{footerContent}
) : null; + const paginationMarkup = pagination ? ( + + ) : null; + const headerTotalsMarkup = !showTotalsInFooter ? totalsMarkup : null; const footerTotalsMarkup = showTotalsInFooter ? ( {totalsMarkup} @@ -398,6 +409,7 @@ class DataTableInner extends PureComponent { {footerTotalsMarkup} + {paginationMarkup} {footerMarkup} diff --git a/polaris-react/src/components/DataTable/tests/DataTable.test.tsx b/polaris-react/src/components/DataTable/tests/DataTable.test.tsx index 1ad973222ef..3bb16813c1b 100644 --- a/polaris-react/src/components/DataTable/tests/DataTable.test.tsx +++ b/polaris-react/src/components/DataTable/tests/DataTable.test.tsx @@ -3,6 +3,7 @@ import {timer} from '@shopify/jest-dom-mocks'; import {mountWithApp} from 'tests/utilities'; import {Checkbox} from '../../Checkbox'; +import {Pagination} from '../../Pagination'; import {Cell, Navigation} from '../components'; import {DataTable} from '../DataTable'; import type {DataTableProps} from '../DataTable'; @@ -639,4 +640,27 @@ describe('', () => { expect(tables).toHaveLength(1); }); }); + + describe('pagination', () => { + it('does not render Pagination when pagination props are not provided', () => { + const dataTable = mountWithApp(); + + expect(dataTable).not.toContainReactComponent(Pagination); + }); + + it('renders Pagination with table type when pagination props are provided', () => { + const paginationProps = { + hasNext: true, + }; + + const dataTable = mountWithApp( + , + ); + + expect(dataTable).toContainReactComponent(Pagination, { + type: 'table', + ...paginationProps, + }); + }); + }); }); diff --git a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx index 3273ac0f2cd..df4627eae0d 100644 --- a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx +++ b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx @@ -3929,3 +3929,116 @@ export function WithSubHeaders() { ); } + +export function WithPagination() { + const customers = [ + { + id: '3410', + url: '#', + name: 'Mae Jemison', + location: 'Decatur, USA', + orders: 20, + amountSpent: '$2,400', + }, + { + id: '3411', + url: '#', + name: 'Joe Jemison', + location: 'Sydney, AU', + orders: 20, + amountSpent: '$1,400', + }, + { + id: '3412', + url: '#', + name: 'Sam Jemison', + location: 'Decatur, USA', + orders: 20, + amountSpent: '$400', + }, + { + id: '3413', + url: '#', + name: 'Mae Jemison', + location: 'Decatur, USA', + orders: 20, + amountSpent: '$4,300', + }, + { + id: '2563', + url: '#', + name: 'Ellen Ochoa', + location: 'Los Angeles, USA', + orders: 30, + amountSpent: '$140', + }, + ]; + const resourceName = { + singular: 'customer', + plural: 'customers', + }; + + const {selectedResources, allResourcesSelected, handleSelectionChange} = + useIndexResourceState(customers); + + const rowMarkup = customers.map( + ({id, name, location, orders, amountSpent}, index) => ( + + + + {name} + + + {location} + + + {orders} + + + + + {amountSpent} + + + + ), + ); + + return ( + + {}, + }} + > + {rowMarkup} + + + ); +} diff --git a/polaris-react/src/components/IndexTable/IndexTable.tsx b/polaris-react/src/components/IndexTable/IndexTable.tsx index db7276dd631..75c0c5b007e 100644 --- a/polaris-react/src/components/IndexTable/IndexTable.tsx +++ b/polaris-react/src/components/IndexTable/IndexTable.tsx @@ -15,6 +15,8 @@ import {EventListener} from '../EventListener'; import {SelectAllActions} from '../SelectAllActions'; // eslint-disable-next-line import/no-deprecated import {LegacyStack} from '../LegacyStack'; +import {Pagination} from '../Pagination'; +import type {PaginationProps} from '../Pagination'; import {Sticky} from '../Sticky'; import {Spinner} from '../Spinner'; import {Text} from '../Text'; @@ -88,6 +90,8 @@ interface IndexTableSortToggleLabels { [key: number]: IndexTableSortToggleLabel; } +export type IndexTablePaginationProps = Omit; + export interface IndexTableBaseProps { headings: NonEmptyArray; promotedBulkActions?: BulkActionsProps['promotedActions']; @@ -118,6 +122,8 @@ export interface IndexTableBaseProps { sortToggleLabels?: IndexTableSortToggleLabels; /** Add zebra striping to table rows */ hasZebraStriping?: boolean; + /** Properties to enable pagination at the bottom of the table. */ + pagination?: IndexTablePaginationProps; } export interface TableHeadingRect { @@ -1162,21 +1168,29 @@ export function IndexTable({ hasMoreItems, condensed, onSelectionChange, + pagination, ...indexTableBaseProps }: IndexTableProps) { + const paginationMarkup = pagination ? ( + + ) : null; + return ( - - {children} - + <> + + {children} + + {paginationMarkup} + ); } diff --git a/polaris-react/src/components/IndexTable/tests/IndexTable.test.tsx b/polaris-react/src/components/IndexTable/tests/IndexTable.test.tsx index 7478a84185a..61801781d38 100644 --- a/polaris-react/src/components/IndexTable/tests/IndexTable.test.tsx +++ b/polaris-react/src/components/IndexTable/tests/IndexTable.test.tsx @@ -22,6 +22,7 @@ import {AfterInitialMount} from '../../AfterInitialMount'; import {UnstyledButton} from '../../UnstyledButton'; import {Tooltip} from '../../Tooltip'; import {IndexProvider} from '../../IndexProvider'; +import {Pagination} from '../../Pagination'; jest.mock('../utilities', () => ({ ...jest.requireActual('../utilities'), @@ -802,4 +803,33 @@ describe('', () => { expect(computeTableDimensions).toHaveBeenCalledTimes(2); }); }); + + describe('pagination', () => { + it('does not render Pagination when pagination props are not provided', () => { + const index = mountWithApp( + + {mockTableItems.map(mockRenderRow)} + , + ); + + expect(index).not.toContainReactComponent(Pagination); + }); + + it('renders Pagination with table type when pagination props are provided', () => { + const paginationProps = { + hasNext: true, + }; + + const index = mountWithApp( + + {mockTableItems.map(mockRenderRow)} + , + ); + + expect(index).toContainReactComponent(Pagination, { + type: 'table', + ...paginationProps, + }); + }); + }); }); diff --git a/polaris-react/src/components/Pagination/Pagination.scss b/polaris-react/src/components/Pagination/Pagination.scss index 6e3c3d4cc9e..e3ac706d98f 100644 --- a/polaris-react/src/components/Pagination/Pagination.scss +++ b/polaris-react/src/components/Pagination/Pagination.scss @@ -24,8 +24,10 @@ } &.table { + border-top: 1px solid var(--p-color-border); + button { - --button-min-height: var(--p-space-600); + --button-min-height: 28px; background-color: var(--p-color-bg-subdued); min-height: var(--button-min-height); min-width: var(--button-min-height); diff --git a/polaris-react/src/components/Pagination/Pagination.stories.tsx b/polaris-react/src/components/Pagination/Pagination.stories.tsx index f8a235c65e9..a547bb90437 100644 --- a/polaris-react/src/components/Pagination/Pagination.stories.tsx +++ b/polaris-react/src/components/Pagination/Pagination.stories.tsx @@ -64,7 +64,6 @@ export function WithTableType() { style={{ maxWidth: 'calc(700px + (2 * var(--p-space-400)))', margin: '0 auto', - border: '1px solid var(--p-color-border)', }} > + {}, + }} + renderItem={(item) => { + const {id, url, name, location} = item; + const media = ; + + return ( + +

+ + {name} + +

+
{location}
+
+ ); + }} + /> + + ); +} diff --git a/polaris-react/src/components/ResourceList/ResourceList.tsx b/polaris-react/src/components/ResourceList/ResourceList.tsx index 3b2eed60686..ab00d9d3c16 100644 --- a/polaris-react/src/components/ResourceList/ResourceList.tsx +++ b/polaris-react/src/components/ResourceList/ResourceList.tsx @@ -31,6 +31,8 @@ import {BulkActions, useIsBulkActionsSticky} from '../BulkActions'; import type {BulkActionsProps} from '../BulkActions'; import {SelectAllActions} from '../SelectAllActions'; import {CheckableButton} from '../CheckableButton'; +import {Pagination} from '../Pagination'; +import type {PaginationProps} from '../Pagination'; import styles from './ResourceList.scss'; @@ -66,6 +68,8 @@ function defaultIdForItem( : index.toString(); } +export type ResourceListPaginationProps = Omit; + export interface ResourceListProps< TItemType extends ResourceListItemData = ResourceListItemData, > { @@ -121,6 +125,8 @@ export interface ResourceListProps< idForItem?(item: TItemType, index: number): string; /** Function to resolve the ids of items */ resolveItemId?(item: TItemType): string; + /** Properties to enable pagination at the bottom of the list. */ + pagination?: ResourceListPaginationProps; } export function ResourceList({ @@ -148,6 +154,7 @@ export function ResourceList({ renderItem, idForItem = defaultIdForItem, resolveItemId, + pagination, }: ResourceListProps) { const i18n = useI18n(); const [selectMode, setSelectMode] = useState( @@ -737,6 +744,10 @@ export function ResourceList({ ) : null; + const paginationMarkup = pagination ? ( + + ) : null; + // This is probably a legit error but I don't have the time to refactor this // eslint-disable-next-line react/jsx-no-constructed-context-values const context = { @@ -767,6 +778,7 @@ export function ResourceList({ {emptySearchStateMarkup} {emptyStateMarkup} {loadingWithoutItemsMarkup} + {paginationMarkup}
diff --git a/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx b/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx index 280bed0a3e6..fa29ba97f37 100644 --- a/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx +++ b/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx @@ -13,6 +13,7 @@ import {EmptyState} from '../../EmptyState'; import {Select} from '../../Select'; import {Spinner} from '../../Spinner'; import {ResourceItem} from '../../ResourceItem'; +import {Pagination} from '../../Pagination'; import {SELECT_ALL_ITEMS} from '../../../utilities/resource-list'; import {ResourceList} from '../ResourceList'; import styles from '../ResourceList.scss'; @@ -1359,6 +1360,35 @@ describe('', () => { expect(computeTableDimensions).toHaveBeenCalledTimes(2); }); }); + + describe('pagination', () => { + it('does not render Pagination when pagination props are not provided', () => { + const resourceList = mountWithApp( + , + ); + + expect(resourceList).not.toContainReactComponent(Pagination); + }); + + it('renders Pagination with table type when pagination props are provided', () => { + const paginationProps = { + hasNext: true, + }; + + const resourceList = mountWithApp( + , + ); + + expect(resourceList).toContainReactComponent(Pagination, { + type: 'table', + ...paginationProps, + }); + }); + }); }); function noop() {} diff --git a/polaris.shopify.com/content/components/lists/resource-list.md b/polaris.shopify.com/content/components/lists/resource-list.md index 3c69a01f387..8e120502407 100644 --- a/polaris.shopify.com/content/components/lists/resource-list.md +++ b/polaris.shopify.com/content/components/lists/resource-list.md @@ -66,6 +66,9 @@ examples: - fileName: resource-list-with-all-of-its-elements.tsx title: With all of its elements description: Use as a broad example that includes most props available to resource list. + - fileName: resource-list-with-pagination.tsx + title: With pagination + description: Use when the list contains many rows and they need paginating. previewImg: /images/components/lists/resource-list.png --- diff --git a/polaris.shopify.com/content/components/tables/data-table.md b/polaris.shopify.com/content/components/tables/data-table.md index c2cb4e89838..c33aa92e7ce 100644 --- a/polaris.shopify.com/content/components/tables/data-table.md +++ b/polaris.shopify.com/content/components/tables/data-table.md @@ -42,6 +42,9 @@ examples: - fileName: data-table-with-sticky-header-enabled.tsx title: With sticky header enabled description: Use as a broad example that includes most props available to data table. + - fileName: data-table-with-pagination.tsx + title: With pagination + description: Use when the table contains many rows and they need paginating. previewImg: /images/components/tables/data-table.png --- diff --git a/polaris.shopify.com/content/components/tables/index-table.md b/polaris.shopify.com/content/components/tables/index-table.md index 64498ba1e4f..f4e6f727ef4 100644 --- a/polaris.shopify.com/content/components/tables/index-table.md +++ b/polaris.shopify.com/content/components/tables/index-table.md @@ -72,6 +72,9 @@ examples: - fileName: index-table-with-subheaders.tsx title: With subheaders description: An index table with multiple table headers. Use to present merchants with resources grouped by a relevant data value to enable faster bulk selection. + - fileName: index-table-with-pagination.tsx + title: With pagination + description: Use when the table contains many rows and they need paginating. previewImg: /images/components/tables/index-table.png --- diff --git a/polaris.shopify.com/pages/examples/data-table-with-pagination.tsx b/polaris.shopify.com/pages/examples/data-table-with-pagination.tsx new file mode 100644 index 00000000000..76613126f4c --- /dev/null +++ b/polaris.shopify.com/pages/examples/data-table-with-pagination.tsx @@ -0,0 +1,48 @@ +import {Page, LegacyCard, DataTable} from '@shopify/polaris'; +import React from 'react'; +import {withPolarisExample} from '../../src/components/PolarisExampleWrapper'; + +function DataTableWithPaginationExample() { + const rows = [ + ['Emerald Silk Gown', '$875.00', 124689, 140, '$122,500.00'], + ['Mauve Cashmere Scarf', '$230.00', 124533, 83, '$19,090.00'], + [ + 'Navy Merino Wool Blazer with khaki chinos and yellow belt', + '$445.00', + 124518, + 32, + '$14,240.00', + ], + ]; + + return ( + + + {}, + }} + /> + + + ); +} + +export default withPolarisExample(DataTableWithPaginationExample); diff --git a/polaris.shopify.com/pages/examples/index-table-with-pagination.tsx b/polaris.shopify.com/pages/examples/index-table-with-pagination.tsx new file mode 100644 index 00000000000..b162de459eb --- /dev/null +++ b/polaris.shopify.com/pages/examples/index-table-with-pagination.tsx @@ -0,0 +1,102 @@ +import { + IndexTable, + LegacyCard, + useIndexResourceState, + Text, + Badge, +} from '@shopify/polaris'; +import React from 'react'; +import {withPolarisExample} from '../../src/components/PolarisExampleWrapper'; + +function IndexTableWithPaginationExample() { + const orders = [ + { + id: '1020', + order: '#1020', + date: 'Jul 20 at 4:34pm', + customer: 'Jaydon Stanton', + total: '$969.44', + paymentStatus: Paid, + fulfillmentStatus: Unfulfilled, + }, + { + id: '1019', + order: '#1019', + date: 'Jul 20 at 3:46pm', + customer: 'Ruben Westerfelt', + total: '$701.19', + paymentStatus: Partially paid, + fulfillmentStatus: Unfulfilled, + }, + { + id: '1018', + order: '#1018', + date: 'Jul 20 at 3.44pm', + customer: 'Leo Carder', + total: '$798.24', + paymentStatus: Paid, + fulfillmentStatus: Unfulfilled, + }, + ]; + const resourceName = { + singular: 'order', + plural: 'orders', + }; + + const {selectedResources, allResourcesSelected, handleSelectionChange} = + useIndexResourceState(orders); + + const rowMarkup = orders.map( + ( + {id, order, date, customer, total, paymentStatus, fulfillmentStatus}, + index, + ) => ( + + + + {order} + + + {date} + {customer} + {total} + {paymentStatus} + {fulfillmentStatus} + + ), + ); + + return ( + + {}, + }} + > + {rowMarkup} + + + ); +} + +export default withPolarisExample(IndexTableWithPaginationExample); diff --git a/polaris.shopify.com/pages/examples/resource-list-with-pagination.tsx b/polaris.shopify.com/pages/examples/resource-list-with-pagination.tsx new file mode 100644 index 00000000000..99e8700ee1e --- /dev/null +++ b/polaris.shopify.com/pages/examples/resource-list-with-pagination.tsx @@ -0,0 +1,57 @@ +import { + LegacyCard, + ResourceList, + Avatar, + ResourceItem, + Text, +} from '@shopify/polaris'; +import React from 'react'; +import {withPolarisExample} from '../../src/components/PolarisExampleWrapper'; + +function ResourceListWithPaginationExample() { + return ( + + {}, + }} + renderItem={(item) => { + const {id, url, name, location} = item; + const media = ; + + return ( + + + {name} + +
{location}
+
+ ); + }} + /> +
+ ); +} + +export default withPolarisExample(ResourceListWithPaginationExample);