Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiTablePagination] Allow page size props to be configured via EuiProvider.componentProps #6951

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,72 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`PaginationBar render - custom page size options 1`] = `
<div>
<EuiSpacer
size="m"
/>
<EuiTablePagination
activePage={0}
itemsPerPage={5}
itemsPerPageOptions={
Array [
1,
2,
3,
]
}
onChangeItemsPerPage={[Function]}
onChangePage={[Function]}
pageCount={0}
/>
</div>
`;

exports[`PaginationBar render - hiding per page options 1`] = `
<div>
<EuiSpacer
size="m"
/>
<EuiTablePagination
activePage={0}
itemsPerPage={5}
itemsPerPageOptions={
Array [
10,
25,
50,
]
}
onChangeItemsPerPage={[Function]}
onChangePage={[Function]}
pageCount={0}
showPerPageOptions={false}
/>
</div>
`;

exports[`PaginationBar render - show all pageSize 1`] = `
<div>
<EuiSpacer
size="m"
/>
<EuiTablePagination
activePage={0}
itemsPerPage={0}
itemsPerPageOptions={
Array [
1,
5,
0,
]
}
onChangeItemsPerPage={[Function]}
onChangePage={[Function]}
pageCount={1}
/>
</div>
`;

exports[`PaginationBar renders 1`] = `
<div>
<div
Expand Down
88 changes: 27 additions & 61 deletions src/components/basic_table/pagination_bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,78 +7,44 @@
*/

import React from 'react';
import { shallow } from 'enzyme';
import { fireEvent } from '@testing-library/react';
import { render } from '../../test/rtl';
import { requiredProps } from '../../test';

import { PaginationBar } from './pagination_bar';

describe('PaginationBar', () => {
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(<PaginationBar {...props} />);

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(<PaginationBar {...props} />);

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(<PaginationBar {...props} />);

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(<PaginationBar {...props} />);

expect(component).toMatchSnapshot();
it('calls onPageChange with the correct off-by-one offset', () => {
const onPageChange = jest.fn();
const { getByLabelText } = render(
<PaginationBar
{...props}
pagination={{
...props.pagination,
totalItemCount: 10,
}}
onPageChange={onPageChange}
/>
);

fireEvent.click(getByLabelText('Page 2 of 2'));
expect(onPageChange).toHaveBeenCalledWith(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@ import { renderHook } from '@testing-library/react-hooks';
import {
EuiComponentDefaultsProvider,
useEuiComponentDefaults,
EUI_COMPONENT_DEFAULTS,
} from './component_defaults';

describe('EuiComponentDefaultsProvider', () => {
it('sets up context that allows accessing the passed `componentDefaults` from anywhere', () => {
it('returns baseline EUI defaults if no consumer configurations are passed', () => {
const wrapper = ({ children }: PropsWithChildren<{}>) => (
<EuiComponentDefaultsProvider componentDefaults={undefined}>
{children}
</EuiComponentDefaultsProvider>
);
const { result } = renderHook(useEuiComponentDefaults, { wrapper });

expect(result.current).toEqual(EUI_COMPONENT_DEFAULTS);
});

it('overrides undefined EUI defaults with consumer defaults', () => {
const wrapper = ({ children }: PropsWithChildren<{}>) => (
<EuiComponentDefaultsProvider
componentDefaults={{
Expand All @@ -32,20 +44,59 @@ describe('EuiComponentDefaultsProvider', () => {
);
const { result } = renderHook(useEuiComponentDefaults, { wrapper });

expect(result.current).toMatchInlineSnapshot(`
expect(result.current.EuiPortal).toMatchInlineSnapshot(`
Object {
"EuiPortal": Object {
"insert": Object {
"position": "before",
"sibling": <div />,
},
"insert": Object {
"position": "before",
"sibling": <div />,
},
}
`);
});

