diff --git a/packages/big-design/src/components/Table/Actions/Actions.tsx b/packages/big-design/src/components/Table/Actions/Actions.tsx index 3f0489986..03de122f4 100644 --- a/packages/big-design/src/components/Table/Actions/Actions.tsx +++ b/packages/big-design/src/components/Table/Actions/Actions.tsx @@ -6,9 +6,10 @@ import { Pagination } from '../../Pagination'; import { Text } from '../../Typography'; import { TableItem, TablePagination, TableSelectable } from '../types'; -import { StyledActions } from './styled'; +import { StyledActions, StyledPaginationContainer } from './styled'; export interface ActionsProps { + itemName?: string; items: T[]; pagination?: TablePagination; selectable?: TableSelectable; @@ -16,7 +17,9 @@ export interface ActionsProps { } export const Actions = memo( - ({ selectable, pagination, tableId, items = [], ...props }: ActionsProps) => { + ({ selectable, pagination, tableId, itemName, items = [], ...props }: ActionsProps) => { + const totalItems = pagination ? pagination.totalItems : items.length; + const handleSelectAll = () => { if (!selectable) { return; @@ -31,20 +34,48 @@ export const Actions = memo( } }; - const renderSelectAllAction = ({ itemType, selectedItems }: TableSelectable) => { + const getSelectAllChecked = () => { + if (!selectable) { + return false; + } + + const { selectAllState, selectedItems } = selectable; + + switch (selectAllState) { + case 'ALL': + return true; + + case 'PARTIAL': + case 'NONE': + return false; + + default: + const totalSelectedItems = selectedItems.length; + const totalItemsInPage = items.length; + + return totalSelectedItems === totalItemsInPage && totalItemsInPage > 0; + } + }; + + const renderSelectAllAction = () => { + if (!selectable) { + return null; + } + + const { selectAllState, selectedItems } = selectable; const totalSelectedItems = selectedItems.length; const totalItemsInPage = items.length; - const isChecked = totalSelectedItems === totalItemsInPage && totalItemsInPage > 0; - const isIndeterminate = totalSelectedItems > 0 && totalSelectedItems !== totalItemsInPage; + + const isChecked = getSelectAllChecked(); + const isIndeterminate = + selectAllState === 'PARTIAL' || (totalSelectedItems > 0 && totalSelectedItems !== totalItemsInPage); return ( - + - {totalSelectedItems === 0 - ? `${totalItemsInPage} ${itemType}` - : `${totalSelectedItems}/${totalItemsInPage} ${itemType}`} + {totalSelectedItems === 0 ? `${totalItems}` : `${totalSelectedItems}/${totalItems}`} @@ -54,13 +85,27 @@ export const Actions = memo( const renderPagination = useMemo( () => pagination && ( - + - + ), [pagination], ); + const renderItemName = () => { + if (typeof itemName !== 'string') { + return null; + } + + const text = Boolean(selectable) ? itemName : `${totalItems} ${itemName}`; + + return ( + + {text} + + ); + }; + return ( - {selectable && renderSelectAllAction(selectable)} + {renderSelectAllAction()} + {renderItemName()} {renderPagination} ); diff --git a/packages/big-design/src/components/Table/Actions/styled.tsx b/packages/big-design/src/components/Table/Actions/styled.tsx index 452595f26..e05b477cc 100644 --- a/packages/big-design/src/components/Table/Actions/styled.tsx +++ b/packages/big-design/src/components/Table/Actions/styled.tsx @@ -4,5 +4,9 @@ import styled from 'styled-components'; import { Flex } from '../../Flex'; export const StyledActions = styled(Flex)``; +export const StyledPaginationContainer = styled(Flex.Item)` + margin-left: auto; +`; StyledActions.defaultProps = { theme: defaultTheme }; +StyledPaginationContainer.defaultProps = { theme: defaultTheme }; diff --git a/packages/big-design/src/components/Table/Table.tsx b/packages/big-design/src/components/Table/Table.tsx index f03f31b70..5edc59f0c 100644 --- a/packages/big-design/src/components/Table/Table.tsx +++ b/packages/big-design/src/components/Table/Table.tsx @@ -16,6 +16,7 @@ const InternalTable = (props: TableProps): React.ReactEl className, columns, id, + itemName, items, keyField = 'id', pagination, @@ -62,7 +63,7 @@ const InternalTable = (props: TableProps): React.ReactEl }; const shouldRenderActions = () => { - return Boolean(pagination) || Boolean(selectable); + return Boolean(pagination) || Boolean(selectable) || Boolean(itemName); }; const getItemKey = (item: T, index: number): string | number => { @@ -129,7 +130,13 @@ const InternalTable = (props: TableProps): React.ReactEl return ( <> {shouldRenderActions() && ( - + )} {renderHeaders()} diff --git a/packages/big-design/src/components/Table/__snapshots__/spec.tsx.snap b/packages/big-design/src/components/Table/__snapshots__/spec.tsx.snap index 5876eee09..24cd7c30f 100644 --- a/packages/big-design/src/components/Table/__snapshots__/spec.tsx.snap +++ b/packages/big-design/src/components/Table/__snapshots__/spec.tsx.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders a pagination component 1`] = ` -.c8 { +.c9 { vertical-align: middle; height: 2rem; width: 2rem; } -.c13 { +.c14 { vertical-align: middle; height: 1.5rem; width: 1.5rem; } -.c3 { +.c4 { box-sizing: border-box; } @@ -22,7 +22,7 @@ exports[`renders a pagination component 1`] = ` box-sizing: border-box; } -.c11 { +.c12 { -webkit-align-content: stretch; -ms-flex-line-pack: stretch; align-content: stretch; @@ -70,7 +70,7 @@ exports[`renders a pagination component 1`] = ` display: flex; } -.c4 { +.c5 { -webkit-align-content: stretch; -ms-flex-line-pack: stretch; align-content: stretch; @@ -94,7 +94,7 @@ exports[`renders a pagination component 1`] = ` display: flex; } -.c2 { +.c3 { -webkit-align-self: auto; -ms-flex-item-align: auto; align-self: auto; @@ -106,7 +106,7 @@ exports[`renders a pagination component 1`] = ` flex-shrink: 1; } -.c9 { +.c10 { border-radius: 0.25rem; box-shadow: 0px 1px 6px rgba(49,52,64,0.2); border: 0; @@ -132,7 +132,7 @@ exports[`renders a pagination component 1`] = ` z-index: 1070; } -.c10 { +.c11 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -153,17 +153,17 @@ exports[`renders a pagination component 1`] = ` padding: 0 1rem; } -.c10[aria-selected='true'] { +.c11[aria-selected='true'] { font-weight: 600; } -.c10[data-highlighted='true'], -.c10[data-highlighted='true'] a { +.c11[data-highlighted='true'], +.c11[data-highlighted='true'] a { background-color: #F0F3FF; color: #3C64F4; } -.c10 a { +.c11 a { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -181,7 +181,7 @@ exports[`renders a pagination component 1`] = ` width: 100%; } -.c6 { +.c7 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -228,37 +228,37 @@ exports[`renders a pagination component 1`] = ` color: #3C64F4; } -.c6:focus { +.c7:focus { outline: none; } -.c6[disabled] { +.c7[disabled] { border-color: #D9DCE9; pointer-events: none; } -.c6 + .bd-button { +.c7 + .bd-button { margin-top: 0.5rem; } -.c6:active { +.c7:active { background-color: #DBE3FE; } -.c6:focus { +.c7:focus { box-shadow: 0 0 0 0.25rem #DBE3FE; } -.c6:hover:not(:active) { +.c7:hover:not(:active) { background-color: #F0F3FF; } -.c6[disabled] { +.c7[disabled] { border-color: transparent; color: #D9DCE9; } -.c12 { +.c13 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -304,37 +304,37 @@ exports[`renders a pagination component 1`] = ` color: #3C64F4; } -.c12:focus { +.c13:focus { outline: none; } -.c12[disabled] { +.c13[disabled] { border-color: #D9DCE9; pointer-events: none; } -.c12 + .bd-button { +.c13 + .bd-button { margin-top: 0.5rem; } -.c12:active { +.c13:active { background-color: #DBE3FE; } -.c12:focus { +.c13:focus { box-shadow: 0 0 0 0.25rem #DBE3FE; } -.c12:hover:not(:active) { +.c13:hover:not(:active) { background-color: #F0F3FF; } -.c12[disabled] { +.c13[disabled] { border-color: transparent; color: #D9DCE9; } -.c7 { +.c8 { -webkit-align-content: center; -ms-flex-line-pack: center; align-content: center; @@ -348,33 +348,37 @@ exports[`renders a pagination component 1`] = ` visibility: visible; } -.c5 { +.c6 { color: #313440; width: auto; } +.c2 { + margin-left: auto; +} + @media (min-width:720px) { - .c6 + .bd-button { + .c7 + .bd-button { margin-top: 0; margin-left: 0.5rem; } } @media (min-width:720px) { - .c6 { + .c7 { width: auto; } } @media (min-width:720px) { - .c12 + .bd-button { + .c13 + .bd-button { margin-top: 0; margin-left: 0.5rem; } } @media (min-width:720px) { - .c12 { + .c13 { width: auto; } } @@ -384,30 +388,29 @@ exports[`renders a pagination component 1`] = ` class="c0 c1" >
`; diff --git a/packages/big-design/src/components/Table/spec.tsx b/packages/big-design/src/components/Table/spec.tsx index 1fbeecd09..1dfef4d75 100644 --- a/packages/big-design/src/components/Table/spec.tsx +++ b/packages/big-design/src/components/Table/spec.tsx @@ -6,17 +6,19 @@ import { Table } from './Table'; interface SimpleTableOptions { className?: string; + columns?: any[]; dataTestId?: string; id?: string; + itemName?: string; style?: CSSProperties; - columns?: any[]; } -const getSimpleTable = ({ className, columns, dataTestId, id, style }: SimpleTableOptions = {}) => ( +const getSimpleTable = ({ className, columns, dataTestId, id, itemName, style }: SimpleTableOptions = {}) => ( { `); }); +test('renders the total number of items + item name', () => { + const { getByText } = render(getSimpleTable({ itemName: 'Test Items' })); + + const itemNameNode = getByText(`5 Test Items`); + + expect(itemNameNode).toBeInTheDocument(); +}); + test('renders a pagination component', () => { const onItemsPerPageChange = jest.fn(); const onPageChange = jest.fn(); @@ -165,7 +175,7 @@ describe('selectable', () => { let columns: any; let items: any; let onSelectionChange: jest.Mock; - const itemType = 'Product'; + const itemName = 'Product'; beforeEach(() => { onSelectionChange = jest.fn(); @@ -187,9 +197,9 @@ describe('selectable', () => { const { container, getAllByRole } = render(
{ const { getAllByRole } = render(
{ const { getAllByRole } = render(
{ fireEvent.click(selectAllCheckbox); expect(onSelectionChange).toHaveBeenCalledWith([]); }); + + test('passing a selectAllState overrides the checkbox state', () => { + const { getAllByRole } = render( +
, + ); + + const [selectAllCheckbox] = getAllByRole('checkbox') as HTMLInputElement[]; + + expect(selectAllCheckbox.checked).toBe(true); + }); }); describe('sortable', () => { diff --git a/packages/big-design/src/components/Table/types.ts b/packages/big-design/src/components/Table/types.ts index d07726a74..d75570cd1 100644 --- a/packages/big-design/src/components/Table/types.ts +++ b/packages/big-design/src/components/Table/types.ts @@ -4,7 +4,7 @@ import { MarginProps } from '../../mixins'; import { PaginationProps } from '../Pagination'; export interface TableSelectable { - itemType?: string; + selectAllState?: 'ALL' | 'PARTIAL' | 'NONE'; selectedItems: T[]; onSelectionChange(selectedItems: T[]): void; } @@ -37,6 +37,7 @@ export type TablePagination = Omit; export interface TableProps extends React.TableHTMLAttributes, MarginProps { columns: Array>; + itemName?: string; items: T[]; keyField?: string; pagination?: TablePagination; diff --git a/packages/docs/PropTables/TablePropTable.tsx b/packages/docs/PropTables/TablePropTable.tsx index 97430f326..5625789ba 100644 --- a/packages/docs/PropTables/TablePropTable.tsx +++ b/packages/docs/PropTables/TablePropTable.tsx @@ -17,6 +17,11 @@ const tableProps: Prop[] = [ description: 'The array of items to display.', required: true, }, + { + name: 'itemName', + types: 'string', + description: 'Item name displayed on the table actions section.', + }, { name: 'keyField', types: 'string', @@ -112,16 +117,18 @@ const tableSelectableProps: Prop[] = [ name: 'selectedItems', types: 'Item[]', description: 'Defines which items are selected.', + required: true, }, { name: 'onSelectionChange', types: '(selectedItems: Item[]) => void', description: 'Function to be called when item selection changes.', + required: true, }, { - name: 'itemType', - types: 'string', - description: 'Item type name displayed on the selected action section. (Eg: 2/10 Products)', + name: 'selectAllState', + types: ['ALL', 'PARTIAL', 'NONE'], + description: 'Used to manually override the select all checkbox state. Useful for multi-page selects.', }, ]; @@ -141,6 +148,7 @@ const tableSortableProps: Prop[] = [ name: 'onSort', types: '(columnHash: string, direction: TableSortDirection, column: TableColumn): void;', description: 'Function to be called when a sortable header is clicked.', + required: true, }, ]; diff --git a/packages/docs/pages/Table/TablePage.tsx b/packages/docs/pages/Table/TablePage.tsx index 5c440bf2e..6cce593b9 100644 --- a/packages/docs/pages/Table/TablePage.tsx +++ b/packages/docs/pages/Table/TablePage.tsx @@ -129,8 +129,8 @@ export default () => { keyField="sku" columns={columns} items={currentItems} + itemName="Products" selectable={{ - itemType: 'Products', onSelectionChange: setSelectedItems, selectedItems, }}