From e76c8da9fc715215dede6a769393196e50381bbf Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:56:51 -0700 Subject: [PATCH] [EuiTablePagination] Allow page size props to be configured via `EuiProvider.componentProps` (#6951) Co-authored-by: Tomasz Kajtoch --- .../pagination_bar.test.tsx.snap | 67 -- .../basic_table/pagination_bar.test.tsx | 88 +-- src/components/focus_trap/focus_trap.tsx | 16 +- src/components/portal/portal.tsx | 15 +- .../component_defaults.test.tsx | 141 +++- .../component_defaults/component_defaults.tsx | 49 +- .../table_pagination.test.tsx.snap | 631 ++++++++---------- .../table_pagination.test.tsx | 100 ++- .../table_pagination/table_pagination.tsx | 39 +- upcoming_changelogs/6951.md | 1 + 10 files changed, 597 insertions(+), 550 deletions(-) create mode 100644 upcoming_changelogs/6951.md diff --git a/src/components/basic_table/__snapshots__/pagination_bar.test.tsx.snap b/src/components/basic_table/__snapshots__/pagination_bar.test.tsx.snap index 2366e0d0f51a..5afd7642a419 100644 --- a/src/components/basic_table/__snapshots__/pagination_bar.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/pagination_bar.test.tsx.snap @@ -1,72 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PaginationBar render - custom page size options 1`] = ` -
- - -
-`; - -exports[`PaginationBar render - hiding per page options 1`] = ` -
- - -
-`; - -exports[`PaginationBar render - show all pageSize 1`] = ` -
- - -
-`; - exports[`PaginationBar renders 1`] = `
{ - it('renders', () => { - const props = { - ...requiredProps, - pagination: { - pageIndex: 0, - pageSize: 5, - totalItemCount: 0, - }, - onPageSizeChange: () => {}, - onPageChange: () => {}, - }; + const props = { + ...requiredProps, + pagination: { + pageIndex: 0, + pageSize: 5, + totalItemCount: 0, + }, + onPageSizeChange: () => {}, + onPageChange: () => {}, + }; + it('renders', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); - test('render - custom page size options', () => { - const props = { - pagination: { - pageIndex: 0, - pageSize: 5, - totalItemCount: 0, - pageSizeOptions: [1, 2, 3], - }, - onPageSizeChange: () => {}, - onPageChange: () => {}, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - test('render - hiding per page options', () => { - const props = { - pagination: { - pageIndex: 0, - pageSize: 5, - totalItemCount: 0, - showPerPageOptions: false, - }, - onPageSizeChange: () => {}, - onPageChange: () => {}, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - test('render - show all pageSize', () => { - const props = { - pagination: { - pageIndex: 0, - pageSize: 0, - pageSizeOptions: [1, 5, 0], - totalItemCount: 5, - }, - onPageSizeChange: () => {}, - onPageChange: () => {}, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); + it('calls onPageChange with the correct off-by-one offset', () => { + const onPageChange = jest.fn(); + const { getByLabelText } = render( + + ); + + fireEvent.click(getByLabelText('Page 2 of 2')); + expect(onPageChange).toHaveBeenCalledWith(1); }); }); diff --git a/src/components/focus_trap/focus_trap.tsx b/src/components/focus_trap/focus_trap.tsx index 52a68bca9ebb..e71a892de1e1 100644 --- a/src/components/focus_trap/focus_trap.tsx +++ b/src/components/focus_trap/focus_trap.tsx @@ -13,7 +13,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar'; import { CommonProps } from '../common'; import { findElementBySelectorOrRef, ElementTarget } from '../../services'; -import { useEuiComponentDefaults } from '../provider/component_defaults'; +import { usePropsWithComponentDefaults } from '../provider/component_defaults'; export type FocusTarget = ElementTarget; @@ -82,16 +82,12 @@ export type EuiFocusTrapProps = Omit< returnFocus?: ReactFocusOnProps['returnFocus']; }; -export const EuiFocusTrap: FunctionComponent = ({ - children, - ...props -}) => { - const { EuiFocusTrap: defaults } = useEuiComponentDefaults(); - return ( - - {children} - +export const EuiFocusTrap: FunctionComponent = (props) => { + const propsWithDefaults = usePropsWithComponentDefaults( + 'EuiFocusTrap', + props ); + return ; }; interface State { diff --git a/src/components/portal/portal.tsx b/src/components/portal/portal.tsx index bb3fae6706ce..d73863b6d06b 100644 --- a/src/components/portal/portal.tsx +++ b/src/components/portal/portal.tsx @@ -15,7 +15,7 @@ import React, { Component, FunctionComponent, ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { EuiNestedThemeContext } from '../../services'; -import { useEuiComponentDefaults } from '../provider/component_defaults'; +import { usePropsWithComponentDefaults } from '../provider/component_defaults'; const INSERT_POSITIONS = ['after', 'before'] as const; type EuiPortalInsertPosition = (typeof INSERT_POSITIONS)[number]; @@ -40,16 +40,9 @@ export interface EuiPortalProps { portalRef?: (ref: HTMLDivElement | null) => void; } -export const EuiPortal: FunctionComponent = ({ - children, - ...props -}) => { - const { EuiPortal: defaults } = useEuiComponentDefaults(); - return ( - - {children} - - ); +export const EuiPortal: FunctionComponent = (props) => { + const propsWithDefaults = usePropsWithComponentDefaults('EuiPortal', props); + return ; }; export class EuiPortalClass extends Component { diff --git a/src/components/provider/component_defaults/component_defaults.test.tsx b/src/components/provider/component_defaults/component_defaults.test.tsx index f35187ed0e32..57f1cf076322 100644 --- a/src/components/provider/component_defaults/component_defaults.test.tsx +++ b/src/components/provider/component_defaults/component_defaults.test.tsx @@ -11,41 +11,138 @@ import { renderHook } from '@testing-library/react-hooks'; import { EuiComponentDefaultsProvider, - useEuiComponentDefaults, + useComponentDefaults, + usePropsWithComponentDefaults, } from './component_defaults'; describe('EuiComponentDefaultsProvider', () => { - it('sets up context that allows accessing the passed `componentDefaults` from anywhere', () => { + describe('useComponentDefaults', () => { + it('allows accessing provided `componentDefaults` from anywhere', () => { + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ); + const { result } = renderHook(useComponentDefaults, { wrapper }); + + expect(result.current).toMatchInlineSnapshot(` + Object { + "EuiPortal": Object { + "insert": Object { + "position": "before", + "sibling":
, + }, + }, + } + `); + }); + }); + + describe('usePropsWithComponentDefaults', () => { const wrapper = ({ children }: PropsWithChildren<{}>) => ( {children} ); - const { result } = renderHook(useEuiComponentDefaults, { wrapper }); - - expect(result.current).toMatchInlineSnapshot(` - Object { - "EuiPortal": Object { - "insert": Object { - "position": "before", - "sibling":
, - }, - }, - } - `); + + it("returns a specific component's provided default props", () => { + const { result } = renderHook( + () => usePropsWithComponentDefaults('EuiTablePagination', {}), + { wrapper } + ); + + expect(result.current).toEqual({ + itemsPerPage: 20, + itemsPerPageOptions: [20, 40, 80, 0], + }); + }); + + it('correctly overrides defaults with actual props passed', () => { + const { result } = renderHook( + () => + usePropsWithComponentDefaults('EuiTablePagination', { + itemsPerPageOptions: [5, 10, 20], + }), + { wrapper } + ); + + expect(result.current).toEqual({ + itemsPerPage: 20, + itemsPerPageOptions: [5, 10, 20], + }); + }); + + it('correctly handles props without a default defined', () => { + const { result } = renderHook( + () => + usePropsWithComponentDefaults('EuiTablePagination', { + showPerPageOptions: false, + }), + { wrapper } + ); + + expect(result.current).toEqual({ + itemsPerPage: 20, + itemsPerPageOptions: [20, 40, 80, 0], + showPerPageOptions: false, + }); + }); + + it('correctly handles components with no defaults defined', () => { + const { result } = renderHook( + () => + usePropsWithComponentDefaults('EuiFocusTrap', { + children: 'test', + crossFrame: true, + }), + { wrapper } + ); + + expect(result.current).toEqual({ + children: 'test', + crossFrame: true, + }); + }); + + it('correctly handles no component defaults defined at all', () => { + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + {children} + ); + const { result } = renderHook( + () => + usePropsWithComponentDefaults('EuiFocusTrap', { + children: 'test', + gapMode: 'margin', + }), + { wrapper } + ); + + expect(result.current).toEqual({ + children: 'test', + gapMode: 'margin', + }); + }); }); // NOTE: Components are in charge of their own testing to ensure that the props - // coming from `useEuiComponentDefaults()` were properly applied. This file - // is simply a very light wrapper that carries prop data. - // @see `src/components/portal/portal.spec.tsx` as an example + // coming from the `componentDefaults` configuration were properly applied. + // Examples: + // @see src/components/portal/portal.spec.tsx + // @see src/components/table/table_pagination/table_pagination.test.tsx }); diff --git a/src/components/provider/component_defaults/component_defaults.tsx b/src/components/provider/component_defaults/component_defaults.tsx index 752fdbd8f3e4..da71ccfc71a1 100644 --- a/src/components/provider/component_defaults/component_defaults.tsx +++ b/src/components/provider/component_defaults/component_defaults.tsx @@ -6,24 +6,36 @@ * Side Public License, v 1. */ -import React, { createContext, useContext, FunctionComponent } from 'react'; +import React, { + createContext, + useContext, + useMemo, + FunctionComponent, +} from 'react'; -import { EuiPortalProps } from '../../portal'; -import { EuiFocusTrapProps } from '../../focus_trap'; +import type { EuiPortalProps } from '../../portal'; +import type { EuiFocusTrapProps } from '../../focus_trap'; +import type { EuiTablePaginationProps } from '../../table'; export type EuiComponentDefaults = { /** * Provide a global configuration for EuiPortal's default insertion position. */ - EuiPortal?: { insert: EuiPortalProps['insert'] }; + EuiPortal?: Pick; /** * Provide a global configuration for EuiFocusTrap's `gapMode` and `crossFrame` props */ EuiFocusTrap?: Pick; /** - * TODO + * Provide global settings for EuiTablePagination's props that affect page size + * / the rows per page selection. + * + * These defaults will be inherited all table and grid components that utilize EuiTablePagination. */ - EuiPagination?: unknown; + EuiTablePagination?: Pick< + EuiTablePaginationProps, + 'itemsPerPage' | 'itemsPerPageOptions' | 'showPerPageOptions' + >; }; // Declaring as a static const for reference integrity/reducing rerenders @@ -49,8 +61,29 @@ export const EuiComponentDefaultsProvider: FunctionComponent<{ }; /* - * Hook + * Hooks */ -export const useEuiComponentDefaults = () => { +export const useComponentDefaults = () => { return useContext(EuiComponentDefaultsContext); }; + +// Merge individual component props with component defaults +export const usePropsWithComponentDefaults = < + TComponentName extends keyof EuiComponentDefaults, + TComponentProps +>( + componentName: TComponentName, + props: TComponentProps +): TComponentProps => { + const context = useContext(EuiComponentDefaultsContext); + + const componentDefaults = context[componentName] ?? emptyDefaults; + + return useMemo( + () => ({ + ...componentDefaults, + ...props, + }), + [componentDefaults, props] + ); +}; diff --git a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap index 3536c41eb829..0186f4897931 100644 --- a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap +++ b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap @@ -1,398 +1,329 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiTablePagination is rendered 1`] = ` -
-
+exports[`EuiTablePagination renders 1`] = ` + +
- -
-
-
-
-
+
+
+
+ -
-
-`; - -exports[`EuiTablePagination is rendered when hiding the per page options 1`] = ` -
-
-
- + +
+
-
-`; - -exports[`EuiTablePagination renders a "show all" itemsPerPage option 1`] = ` -
+
+
+
-
-
+ `; diff --git a/src/components/table/table_pagination/table_pagination.test.tsx b/src/components/table/table_pagination/table_pagination.test.tsx index d05e77f59721..42ad1c6b9df7 100644 --- a/src/components/table/table_pagination/table_pagination.test.tsx +++ b/src/components/table/table_pagination/table_pagination.test.tsx @@ -7,9 +7,12 @@ */ import React from 'react'; +import { fireEvent } from '@testing-library/react'; import { render } from '../../../test/rtl'; import { requiredProps } from '../../../test/required_props'; +import { EuiProvider } from '../../provider'; + import { EuiTablePagination } from './table_pagination'; describe('EuiTablePagination', () => { @@ -18,16 +21,45 @@ describe('EuiTablePagination', () => { pageCount: 5, onChangePage: jest.fn(), }; - test('is rendered', () => { - const { container } = render( + + it('renders', () => { + const { getByTestSubject, baseElement } = render( ); - expect(container.firstChild).toMatchSnapshot(); + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(baseElement).toMatchSnapshot(); }); - test('is rendered when hiding the per page options', () => { - const { container } = render( + it('renders custom items per page / page sizes', () => { + const { getByText } = render( + + ); + + expect(getByText('Rows per page: 10')).toBeTruthy(); + }); + + it('renders custom items per page size options', () => { + const { getByTestSubject } = render( + + ); + + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByTestSubject('tablePagination-1-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-2-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-3-rows')).toBeTruthy(); + }); + + it('hides the per page options', () => { + const { queryByTestSubject } = render( { /> ); - expect(container.firstChild).toMatchSnapshot(); + expect(queryByTestSubject('tablePaginationPopoverButton')).toBe(null); }); - test('renders a "show all" itemsPerPage option', () => { - const { container } = render( + it('renders a "show all" itemsPerPage option', () => { + const { getByText, getByTestSubject } = render( { /> ); - expect(container.firstChild).toMatchSnapshot(); + expect(getByText('Showing all rows')).toBeTruthy(); + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByText('Show all rows')).toBeTruthy(); + }); + + describe('configurable defaults', () => { + test('itemsPerPage', () => { + const { getByText } = render( + + + , + { wrapper: undefined } + ); + + expect(getByText('Rows per page: 20')).toBeTruthy(); + }); + + test('itemsPerPageOptions', () => { + const { getByTestSubject } = render( + + + , + { wrapper: undefined } + ); + + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByTestSubject('tablePaginationRowOptions').textContent).toEqual( + '5 rows10 rows15 rows' + ); + }); + + test('showPerPageOptions', () => { + const { queryByTestSubject } = render( + + + , + { wrapper: undefined } + ); + + expect(queryByTestSubject('tablePaginationPopoverButton')).toBe(null); + }); }); }); diff --git a/src/components/table/table_pagination/table_pagination.tsx b/src/components/table/table_pagination/table_pagination.tsx index 3c94efc0cced..8b254a9756ec 100644 --- a/src/components/table/table_pagination/table_pagination.tsx +++ b/src/components/table/table_pagination/table_pagination.tsx @@ -20,6 +20,8 @@ import { EuiPagination, EuiPaginationProps } from '../../pagination'; import { EuiPopover } from '../../popover'; import { EuiI18n } from '../../i18n'; +import { usePropsWithComponentDefaults } from '../../provider/component_defaults'; + export type PageChangeHandler = EuiPaginationProps['onPageClick']; export type ItemsPerPageChangeHandler = (pageSize: number) => void; @@ -27,16 +29,22 @@ export interface EuiTablePaginationProps extends Omit { /** * Option to completely hide the "Rows per page" selector. + * + * @default true */ showPerPageOptions?: boolean; /** * Current selection for "Rows per page". * Pass `0` to display the selected "Show all" option and hide the pagination. + * + * @default 50 */ itemsPerPage?: number; /** * Custom array of options for "Rows per page". * Pass `0` as one of the options to create a "Show all" option. + * + * @default [10, 20, 50, 100] */ itemsPerPageOptions?: number[]; /** @@ -51,16 +59,20 @@ export interface EuiTablePaginationProps 'aria-label'?: string; } -export const EuiTablePagination: FunctionComponent = ({ - activePage, - itemsPerPage = 50, - itemsPerPageOptions = [10, 20, 50, 100], - showPerPageOptions = true, - onChangeItemsPerPage = () => {}, - onChangePage, - pageCount, - ...rest -}) => { +export const EuiTablePagination: FunctionComponent = ( + props +) => { + const { + activePage, + itemsPerPage = 50, + itemsPerPageOptions = [10, 20, 50, 100], + showPerPageOptions = true, + onChangeItemsPerPage, + onChangePage, + pageCount, + ...rest + } = usePropsWithComponentDefaults('EuiTablePagination', props); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const togglePopover = useCallback(() => { @@ -105,7 +117,7 @@ export const EuiTablePagination: FunctionComponent = ({ icon={itemsPerPageOption === itemsPerPage ? 'check' : 'empty'} onClick={() => { closePopover(); - onChangeItemsPerPage(itemsPerPageOption); + onChangeItemsPerPage?.(itemsPerPageOption); }} data-test-subj={`tablePagination-${itemsPerPageOption}-rows`} > @@ -134,7 +146,10 @@ export const EuiTablePagination: FunctionComponent = ({ panelPaddingSize="none" anchorPosition="upRight" > - + ); diff --git a/upcoming_changelogs/6951.md b/upcoming_changelogs/6951.md new file mode 100644 index 000000000000..98f082b1149a --- /dev/null +++ b/upcoming_changelogs/6951.md @@ -0,0 +1 @@ +- `EuiTablePagination`'s `itemsPerPage`, `itemsPerPageOptions`, and `showPerPageOptions` props can now be configured globally via `EuiProvider.componentDefaults`