it('does not override existing EUI defaults if a consumer does not configure the component', () => {
const wrapper = ({ children }: PropsWithChildren<{}>) => (
<EuiComponentDefaultsProvider
componentDefaults={{ EuiFocusTrap: { gapMode: 'margin' } }}
>
{children}
</EuiComponentDefaultsProvider>
);
const { result } = renderHook(useEuiComponentDefaults, { wrapper });

// Should not override other existing EUI component defaults
expect(result.current.EuiTablePagination).toEqual(
EUI_COMPONENT_DEFAULTS.EuiTablePagination
);
// Should set/override the passed defaults
expect(result.current.EuiFocusTrap).toEqual({ gapMode: 'margin' });
});

it('does not override all other existing EUI default props if a consumer only configures a subset of the component props', () => {
const wrapper = ({ children }: PropsWithChildren<{}>) => (
<EuiComponentDefaultsProvider
componentDefaults={{
// Note: This prop is specifically chosen as an array / as a regression test
// against recursive object/array merging. We should only be merging 2 levels
// deep into each component, otherwise the consumer `itemsPerPageOptions` will
// incorrectly merge into EUI's array instead of overriding
EuiTablePagination: { itemsPerPageOptions: [0, 10] },
}}
>
{children}
</EuiComponentDefaultsProvider>
);
const { result } = renderHook(useEuiComponentDefaults, { wrapper });

expect(result.current.EuiTablePagination).toEqual({
...EUI_COMPONENT_DEFAULTS.EuiTablePagination,
itemsPerPageOptions: [0, 10],
});
});

// 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 `useEuiComponentDefaults()` were properly applied.
// Examples:
// @see src/components/portal/portal.spec.tsx
// @see src/components/table/table_pagination/table_pagination.test.tsx
});
73 changes: 62 additions & 11 deletions src/components/provider/component_defaults/component_defaults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,92 @@
* 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<EuiPortalProps, 'insert'>;
/**
* Provide a global configuration for EuiFocusTrap's `gapMode` and `crossFrame` props
*/
EuiFocusTrap?: Pick<EuiFocusTrapProps, 'gapMode' | 'crossFrame'>;
/**
* 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
const emptyDefaults = {};
/**
* The above types are external/consumer facing and have many optional props
* The below types reflect props EUI provides defaults for and should always
* be present within the internal context
*/
type _EuiComponentDefaults = Required<EuiComponentDefaults> & {
EuiTablePagination: Required<EuiComponentDefaults['EuiTablePagination']>;
};

export const EUI_COMPONENT_DEFAULTS: _EuiComponentDefaults = {
EuiPortal: {},
EuiFocusTrap: {},
EuiTablePagination: {
itemsPerPage: 50,
itemsPerPageOptions: [10, 20, 50, 100],
showPerPageOptions: true,
},
};
cee-chen marked this conversation as resolved.
Show resolved Hide resolved

/*
* Context
*/
export const EuiComponentDefaultsContext =
createContext<EuiComponentDefaults>(emptyDefaults);
export const EuiComponentDefaultsContext = createContext<_EuiComponentDefaults>(
EUI_COMPONENT_DEFAULTS
);

/*
* Component
*/
export const EuiComponentDefaultsProvider: FunctionComponent<{
componentDefaults?: EuiComponentDefaults;
}> = ({ componentDefaults = emptyDefaults, children }) => {
}> = ({ componentDefaults: configuredDefaults, children }) => {
// Merge consumer configured component props with baseline EUI component props
const componentDefaults: _EuiComponentDefaults = useMemo(() => {
if (!configuredDefaults) return EUI_COMPONENT_DEFAULTS;

const mergedDefaults: _EuiComponentDefaults = { ...EUI_COMPONENT_DEFAULTS };

Object.entries(configuredDefaults).forEach(
([componentName, componentProps]) => {
Object.entries(componentProps as any).forEach(
([propName, propValue]) => {
if (propValue !== undefined) {
// @ts-ignore Object.entries is inherently untyped, but we don't need it to be here
mergedDefaults[componentName][propName] = propValue;
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
}
}
);
}
);

return mergedDefaults;
}, [configuredDefaults]);

return (
<EuiComponentDefaultsContext.Provider value={componentDefaults}>
{children}
Expand Down
Loading