diff --git a/src-docs/src/components/guide_page/_guide_page.scss b/src-docs/src/components/guide_page/_guide_page.scss index 578ef92e5df..27e8150a5a1 100644 --- a/src-docs/src/components/guide_page/_guide_page.scss +++ b/src-docs/src/components/guide_page/_guide_page.scss @@ -42,12 +42,9 @@ } .guideSideNav__itemBadge { - margin-inline: $euiSizeXS; - } - - // Shift the margin on the badge when selected and the dropdown arrow no longer shows - .euiSideNavItemButton-isSelected .guideSideNav__itemBadge { - margin-right: 0; + margin-inline-start: $euiSizeXS; + // Decrease distance from right side to allow for longer titles and sub-items + margin-inline-end: -$euiSizeS; } } diff --git a/src-docs/src/components/guide_page/guide_page_chrome.js b/src-docs/src/components/guide_page/guide_page_chrome.js index 65ab0b07d23..848298746d4 100644 --- a/src-docs/src/components/guide_page/guide_page_chrome.js +++ b/src-docs/src/components/guide_page/guide_page_chrome.js @@ -41,6 +41,24 @@ export class GuidePageChrome extends Component { }); }; + renderSideNavBadge = ({ isBeta, isNew }) => { + if (isBeta) { + return ( + + BETA + + ); + } + if (isNew) { + return ( + + NEW + + ); + } + return undefined; + }; + scrollNavSectionIntoView = () => { // wait a bit for react to blow away and re-create the DOM // then scroll the selected nav section into view @@ -80,7 +98,7 @@ export class GuidePageChrome extends Component { return; } - return subSectionsWithTitles.map(({ title, sections }) => { + return subSectionsWithTitles.map(({ title, isBeta, isNew, sections }) => { const id = slugify(title); const subSectionHref = `${href}/${id}`; @@ -115,6 +133,7 @@ export class GuidePageChrome extends Component { : '', items: subItems, forceOpen: !!searchTerm || isCurrentlyOpenSubSection, + icon: this.renderSideNavBadge({ isBeta, isNew }), }; }); }; @@ -146,16 +165,6 @@ export class GuidePageChrome extends Component { const href = `#/${path}`; - const badge = isBeta ? ( - - BETA - - ) : isNew ? ( - - NEW - - ) : undefined; - let visibleName = name; if (searchTerm) { visibleName = ( @@ -176,7 +185,7 @@ export class GuidePageChrome extends Component { isSelected: item.path === this.props.currentRoute.path, forceOpen: !!(searchTerm && hasMatchingSubItem), className: 'guideSideNav__item', - icon: badge, + icon: this.renderSideNavBadge({ isBeta, isNew }), }; }); diff --git a/src-docs/src/components/guide_section/guide_section.tsx b/src-docs/src/components/guide_section/guide_section.tsx index 4a989bb6829..0ecc4ab5e70 100644 --- a/src-docs/src/components/guide_section/guide_section.tsx +++ b/src-docs/src/components/guide_section/guide_section.tsx @@ -35,6 +35,8 @@ export interface GuideSectionProps > { id?: string; title?: string; + isBeta?: boolean; + isNew?: boolean; text?: ReactNode; source?: any[]; demo?: ReactNode; @@ -83,6 +85,8 @@ export const GuideSectionCodeTypesMap = { export const GuideSection: FunctionComponent = ({ id, title, + isBeta, + isNew, text, demo, fullScreen, @@ -210,7 +214,13 @@ export const GuideSection: FunctionComponent = ({ className={classNames('guideSection', className)} > - + {text} diff --git a/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx b/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx index 5f50988eb5d..75508a8d44a 100644 --- a/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx +++ b/src-docs/src/components/guide_section/guide_section_parts/guide_section_text.tsx @@ -1,27 +1,40 @@ import React, { FunctionComponent, ReactNode } from 'react'; -import { EuiSpacer } from '../../../../../src/components/spacer'; -import { EuiTitle } from '../../../../../src/components/title'; -import { EuiText } from '../../../../../src/components/text'; + +import { + EuiSpacer, + EuiTitle, + EuiText, + EuiBetaBadge, +} from '../../../../../src/components'; export const LANGUAGES = ['javascript', 'html'] as const; type GuideSectionExampleText = { title?: ReactNode; id?: string; + isBeta?: boolean; + isNew?: boolean; children?: ReactNode; wrapText?: boolean; }; export const GuideSectionExampleText: FunctionComponent< GuideSectionExampleText -> = ({ title, id, children, wrapText = true }) => { +> = ({ title, id, isBeta, isNew, children, wrapText = true }) => { let titleNode; if (title) { + const badge = (isBeta || isNew) && ( + + ); + titleNode = ( <> -

{title}

+

+ {title} + {badge && <> {badge}} +

diff --git a/src-docs/src/views/datagrid/_snippets.tsx b/src-docs/src/views/datagrid/_snippets.tsx index 85a620d83c2..fb3921346bc 100644 --- a/src-docs/src/views/datagrid/_snippets.tsx +++ b/src-docs/src/views/datagrid/_snippets.tsx @@ -69,8 +69,8 @@ renderCustomDataGridBody={({ visibleColumns, visibleRowData, Cell }) => ( )}`, pagination: `pagination={{ pageIndex: 1, - pageSize: 100, - pageSizeOptions: [50, 100, 200], + pageSize: 100, // If not specified, defaults to EuiTablePagination.itemsPerPage + pageSizeOptions: [50, 100, 200], // If not specified, defaults to EuiTablePagination.itemsPerPageOptions onChangePage: () => {}, onChangeItemsPerPage: () => {}, }}`, diff --git a/src-docs/src/views/datagrid/advanced/custom_renderer.tsx b/src-docs/src/views/datagrid/advanced/custom_renderer.tsx index 142df63ec57..289d59f09a5 100644 --- a/src-docs/src/views/datagrid/advanced/custom_renderer.tsx +++ b/src-docs/src/views/datagrid/advanced/custom_renderer.tsx @@ -168,7 +168,7 @@ export default () => { ); // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const onChangePage = useCallback( (pageIndex) => { setPagination((pagination) => ({ ...pagination, pageIndex })); @@ -294,7 +294,6 @@ export default () => { inMemory={{ level: 'sorting' }} pagination={{ ...pagination, - pageSizeOptions: [10, 25, 50], onChangePage: onChangePage, onChangeItemsPerPage: onChangePageSize, }} diff --git a/src-docs/src/views/datagrid/advanced/datagrid_memory_example.js b/src-docs/src/views/datagrid/advanced/datagrid_memory_example.js index 506f35d67cf..e55927a2750 100644 --- a/src-docs/src/views/datagrid/advanced/datagrid_memory_example.js +++ b/src-docs/src/views/datagrid/advanced/datagrid_memory_example.js @@ -23,7 +23,6 @@ const inMemorySortingDataGridSource = require('!!raw-loader!./in_memory_sorting' import { EuiDataGridColumn, - EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridInMemory, EuiDataGridStyle, @@ -32,6 +31,7 @@ import { EuiDataGridCellValueElementProps, EuiDataGridSchemaDetector, } from '!!prop-loader!../../../../../src/components/datagrid/data_grid_types'; +import { EuiDataGridPaginationProps } from '../basics/_props'; export const DataGridMemoryExample = { sections: [ diff --git a/src-docs/src/views/datagrid/advanced/in_memory.js b/src-docs/src/views/datagrid/advanced/in_memory.js index 81b140ae10d..b962d1629fc 100644 --- a/src-docs/src/views/datagrid/advanced/in_memory.js +++ b/src-docs/src/views/datagrid/advanced/in_memory.js @@ -130,7 +130,6 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [10, 50, 100], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/datagrid/advanced/in_memory_enhancements.js b/src-docs/src/views/datagrid/advanced/in_memory_enhancements.js index 9dc632d83a8..437ea0fd884 100644 --- a/src-docs/src/views/datagrid/advanced/in_memory_enhancements.js +++ b/src-docs/src/views/datagrid/advanced/in_memory_enhancements.js @@ -131,7 +131,6 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [10, 50, 100], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/datagrid/advanced/in_memory_pagination.js b/src-docs/src/views/datagrid/advanced/in_memory_pagination.js index 349c93e6572..824ced39c49 100644 --- a/src-docs/src/views/datagrid/advanced/in_memory_pagination.js +++ b/src-docs/src/views/datagrid/advanced/in_memory_pagination.js @@ -52,7 +52,7 @@ for (let i = 1; i < 100; i++) { export default () => { // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const onChangeItemsPerPage = useCallback( (pageSize) => setPagination((pagination) => ({ @@ -115,7 +115,6 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [10, 50, 0], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/datagrid/advanced/in_memory_sorting.js b/src-docs/src/views/datagrid/advanced/in_memory_sorting.js index 5473a9b5728..4e9ece2c11c 100644 --- a/src-docs/src/views/datagrid/advanced/in_memory_sorting.js +++ b/src-docs/src/views/datagrid/advanced/in_memory_sorting.js @@ -52,7 +52,7 @@ for (let i = 1; i < 100; i++) { export default () => { // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const onChangeItemsPerPage = useCallback( (pageSize) => setPagination((pagination) => ({ @@ -101,7 +101,6 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [10, 50, 100], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/datagrid/advanced/ref.tsx b/src-docs/src/views/datagrid/advanced/ref.tsx index 658e60e7ba6..e82eb22bbf2 100644 --- a/src-docs/src/views/datagrid/advanced/ref.tsx +++ b/src-docs/src/views/datagrid/advanced/ref.tsx @@ -111,7 +111,7 @@ export default () => { ); // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const onChangePage = useCallback( (pageIndex) => { setPagination((pagination) => ({ ...pagination, pageIndex })); @@ -224,7 +224,6 @@ export default () => { } pagination={{ ...pagination, - pageSizeOptions: [25, 50], onChangePage: onChangePage, onChangeItemsPerPage: onChangePageSize, }} diff --git a/src-docs/src/views/datagrid/basics/_props.tsx b/src-docs/src/views/datagrid/basics/_props.tsx index 33d578b0ca6..875890ca701 100644 --- a/src-docs/src/views/datagrid/basics/_props.tsx +++ b/src-docs/src/views/datagrid/basics/_props.tsx @@ -1,5 +1,8 @@ -import React from 'react'; -import { EuiDataGrid } from '../../../../../src/components'; +import React, { FunctionComponent } from 'react'; +import { + EuiDataGrid, + EuiDataGridPaginationProps as _EuiDataGridPaginationProps, +} from '../../../../../src/components'; import { DataGridPropsTable } from '../_props_table'; import { gridSnippets } from '../_snippets'; @@ -41,3 +44,8 @@ export const DataGridTopProps = () => { /> ); }; + +// Loading `EuiDataGridPaginationProps` via !prop-loader doesn't correctly inherit @defaults +export const EuiDataGridPaginationProps: FunctionComponent< + _EuiDataGridPaginationProps +> = () =>
; diff --git a/src-docs/src/views/datagrid/basics/container.js b/src-docs/src/views/datagrid/basics/container.js index a4b94038ff5..d90206f0885 100644 --- a/src-docs/src/views/datagrid/basics/container.js +++ b/src-docs/src/views/datagrid/basics/container.js @@ -34,7 +34,7 @@ for (let i = 1; i < 20; i++) { } export default () => { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -74,7 +74,6 @@ export default () => { renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/basics/datagrid.js b/src-docs/src/views/datagrid/basics/datagrid.js index 980a1d461a1..a8898bb14b7 100644 --- a/src-docs/src/views/datagrid/basics/datagrid.js +++ b/src-docs/src/views/datagrid/basics/datagrid.js @@ -372,7 +372,7 @@ const trailingControlColumns = [ export default () => { // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const onChangeItemsPerPage = useCallback( (pageSize) => setPagination((pagination) => ({ @@ -419,7 +419,6 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [10, 50, 100], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/datagrid/basics/datagrid_example.js b/src-docs/src/views/datagrid/basics/datagrid_example.js index 67eb4a16857..42aff13527c 100644 --- a/src-docs/src/views/datagrid/basics/datagrid_example.js +++ b/src-docs/src/views/datagrid/basics/datagrid_example.js @@ -12,7 +12,7 @@ import { } from '../../../../../src/components'; import DataGrid from './datagrid'; -import { DataGridTopProps } from './_props'; +import { DataGridTopProps, EuiDataGridPaginationProps } from './_props'; const dataGridSource = require('!!raw-loader!./datagrid'); import DataGridContainer from './container'; @@ -28,7 +28,6 @@ const dataGridVirtualizationConstrainedSource = require('!!raw-loader!./virtuali import { EuiDataGridColumn, EuiDataGridColumnCellAction, - EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridInMemory, EuiDataGridStyle, diff --git a/src-docs/src/views/datagrid/basics/flex.js b/src-docs/src/views/datagrid/basics/flex.js index 996dff495ca..2b32608c7ee 100644 --- a/src-docs/src/views/datagrid/basics/flex.js +++ b/src-docs/src/views/datagrid/basics/flex.js @@ -40,7 +40,7 @@ for (let i = 1; i < 20; i++) { } export default () => { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -89,7 +89,6 @@ export default () => { } pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/cells_popovers/column_cell_actions.js b/src-docs/src/views/datagrid/cells_popovers/column_cell_actions.js index a697c159b6a..596300865aa 100644 --- a/src-docs/src/views/datagrid/cells_popovers/column_cell_actions.js +++ b/src-docs/src/views/datagrid/cells_popovers/column_cell_actions.js @@ -120,7 +120,7 @@ for (let i = 1; i < 5; i++) { } export default () => { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -153,7 +153,6 @@ export default () => { renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/schema_columns/column_actions.js b/src-docs/src/views/datagrid/schema_columns/column_actions.js index a346b1fff0f..dd0f444057f 100644 --- a/src-docs/src/views/datagrid/schema_columns/column_actions.js +++ b/src-docs/src/views/datagrid/schema_columns/column_actions.js @@ -69,7 +69,7 @@ for (let i = 1; i < 5; i++) { } export default () => { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -102,7 +102,6 @@ export default () => { renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/schema_columns/column_widths.js b/src-docs/src/views/datagrid/schema_columns/column_widths.js index 766656e4331..f76c1e18a5f 100644 --- a/src-docs/src/views/datagrid/schema_columns/column_widths.js +++ b/src-docs/src/views/datagrid/schema_columns/column_widths.js @@ -46,7 +46,7 @@ for (let i = 1; i < 5; i++) { } export default () => { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -79,7 +79,6 @@ export default () => { renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/schema_columns/control_columns.js b/src-docs/src/views/datagrid/schema_columns/control_columns.js index 68a7d29fcd7..c6d45022939 100644 --- a/src-docs/src/views/datagrid/schema_columns/control_columns.js +++ b/src-docs/src/views/datagrid/schema_columns/control_columns.js @@ -309,10 +309,7 @@ const trailingControlColumns = [ ]; export default function DataGrid() { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 15, - }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const setPageIndex = useCallback( (pageIndex) => setPagination((pagination) => ({ ...pagination, pageIndex })), @@ -369,7 +366,6 @@ export default function DataGrid() { renderCellValue={renderCellValue} pagination={{ ...pagination, - pageSizeOptions: [5, 15, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/schema_columns/footer_row.js b/src-docs/src/views/datagrid/schema_columns/footer_row.js index 8a92932cf71..c06ed3fbb8e 100644 --- a/src-docs/src/views/datagrid/schema_columns/footer_row.js +++ b/src-docs/src/views/datagrid/schema_columns/footer_row.js @@ -130,7 +130,7 @@ const RenderFooterCellValue = ({ columnId, setCellProps }) => { export default () => { // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const onChangeItemsPerPage = useCallback( (pageSize) => setPagination((pagination) => ({ @@ -177,7 +177,6 @@ export default () => { } pagination={{ ...pagination, - pageSizeOptions: [10, 15, 20], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/datagrid/styling/styling_grid.js b/src-docs/src/views/datagrid/styling/styling_grid.js index 1d153dc7dd5..540cbb6a578 100644 --- a/src-docs/src/views/datagrid/styling/styling_grid.js +++ b/src-docs/src/views/datagrid/styling/styling_grid.js @@ -59,10 +59,7 @@ const DataGridStyle = ({ header = 'underline', footer = 'overline', }) => { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 5, - }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) ); @@ -111,7 +108,6 @@ const DataGridStyle = ({ renderFooterCellValue={renderFooterCellValue} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/toolbar/_grid.js b/src-docs/src/views/datagrid/toolbar/_grid.js index d6d0e7894fa..0d6e8b5d3eb 100644 --- a/src-docs/src/views/datagrid/toolbar/_grid.js +++ b/src-docs/src/views/datagrid/toolbar/_grid.js @@ -55,10 +55,7 @@ const DataGridStyle = ({ allowHideColumns, allowOrderingColumns, }) => { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 5, - }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) ); @@ -139,7 +136,6 @@ const DataGridStyle = ({ renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/datagrid/toolbar/additional_controls.tsx b/src-docs/src/views/datagrid/toolbar/additional_controls.tsx index fa98c19745a..606e3c21885 100644 --- a/src-docs/src/views/datagrid/toolbar/additional_controls.tsx +++ b/src-docs/src/views/datagrid/toolbar/additional_controls.tsx @@ -50,7 +50,7 @@ for (let i = 1; i < 20; i++) { } export default () => { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [pagination, setPagination] = useState({ pageIndex: 0 }); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const flyoutTitleId = useGeneratedHtmlId({ prefix: 'dataGridAdditionalControlsFlyout', @@ -129,7 +129,6 @@ export default () => { renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} diff --git a/src-docs/src/views/guidelines/getting_started/getting_started.js b/src-docs/src/views/guidelines/getting_started/getting_started.js index 608abe7a5bd..24610fd6717 100644 --- a/src-docs/src/views/guidelines/getting_started/getting_started.js +++ b/src-docs/src/views/guidelines/getting_started/getting_started.js @@ -7,6 +7,7 @@ import { AppSetup } from './_app_setup'; import { Tokens } from './_tokens'; import { Customizing } from './_customizing'; import { ThemeNotice } from '../../../views/theme/_components/_theme_notice.tsx'; +import { euiProviderComponentDefaultsSnippet } from '../../provider/provider_component_defaults'; export const GettingStarted = { title: 'Getting started', @@ -268,5 +269,29 @@ import { findByTestSubject, render, screen } from '@elastic/eui/lib/test/rtl'; / ), }, + { + title: 'Customizing component defaults', + wrapText: false, + text: ( + <> + +

+ While all props can be individually customized via props, some + components can have their default props customized globally via{' '} + EuiProvider's{' '} + componentDefaults API.{' '} + + Read more in EuiProvider's documentation + + . +

+
+ + + {euiProviderComponentDefaultsSnippet} + + + ), + }, ], }; diff --git a/src-docs/src/views/provider/provider_component_defaults.tsx b/src-docs/src/views/provider/provider_component_defaults.tsx new file mode 100644 index 00000000000..f2561c5a6f1 --- /dev/null +++ b/src-docs/src/views/provider/provider_component_defaults.tsx @@ -0,0 +1,22 @@ +import React, { FunctionComponent } from 'react'; + +import { EuiComponentDefaults } from '../../../../src/components/provider/component_defaults'; + +// Used to generate a "component" that is parsed for its types +// and used to generate a prop table +export const EuiComponentDefaultsProps: FunctionComponent< + EuiComponentDefaults +> = () => <>; + +// Used by both getting started and EuiProvider component documentation pages +// Exported in one place for DRYness +export const euiProviderComponentDefaultsSnippet = ` + + +`; diff --git a/src-docs/src/views/provider/provider_example.js b/src-docs/src/views/provider/provider_example.js index a6ac1a53ec6..c01f8a9b524 100644 --- a/src-docs/src/views/provider/provider_example.js +++ b/src-docs/src/views/provider/provider_example.js @@ -8,6 +8,7 @@ import { EuiCodeBlock, EuiLink, EuiSpacer, + EuiCallOut, } from '../../../../src/components'; import { GuideSectionPropsTable } from '../../components/guide_section/guide_section_parts/guide_section_props_table'; @@ -15,6 +16,10 @@ import { GuideSectionPropsTable } from '../../components/guide_section/guide_sec import Setup from './provider_setup'; import GlobalStyles from './provider_styles'; import Warnings from './provider_warning'; +import { + EuiComponentDefaultsProps, + euiProviderComponentDefaultsSnippet, +} from './provider_component_defaults'; export const ProviderExample = { title: 'Provider', @@ -135,6 +140,66 @@ export const ProviderExample = { ), }, + { + title: 'Component defaults', + isBeta: true, + text: ( + + +

+ This functionality is still currently in beta, and the list of + components as well as defaults that EUI will be supporting is + still under consideration. If you have a component you would like + to see added, feel free to{' '} + + discuss that request in EUI's GitHub repo + + . +

+
+ + +

+ All EUI components ship with a set of baseline defaults that can + usually be configured via props. For example,{' '} + + EuiFocusTrap + {' '} + defaults to crossFrame={'{false}'} - i.e., it + does not trap focus between iframes. If you wanted to change that + behavior in your app across all instances of{' '} + EuiFocusTrap, you would be stuck manually passing + that prop over and over again, including in higher-level components + (like modals, popovers, and flyouts) that utilize focus traps. +

+

+ EuiProvider allows overriding some component + defaults across all component usages globally via the{' '} + componentDefaults prop like so: +

+ + + {euiProviderComponentDefaultsSnippet} + + +

+ The above example would override EUI's default table pagination size + (50) across all usages of EUI tables and data grids, all EUI focus + traps would trap focus even from iframes, and all EUI portals would + be inserted at a specified position (instead of the end of the + document body). +

+

+ The current list of supported components and the prop defaults they + accept are: +

+ +
+ ), + }, { title: 'Enforce usage', text: ( diff --git a/src-docs/src/views/tables/basic/basic_section.js b/src-docs/src/views/tables/basic/basic_section.js index e252cb2ffd4..3bee817ff28 100644 --- a/src-docs/src/views/tables/basic/basic_section.js +++ b/src-docs/src/views/tables/basic/basic_section.js @@ -7,7 +7,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js index f6fa59987d9..e7da5170060 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js @@ -9,7 +9,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_custom_sorting_section.js b/src-docs/src/views/tables/in_memory/in_memory_custom_sorting_section.js index 2ddbcd10efe..50505712592 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_custom_sorting_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_custom_sorting_section.js @@ -9,7 +9,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_search_callback_section.js b/src-docs/src/views/tables/in_memory/in_memory_search_callback_section.js index 6acc470a7bf..ad702b45f06 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_search_callback_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_search_callback_section.js @@ -8,7 +8,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_search_external_section.js b/src-docs/src/views/tables/in_memory/in_memory_search_external_section.js index c8297b245eb..553bab0a679 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_search_external_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_search_external_section.js @@ -8,7 +8,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_search_section.js b/src-docs/src/views/tables/in_memory/in_memory_search_section.js index 208e81b8800..7a74191d70c 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_search_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_search_section.js @@ -9,7 +9,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_section.js b/src-docs/src/views/tables/in_memory/in_memory_section.js index 56729011ed0..2fc8a6d732a 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_section.js @@ -9,7 +9,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection_section.js b/src-docs/src/views/tables/in_memory/in_memory_selection_section.js index ea3897d6666..f294135ca14 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_selection_section.js +++ b/src-docs/src/views/tables/in_memory/in_memory_selection_section.js @@ -8,7 +8,7 @@ import { Criteria, CriteriaWithPagination, } from '!!prop-loader!../../../../../src/components/basic_table/basic_table'; -import { Pagination } from '!!prop-loader!../../../../../src/components/basic_table/pagination_bar'; +import { Pagination } from '../paginated/_props'; import { EuiTableFieldDataColumnType, EuiTableComputedColumnType, diff --git a/src-docs/src/views/tables/paginated/_props.tsx b/src-docs/src/views/tables/paginated/_props.tsx new file mode 100644 index 00000000000..7817dfcc79f --- /dev/null +++ b/src-docs/src/views/tables/paginated/_props.tsx @@ -0,0 +1,6 @@ +import React, { FunctionComponent } from 'react'; + +import { Pagination as _Pagination } from '../../../../../src/components/basic_table/pagination_bar'; + +// Loading `Pagination` directly via !prop-loader doesn't correctly inherit @defaults +export const Pagination: FunctionComponent<_Pagination> = () =>
; diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap index 53f073b2eb4..4d786deea65 100644 --- a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap @@ -1,6 +1,102 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiInMemoryTable behavior pagination 1`] = ` + +`; + +exports[`EuiInMemoryTable empty array 1`] = `
- Name -
-
- - name3 - -
- - - - -
- Name -
-
- name4 + No items found
@@ -105,255 +175,10 @@ exports[`EuiInMemoryTable behavior pagination 1`] = `
-
-
-
-
-
-
- -
-
-
-
- -
-
-
`; -exports[`EuiInMemoryTable empty array 1`] = ` - - } - onChange={[Function]} - responsive={true} - tableLayout="fixed" -/> -`; - exports[`EuiInMemoryTable with executeQueryOptions 1`] = ` - - } - onChange={[Function]} - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with initial selection 1`] = ` - - } - onChange={[Function]} - responsive={true} - selection={ - Object { - "initialSelected": Array [ - Object { - "id": "1", - "name": "name1", - }, - ], - "onSelectionChange": [Function], - } - } - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with initial sorting 1`] = ` `; exports[`EuiInMemoryTable with items 1`] = ` - - } - onChange={[Function]} - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with items and expanded item 1`] = ` - - expanded row content -
, - } - } - items={ - Array [ - Object { - "id": "1", - "name": "name1", - }, - Object { - "id": "2", - "name": "name2", - }, - Object { - "id": "3", - "name": "name3", - }, - ] - } - noItemsMessage={ - - } - onChange={[Function]} - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with items and message - expecting to show the items 1`] = ` - -`; - -exports[`EuiInMemoryTable with message 1`] = ` - -`; - -exports[`EuiInMemoryTable with message and loading 1`] = ` - -`; - -exports[`EuiInMemoryTable with pagination 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 2, - "pageSizeOptions": Array [ - 2, - 4, - 6, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination and "show all" page size 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 0, - "pageSizeOptions": Array [ - 1, - 2, - 3, - 0, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination and default page size and index 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 1, - "pageSize": 2, - "pageSizeOptions": Array [ - 1, - 2, - 3, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination and selection 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination, default page size and error 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 4, - "pageSizeOptions": Array [ - 2, - 4, - 6, - ], - "showPerPageOptions": undefined, - "totalItemCount": 1, - } - } - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination, hiding the per page options 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "showPerPageOptions": false, - "totalItemCount": 3, - } - } - responsive={true} - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination, selection and sorting 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination, selection, sorting and simple search 1`] = ` -
- - - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - /> -
-`; - -exports[`EuiInMemoryTable with pagination, selection, sorting and a single record action 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination, selection, sorting and column renderer 1`] = ` - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 2, - "pageSizeOptions": Array [ - 2, - 4, - 6, - ], - "showPerPageOptions": undefined, - "totalItemCount": 3, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" -/> -`; - -exports[`EuiInMemoryTable with pagination, selection, sorting and configured search 1`] = ` -
- - - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 1, - } - } - responsive={true} - selection={ - Object { - "onSelectionChange": [Function], - } - } - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - /> -
-`; - -exports[`EuiInMemoryTable with search and component between search and table 1`] = ` -
- - +>
- Children Between -
- +
+
+
+
+
+ + + + + + + + + + + + + + + + + +
- } - onChange={[Function]} - responsive={true} - tableLayout="fixed" - /> +
+ + + Name + + + description + + +
+
+ Name +
+
+ + name1 + +
+
+
+ Name +
+
+ + name2 + +
+
+
+ Name +
+
+ + name3 + +
+
+
`; 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 2366e0d0f51..5afd7642a41 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`] = `
extends Component< EuiBasicTableProps, State > { + static contextType = EuiComponentDefaultsContext; + declare context: ContextType; + static defaultProps = { responsive: true, tableLayout: 'fixed', @@ -366,6 +377,14 @@ export class EuiBasicTable extends Component< this.getInitialSelection(); } + get pageSize() { + return ( + this.props.pagination?.pageSize ?? + this.context.EuiTablePagination?.itemsPerPage ?? + euiTablePaginationDefaults.itemsPerPage + ); + } + getInitialSelection() { if ( this.props.selection && @@ -387,7 +406,7 @@ export class EuiBasicTable extends Component< if (hasPagination(props)) { criteria.page = { index: props.pagination.pageIndex, - size: props.pagination.pageSize, + size: this.pageSize, }; } if (props.sorting) { @@ -610,8 +629,8 @@ export class EuiBasicTable extends Component< const itemCount = items.length; const totalItemCount = pagination ? pagination.totalItemCount : itemCount; const page = pagination ? pagination.pageIndex + 1 : 1; - const pageCount = pagination?.pageSize - ? Math.ceil(pagination.totalItemCount / pagination.pageSize) + const pageCount = pagination + ? Math.ceil(pagination.totalItemCount / this.pageSize) : 1; let captionElement; @@ -896,9 +915,8 @@ export class EuiBasicTable extends Component< content = items.map((item: T, index: number) => { // if there's pagination the item's index must be adjusted to the where it is in the whole dataset const tableItemIndex = - hasPagination(this.props) && this.props.pagination.pageSize > 0 - ? this.props.pagination.pageIndex * this.props.pagination.pageSize + - index + hasPagination(this.props) && this.pageSize > 0 + ? this.props.pagination.pageIndex * this.pageSize + index : index; return this.renderItemRow(item, tableItemIndex); }); diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 1753fee2005..96a46d1d607 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -8,8 +8,12 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; +import { fireEvent } from '@testing-library/react'; +import { render } from '../../test/rtl'; import { requiredProps } from '../../test'; +import { EuiProvider } from '../provider'; + import { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; import { keys, SortDirection } from '../../services'; import { SearchFilterConfig } from '../search_bar/filters'; @@ -32,8 +36,7 @@ interface ComplexItem { }; } -// Shallow snapshots were converted to `dive()` to match output before Emotion wrappers were added -// TODO: Convert to RTL render, or even better, use specific assertions instead of snapshots +// TODO: Convert remaining shallow/mount tests to RTL describe('EuiInMemoryTable', () => { test('empty array', () => { @@ -48,9 +51,9 @@ describe('EuiInMemoryTable', () => { }, ], }; - const component = shallow(); + const { container } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('with message', () => { @@ -66,9 +69,9 @@ describe('EuiInMemoryTable', () => { ], message: 'where my items at?', }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('where my items at?')).toBeTruthy(); }); test('with message and loading', () => { @@ -85,9 +88,9 @@ describe('EuiInMemoryTable', () => { message: 'Loading items....', loading: true, }; - const component = shallow(); + const { container } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(container.querySelector('.euiBasicTable-loading')).toBeTruthy(); }); test('with executeQueryOptions', () => { @@ -126,9 +129,9 @@ describe('EuiInMemoryTable', () => { }, ], }; - const component = shallow(); + const { container } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); }); test('with items and expanded item', () => { @@ -151,9 +154,9 @@ describe('EuiInMemoryTable', () => { '1':
expanded row content
, }, }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('expanded row content')).toBeTruthy(); }); test('with items and message - expecting to show the items', () => { @@ -173,9 +176,9 @@ describe('EuiInMemoryTable', () => { }, ], }; - const component = shallow(); + const { queryByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(queryByText('show me!')).toBeFalsy(); }); test('with pagination', () => { @@ -197,9 +200,9 @@ describe('EuiInMemoryTable', () => { pageSizeOptions: [2, 4, 6], }, }; - const component = shallow(); + const { container } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(container.querySelector('.euiPagination')).toBeTruthy(); }); test('with pagination and default page size and index', () => { @@ -223,9 +226,12 @@ describe('EuiInMemoryTable', () => { pageSizeOptions: [1, 2, 3], }, }; - const component = shallow(); + const { getByText, getByTestSubject } = render( + + ); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('Rows per page: 2')).toBeTruthy(); + expect(getByTestSubject('pagination-button-1')).toBeDisabled(); // disabled = current page }); test('with pagination and "show all" page size', () => { @@ -248,9 +254,9 @@ describe('EuiInMemoryTable', () => { pageSizeOptions: [1, 2, 3, 0], }, }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('Showing all rows')).toBeTruthy(); }); test('with pagination, default page size and error', () => { @@ -270,9 +276,12 @@ describe('EuiInMemoryTable', () => { pageSizeOptions: [2, 4, 6], }, }; - const component = shallow(); + const { getByText, queryByTestSubject } = render( + + ); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('ouch!')).toBeTruthy(); + expect(queryByTestSubject('tablePaginationPopoverButton')).toBe(null); }); test('with pagination, hiding the per page options', () => { @@ -294,9 +303,9 @@ describe('EuiInMemoryTable', () => { showPerPageOptions: false, }, }; - const component = shallow(); + const { queryByTestSubject } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(queryByTestSubject('tablePaginationPopoverButton')).toBe(null); }); describe('sorting', () => { @@ -566,10 +575,13 @@ describe('EuiInMemoryTable', () => { }, }, }; - const component = shallow(); - - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + const { container } = render(); expect(itemsProp).toEqual(items); + + const cells = container.querySelectorAll('td'); + expect(cells[0]).toHaveTextContent('name3'); + expect(cells[1]).toHaveTextContent('name2'); + expect(cells[2]).toHaveTextContent('name1'); }); test('with initial selection', () => { @@ -593,9 +605,16 @@ describe('EuiInMemoryTable', () => { initialSelected: [{ id: '1', name: 'name1' }], }, }; - const component = shallow(); - - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + const { container } = render(); + const selections = container.querySelectorAll( + '[data-test-subj^="checkboxSelect"]' + ); + + expect(selections.length).toEqual(4); + expect(selections[0]).not.toBeChecked(); // Select all row + expect(selections[1]).toBeChecked(); + expect(selections[2]).not.toBeChecked(); + expect(selections[3]).not.toBeChecked(); }); test('with pagination and selection', () => { @@ -619,9 +638,10 @@ describe('EuiInMemoryTable', () => { onSelectionChange: () => undefined, }, }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('Page 1 of 1')).toBeTruthy(); + expect(getByText('Select all rows')).toBeTruthy(); }); test('with pagination, selection and sorting', () => { @@ -647,9 +667,11 @@ describe('EuiInMemoryTable', () => { onSelectionChange: () => undefined, }, }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('Page 1 of 1')).toBeTruthy(); + expect(getByText('Select all rows')).toBeTruthy(); + expect(getByText('Sorting')).toBeTruthy(); }); test('with pagination, selection, sorting and column renderer', () => { @@ -678,9 +700,12 @@ describe('EuiInMemoryTable', () => { onSelectionChange: () => undefined, }, }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('Page 1 of 2')).toBeTruthy(); + expect(getByText('Select all rows')).toBeTruthy(); + expect(getByText('Sorting')).toBeTruthy(); + expect(getByText('NAME1')).toBeTruthy(); }); test('with pagination, selection, sorting and a single record action', () => { @@ -716,9 +741,12 @@ describe('EuiInMemoryTable', () => { onSelectionChange: () => undefined, }, }; - const component = shallow(); + const { getByText } = render(); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByText('Page 1 of 1')).toBeTruthy(); + expect(getByText('Select all rows')).toBeTruthy(); + expect(getByText('Sorting')).toBeTruthy(); + expect(getByText('Actions')).toBeTruthy(); }); test('with pagination, selection, sorting and simple search', () => { @@ -755,9 +783,15 @@ describe('EuiInMemoryTable', () => { onSelectionChange: () => undefined, }, }; - const component = shallow(); - - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + const { getByText, getByPlaceholderText } = render( + + ); + + expect(getByText('Page 1 of 1')).toBeTruthy(); + expect(getByText('Select all rows')).toBeTruthy(); + expect(getByText('Sorting')).toBeTruthy(); + expect(getByText('Actions')).toBeTruthy(); + expect(getByPlaceholderText('Search...')).toBeTruthy(); }); test('with search and component between search and table', () => { @@ -790,9 +824,12 @@ describe('EuiInMemoryTable', () => { search: true, childrenBetween:
Children Between
, }; - const component = shallow(); + const { getByPlaceholderText, getByText } = render( + + ); - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + expect(getByPlaceholderText('Search...')).toBeTruthy(); + expect(getByText('Children Between')).toBeTruthy(); }); test('with pagination, selection, sorting and configured search', () => { @@ -844,9 +881,22 @@ describe('EuiInMemoryTable', () => { onSelectionChange: () => undefined, }, }; - const component = shallow(); - - expect(component.find(EuiInMemoryTable).dive()).toMatchSnapshot(); + const { container, queryByText } = render(); + + expect(queryByText('Page 1 of 1')).toBeTruthy(); + expect(queryByText('Select all rows')).toBeTruthy(); + expect(queryByText('Sorting')).toBeTruthy(); + + expect(container.querySelector('input[type="search"]')).toHaveValue( + 'name:name1' + ); + const filterButton = container.querySelector('.euiFilterButton'); + expect(filterButton).toHaveTextContent('Name1'); + expect(filterButton?.getAttribute('aria-pressed')).toEqual('true'); + + expect(queryByText('name1')).toBeTruthy(); + expect(queryByText('name2')).toBeFalsy(); + expect(queryByText('name3')).toBeFalsy(); }); describe('search interaction & functionality', () => { @@ -1058,17 +1108,47 @@ describe('EuiInMemoryTable', () => { pageSizeOptions: [2, 4, 6], }, }; - const component = mount(); + const { getByTestSubject, rerender, container } = render( + + ); - component - .find('a[data-test-subj="pagination-button-1"]') - .simulate('click'); + fireEvent.click(getByTestSubject('pagination-button-1')); // forces EuiInMemoryTable's getDerivedStateFromProps to re-execute // this is specifically testing regression against https://github.com/elastic/eui/issues/1007 - component.setProps({}); + rerender(); - expect(component.render()).toMatchSnapshot(); + expect(container.querySelector('.euiPagination')).toMatchSnapshot(); + }); + + // Other pagination tests already check default pagination sizes + // and individual pagination setting, so we'll just check here that + // `componentDefaults` is respected + test('pagination inherited from EuiProvider componentDefaults', () => { + const props = { + items: [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }], + columns: [{ field: 'title', name: 'Title' }], + }; + const { getByText, getByTestSubject } = render( + + + , + { wrapper: undefined } + ); + + expect(getByText('Rows per page: 50')).toBeTruthy(); + + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByTestSubject('tablePagination-25-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-50-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-100-rows')).toBeTruthy(); }); test('pagination with actions column and sorting set to true', async () => { @@ -1091,11 +1171,9 @@ describe('EuiInMemoryTable', () => { pageSizeOptions: [2, 4, 6], }, }; - const component = mount(); + const { getByTestSubject } = render(); - component - .find('a[data-test-subj="pagination-button-1"]') - .simulate('click'); + fireEvent.click(getByTestSubject('pagination-button-1')); }); test('onTableChange callback', () => { @@ -1122,13 +1200,13 @@ describe('EuiInMemoryTable', () => { onTableChange: jest.fn(), }; - const component = mount(); + const { getByTestSubject, container } = render( + + ); expect(props.onTableChange).toHaveBeenCalledTimes(0); // Pagination change - component - .find('a[data-test-subj="pagination-button-1"]') - .simulate('click'); + fireEvent.click(getByTestSubject('pagination-button-1')); expect(props.onTableChange).toHaveBeenCalledTimes(1); expect(props.onTableChange).toHaveBeenLastCalledWith({ sort: {}, @@ -1139,11 +1217,11 @@ describe('EuiInMemoryTable', () => { }); // Sorting change - component - .find( + fireEvent.click( + container.querySelector( '[data-test-subj*="tableHeaderCell_name_0"] [data-test-subj="tableHeaderSortButton"]' - ) - .simulate('click'); + )! + ); expect(props.onTableChange).toHaveBeenCalledTimes(2); expect(props.onTableChange).toHaveBeenLastCalledWith({ sort: { @@ -1157,9 +1235,7 @@ describe('EuiInMemoryTable', () => { }); // Sorted pagination change - component - .find('a[data-test-subj="pagination-button-1"]') - .simulate('click'); + fireEvent.click(getByTestSubject('pagination-button-1')); expect(props.onTableChange).toHaveBeenCalledTimes(3); expect(props.onTableChange).toHaveBeenLastCalledWith({ sort: { @@ -1196,7 +1272,7 @@ describe('EuiInMemoryTable', () => { }, ]; const onTableChange = jest.fn(); - const component = mount( + const { getByTestSubject, container, rerender } = render( { ); // ensure table is on 2nd page (pageIndex=1) - expect( - component.find('button[data-test-subj="pagination-button-1"][disabled]') - .length - ).toBe(1); - expect(component.find('td').at(0).text()).toBe('Index2'); - expect(component.find('td').at(1).text()).toBe('Index3'); + expect(getByTestSubject('pagination-button-1')).toBeDisabled(); + expect(container.querySelectorAll('td')[0]).toHaveTextContent('Index2'); + expect(container.querySelectorAll('td')[1]).toHaveTextContent('Index3'); // click the first pagination button - component - .find('a[data-test-subj="pagination-button-0"]') - .simulate('click'); + fireEvent.click(getByTestSubject('pagination-button-0')); expect(onTableChange).toHaveBeenCalledTimes(1); expect(onTableChange).toHaveBeenCalledWith({ sort: {}, @@ -1227,24 +1298,24 @@ describe('EuiInMemoryTable', () => { }); // ensure table is still on the 2nd page (pageIndex=1) - expect( - component.find('button[data-test-subj="pagination-button-1"][disabled]') - .length - ).toBe(1); - expect(component.find('td').at(0).text()).toBe('Index2'); - expect(component.find('td').at(1).text()).toBe('Index3'); + expect(getByTestSubject('pagination-button-1')).toBeDisabled(); + expect(container.querySelectorAll('td')[0]).toHaveTextContent('Index2'); + expect(container.querySelectorAll('td')[1]).toHaveTextContent('Index3'); // re-render with an updated `pageIndex` value - pagination.pageIndex = 2; - component.setProps({ pagination }); + rerender( + + ); // ensure table is on 3rd page (pageIndex=2) - expect( - component.find('button[data-test-subj="pagination-button-2"][disabled]') - .length - ).toBe(1); - expect(component.find('td').at(0).text()).toBe('Index4'); - expect(component.find('td').at(1).text()).toBe('Index5'); + expect(getByTestSubject('pagination-button-2')).toBeDisabled(); + expect(container.querySelectorAll('td')[0]).toHaveTextContent('Index4'); + expect(container.querySelectorAll('td')[1]).toHaveTextContent('Index5'); }); it('respects pageSize', () => { diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index f03dacd5fef..dd606fd58d2 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -20,10 +20,7 @@ import { EuiTableSortingType, } from './table_types'; import { PropertySort } from '../../services'; -import { - defaults as paginationBarDefaults, - Pagination as PaginationBarType, -} from './pagination_bar'; +import { Pagination as PaginationBarType } from './pagination_bar'; import { isString } from '../../services/predicate'; import { Comparators, Direction } from '../../services/sort'; import { EuiSearchBar, Query } from '../search_bar'; @@ -31,7 +28,14 @@ import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; import { EuiSearchBarProps } from '../search_bar/search_bar'; import { SchemaType } from '../search_bar/search_box'; -import { EuiTablePaginationProps } from '../table'; +import { + EuiTablePaginationProps, + euiTablePaginationDefaults, +} from '../table/table_pagination'; +import { + EuiComponentDefaultsContext, + EuiComponentDefaults, +} from '../provider/component_defaults'; interface onChangeArgument { query: Query | null; @@ -72,7 +76,13 @@ type InMemoryTableProps = Omit< * Configures #Search. */ search?: Search; + /** + * Configures #Pagination + */ pagination?: undefined; + /** + * Configures #EuiTableSortingType + */ sorting?: Sorting; /** * Set `allowNeutralSort` to false to force column sorting. Defaults to true. @@ -152,7 +162,10 @@ const getQueryFromSearch = ( return isString(query) ? EuiSearchBar.Query.parse(query) : query; }; -const getInitialPagination = (pagination: Pagination | undefined) => { +const getInitialPagination = ( + pagination: Pagination | undefined, + consumerDefaults: EuiComponentDefaults['EuiTablePagination'] +) => { if (!pagination) { return { pageIndex: undefined, @@ -160,14 +173,19 @@ const getInitialPagination = (pagination: Pagination | undefined) => { }; } + const defaults = { + ...euiTablePaginationDefaults, + ...consumerDefaults, + }; + const { - pageSizeOptions = paginationBarDefaults.pageSizeOptions, - showPerPageOptions, + pageSizeOptions = defaults.itemsPerPageOptions, + showPerPageOptions = defaults.showPerPageOptions, } = pagination as PaginationOptions; - const defaultPageSize = pageSizeOptions - ? pageSizeOptions[0] - : paginationBarDefaults.pageSizeOptions[0]; + const defaultPageSize = pageSizeOptions?.includes(defaults.itemsPerPage) + ? defaults.itemsPerPage + : pageSizeOptions[0]; const initialPageIndex = pagination === true @@ -181,7 +199,7 @@ const getInitialPagination = (pagination: Pagination | undefined) => { if ( showPerPageOptions && initialPageSize != null && - (!pageSizeOptions || !pageSizeOptions.includes(initialPageSize)) + !pageSizeOptions?.includes(initialPageSize) ) { throw new Error( `EuiInMemoryTable received initialPageSize ${initialPageSize}, which wasn't provided within pageSizeOptions.` @@ -262,6 +280,8 @@ export class EuiInMemoryTable extends Component< EuiInMemoryTableProps, State > { + static contextType = EuiComponentDefaultsContext; + static defaultProps = { responsive: true, tableLayout: 'fixed', @@ -362,12 +382,12 @@ export class EuiInMemoryTable extends Component< return null; } - constructor(props: EuiInMemoryTableProps) { + constructor(props: EuiInMemoryTableProps, context: EuiComponentDefaults) { super(props); const { columns, search, pagination, sorting, allowNeutralSort } = props; const { pageIndex, pageSize, pageSizeOptions, showPerPageOptions } = - getInitialPagination(pagination); + getInitialPagination(pagination, context.EuiTablePagination); const { sortName, sortDirection } = getInitialSorting(columns, sorting); this.state = { diff --git a/src/components/basic_table/pagination_bar.test.tsx b/src/components/basic_table/pagination_bar.test.tsx index 5233bb14a65..452c3ccb6c3 100644 --- a/src/components/basic_table/pagination_bar.test.tsx +++ b/src/components/basic_table/pagination_bar.test.tsx @@ -7,78 +7,106 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { fireEvent } from '@testing-library/react'; import { render } from '../../test/rtl'; import { requiredProps } from '../../test'; +import { EuiProvider } from '../provider'; + 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(); 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: () => {}, - }; + it('calls onPageChange with the correct off-by-one offset', () => { + const onPageChange = jest.fn(); + const { getByLabelText } = render( + + ); - const component = shallow(); - - expect(component).toMatchSnapshot(); + fireEvent.click(getByLabelText('Page 2 of 2')); + expect(onPageChange).toHaveBeenCalledWith(1); }); - test('render - hiding per page options', () => { - const props = { - pagination: { - pageIndex: 0, - pageSize: 5, - totalItemCount: 0, - showPerPageOptions: false, - }, - onPageSizeChange: () => {}, - onPageChange: () => {}, - }; + describe('EuiTablePagination component defaults', () => { + it('falls back to EuiTablePagination defaults', () => { + const { getByText, getByTestSubject } = render( + + ); - const component = shallow(); + expect(getByText('Rows per page: 10')).toBeTruthy(); + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByTestSubject('tablePagination-10-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-25-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-50-rows')).toBeTruthy(); + }); - expect(component).toMatchSnapshot(); - }); + it('correctly uses configured EuiTablePagination defaults', () => { + const { getByText, getByTestSubject } = render( + + + , + { wrapper: undefined } + ); - test('render - show all pageSize', () => { - const props = { - pagination: { - pageIndex: 0, - pageSize: 0, - pageSizeOptions: [1, 5, 0], - totalItemCount: 5, - }, - onPageSizeChange: () => {}, - onPageChange: () => {}, - }; + expect(getByText('Rows per page: 5')).toBeTruthy(); + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByTestSubject('tablePagination-5-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-15-rows')).toBeTruthy(); + }); - const component = shallow(); + it('correctly overrides all defaults', () => { + const { getByText, getByTestSubject } = render( + + ); - expect(component).toMatchSnapshot(); + expect(getByText('Rows per page: 4')).toBeTruthy(); + fireEvent.click(getByTestSubject('tablePaginationPopoverButton')); + expect(getByTestSubject('tablePagination-2-rows')).toBeTruthy(); + expect(getByTestSubject('tablePagination-4-rows')).toBeTruthy(); + }); }); }); diff --git a/src/components/basic_table/pagination_bar.tsx b/src/components/basic_table/pagination_bar.tsx index 377eafe595b..d5eb6e137ac 100644 --- a/src/components/basic_table/pagination_bar.tsx +++ b/src/components/basic_table/pagination_bar.tsx @@ -7,8 +7,12 @@ */ import React, { useEffect } from 'react'; + import { EuiSpacer } from '../spacer'; -import { EuiTablePagination } from '../table'; +import { + EuiTablePagination, + useEuiTablePaginationDefaults, +} from '../table/table_pagination'; import { ItemsPerPageChangeHandler, PageChangeHandler, @@ -22,8 +26,10 @@ export interface Pagination { /** * The maximum number of items that can be shown in a single page. * Pass `0` to display the selected "Show all" option and hide the pagination. + * + * @default 10 */ - pageSize: number; + pageSize?: number; /** * The total number of items the page is "sliced" of */ @@ -31,10 +37,14 @@ export interface Pagination { /** * Configures the page size dropdown options. * Pass `0` as one of the options to create a "Show all" option. + * + * @default [10, 25, 50] */ pageSizeOptions?: number[]; /** - * Hides the page size dropdown + * Set to false to hide the page size dropdown + * + * @default true */ showPerPageOptions?: boolean; } @@ -50,10 +60,6 @@ export interface PaginationBarProps { 'aria-label'?: string; } -export const defaults = { - pageSizeOptions: [10, 25, 50], -}; - export const PaginationBar = ({ pagination, onPageSizeChange, @@ -61,26 +67,30 @@ export const PaginationBar = ({ 'aria-controls': ariaControls, 'aria-label': ariaLabel, }: PaginationBarProps) => { - const pageSizeOptions = pagination.pageSizeOptions - ? pagination.pageSizeOptions - : defaults.pageSizeOptions; - const pageCount = pagination.pageSize - ? Math.ceil(pagination.totalItemCount / pagination.pageSize) - : 1; + const defaults = useEuiTablePaginationDefaults(); + const { + pageIndex, + totalItemCount, + pageSize = defaults.itemsPerPage, + pageSizeOptions = defaults.itemsPerPageOptions, + showPerPageOptions = defaults.showPerPageOptions, + } = pagination; + + const pageCount = pageSize ? Math.ceil(totalItemCount / pageSize) : 1; useEffect(() => { - if (pageCount < pagination.pageIndex + 1) { + if (pageCount < pageIndex + 1) { onPageChange?.(pageCount - 1); } - }, [pageCount, onPageChange, pagination]); + }, [pageCount, onPageChange, pageIndex]); return (
{ pagination={{ pageIndex: 3, pageSize: 20, + pageSizeOptions: [20], onChangePage: () => {}, onChangeItemsPerPage: () => {}, }} diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 9a62eae3ffd..92b190a7565 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -13,6 +13,7 @@ import { GridOnItemsRenderedProps, } from 'react-window'; import { useGeneratedHtmlId } from '../../services'; +import { useEuiTablePaginationDefaults } from '../table/table_pagination'; import { EuiFocusTrap } from '../focus_trap'; import { EuiI18n, useEuiI18n } from '../i18n'; import { useMutationObserver } from '../observer/mutation_observer'; @@ -118,7 +119,7 @@ export const EuiDataGrid = forwardRef( className, gridStyle, toolbarVisibility = true, - pagination, + pagination: _pagination, sorting, inMemory, onColumnResize, @@ -134,6 +135,17 @@ export const EuiDataGrid = forwardRef( /** * Merge consumer settings with defaults */ + const paginationDefaults = useEuiTablePaginationDefaults(); + const pagination = useMemo(() => { + return _pagination + ? { + pageSize: paginationDefaults.itemsPerPage, + pageSizeOptions: paginationDefaults.itemsPerPageOptions, + ..._pagination, + } + : _pagination; + }, [_pagination, paginationDefaults]); + const gridStyleWithDefaults = useMemo( () => ({ ...startingStyles, ...gridStyle }), [gridStyle] diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index daf489cb48e..7bdf0d5ba8b 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -300,7 +300,7 @@ export type CommonGridProps = CommonProps & */ inMemory?: EuiDataGridInMemory; /** - * A #EuiDataGridPagination object. Omit to disable pagination completely. + * A #EuiDataGridPaginationProps object. Omit to disable pagination completely. */ pagination?: EuiDataGridPaginationProps; /** @@ -429,7 +429,7 @@ export interface EuiDataGridBodyProps { renderFooterCellValue?: EuiDataGridCellProps['renderCellValue']; renderCustomGridBody?: EuiDataGridProps['renderCustomGridBody']; interactiveCellId: EuiDataGridCellProps['interactiveCellId']; - pagination?: EuiDataGridPaginationProps; + pagination?: Required; headerIsInteractive: boolean; handleHeaderMutation: MutationCallback; setVisibleColumns: EuiDataGridHeaderRowProps['setVisibleColumns']; @@ -594,7 +594,7 @@ export interface EuiDataGridCellProps { rowHeightsOptions?: EuiDataGridRowHeightsOptions; rowHeightUtils?: RowHeightUtilsType; rowManager?: EuiDataGridRowManager; - pagination?: EuiDataGridPaginationProps; + pagination?: Required; } export interface EuiDataGridCellState { @@ -918,12 +918,16 @@ export interface EuiDataGridPaginationProps { /** * How many rows should initially be shown per page. * Pass `0` to display the selected "Show all" option and hide the pagination. + * + * @default 10 */ - pageSize: number; + pageSize?: number; /** * An array of page sizes the user can select from. * Pass `0` as one of the options to create a "Show all" option. - * Leave this prop undefined or use an empty array to hide "Rows per page" select button. + * Pass an empty array to hide "Rows per page" select button. + * + * @default [10, 25, 50] */ pageSizeOptions?: number[]; /** diff --git a/src/components/datagrid/utils/data_grid_pagination.test.tsx b/src/components/datagrid/utils/data_grid_pagination.test.tsx index e84c6ca7349..2c176b702ed 100644 --- a/src/components/datagrid/utils/data_grid_pagination.test.tsx +++ b/src/components/datagrid/utils/data_grid_pagination.test.tsx @@ -7,7 +7,8 @@ */ import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { fireEvent } from '@testing-library/react'; +import { render } from '../../../test/rtl'; import { DataGridFocusContext } from './focus'; import { mockFocusContext } from './__mocks__/focus_context'; @@ -27,134 +28,77 @@ describe('EuiDataGridPaginationRenderer', () => { beforeEach(() => jest.clearAllMocks()); it('renders', () => { - const component = shallow(); - expect(component).toMatchInlineSnapshot(` -
- -
- `); + const { container, getByText } = render( + + ); + expect(container.firstChild).toHaveClass('euiDataGrid__pagination'); + fireEvent.click(getByText('Rows per page: 25')); + expect(getByText('25 rows')).toBeTruthy(); + expect(getByText('Page 1 of 4')).toBeTruthy(); }); it('renders a detailed aria-label', () => { - const component = shallow( + const { getAllByLabelText } = render( ); - expect(component).toMatchInlineSnapshot(` -
- -
- `); + expect( + getAllByLabelText('Pagination for preceding grid: Test Grid') + ).toBeTruthy(); }); it('hides the page size selection if pageSizeOptions is empty', () => { - const component = shallow( + const { queryByTestSubject, queryByText } = render( ); - expect(component).toMatchInlineSnapshot(` -
- -
- `); + expect(queryByTestSubject('tablePaginationPopoverButton')).toBeFalsy(); + expect(queryByText('Rows per page:')).toBeFalsy(); }); it('handles the "show all" page size option', () => { - const component = shallow( + const { getByText } = render( ); - expect(component).toMatchInlineSnapshot(` -
- -
- `); + expect(getByText('Showing all rows')).toBeTruthy(); }); it('does not render if there are fewer rows than the smallest page size option', () => { - const component = shallow( + const { container } = render( ); - expect(component.isEmptyRender()).toBe(true); + expect(container).toBeEmptyDOMElement(); }); it('focuses the first data cell on page change', () => { - const component = mount( + const { getByTestSubject } = render( ); - const onChangePage: Function = component - .find('EuiTablePagination') - .prop('onChangePage'); - onChangePage(3); + fireEvent.click(getByTestSubject('pagination-button-2')); expect(mockFocusContext.setFocusedCell).toHaveBeenCalledWith([0, 0]); }); + + describe('EuiProvider component defaults', () => { + it('falls back to EuiTablePagination defaults if pageSize and pageSizeOptions are undefined', () => { + const { getByText } = render( + + ); + fireEvent.click(getByText('Rows per page: 10')); + expect(getByText('10 rows')).toBeTruthy(); + expect(getByText('25 rows')).toBeTruthy(); + expect(getByText('50 rows')).toBeTruthy(); + }); + }); }); diff --git a/src/components/datagrid/utils/data_grid_pagination.tsx b/src/components/datagrid/utils/data_grid_pagination.tsx index bbdcd21bad5..25ed1771b17 100644 --- a/src/components/datagrid/utils/data_grid_pagination.tsx +++ b/src/components/datagrid/utils/data_grid_pagination.tsx @@ -7,8 +7,12 @@ */ import React, { useCallback, useContext } from 'react'; + import { useEuiI18n } from '../../i18n'; // Note: this file must be named data_grid_pagination to match i18n tokens -import { EuiTablePagination } from '../../table/table_pagination'; +import { + EuiTablePagination, + useEuiTablePaginationDefaults, +} from '../../table/table_pagination'; import { EuiDataGridPaginationProps, EuiDataGridPaginationRendererProps, @@ -17,14 +21,18 @@ import { DataGridFocusContext } from './focus'; export const EuiDataGridPaginationRenderer = ({ pageIndex, - pageSize, - pageSizeOptions, + pageSize: _pageSize, + pageSizeOptions: _pageSizeOptions, onChangePage: _onChangePage, onChangeItemsPerPage, rowCount, controls, 'aria-label': ariaLabel, }: EuiDataGridPaginationRendererProps) => { + const defaults = useEuiTablePaginationDefaults(); + const pageSize = _pageSize ?? defaults.itemsPerPage; + const pageSizeOptions = _pageSizeOptions ?? defaults.itemsPerPageOptions; + const detailedPaginationLabel = useEuiI18n( 'euiDataGridPagination.detailedPaginationLabel', 'Pagination for preceding grid: {label}', @@ -46,8 +54,7 @@ export const EuiDataGridPaginationRenderer = ({ ); const pageCount = pageSize ? Math.ceil(rowCount / pageSize) : 1; - const minSizeOption = - pageSizeOptions && [...pageSizeOptions].sort((a, b) => a - b)[0]; + const minSizeOption = [...pageSizeOptions].sort((a, b) => a - b)[0]; if (rowCount < (minSizeOption || pageSize)) { /** @@ -58,8 +65,8 @@ export const EuiDataGridPaginationRenderer = ({ return null; } - // hide select rows per page if pageSizeOptions is undefined or an empty array - const hidePerPageOptions = !pageSizeOptions || pageSizeOptions.length === 0; + // Hide select rows per page if pageSizeOptions is an empty array + const hidePerPageOptions = pageSizeOptions.length === 0; return (
diff --git a/src/components/datagrid/utils/focus.test.tsx b/src/components/datagrid/utils/focus.test.tsx index a3ff7ec87bd..9b2e0db5d37 100644 --- a/src/components/datagrid/utils/focus.test.tsx +++ b/src/components/datagrid/utils/focus.test.tsx @@ -387,6 +387,7 @@ describe('createKeyDownHandler', () => { const pagination = { pageIndex: 0, pageSize: 5, + pageSizeOptions: [5], onChangePage: jest.fn(), onChangeItemsPerPage: () => {}, }; @@ -433,6 +434,7 @@ describe('createKeyDownHandler', () => { const pagination = { pageIndex: 1, pageSize: 5, + pageSizeOptions: [5], onChangePage: jest.fn(), onChangeItemsPerPage: () => {}, }; diff --git a/src/components/datagrid/utils/focus.ts b/src/components/datagrid/utils/focus.ts index d896efff152..0dd226faa33 100644 --- a/src/components/datagrid/utils/focus.ts +++ b/src/components/datagrid/utils/focus.ts @@ -181,7 +181,7 @@ export const createKeyDownHandler = ({ visibleRowCount: number; visibleRowStartIndex: number; rowCount: EuiDataGridProps['rowCount']; - pagination: EuiDataGridProps['pagination']; + pagination: Required; hasFooter: boolean; headerIsInteractive: boolean; focusContext: DataGridFocusContextShape; diff --git a/src/components/datagrid/utils/ref.test.ts b/src/components/datagrid/utils/ref.test.ts index 8a00ed87c07..eaad811867f 100644 --- a/src/components/datagrid/utils/ref.test.ts +++ b/src/components/datagrid/utils/ref.test.ts @@ -72,8 +72,9 @@ describe('useSortPageCheck', () => { describe('if the grid is paginated', () => { const pagination = { - pageSize: 20, pageIndex: 0, + pageSize: 20, + pageSizeOptions: [20], onChangePage: jest.fn(), onChangeItemsPerPage: jest.fn(), }; diff --git a/src/components/datagrid/utils/ref.ts b/src/components/datagrid/utils/ref.ts index fa0f70be4a2..a63ef367dd6 100644 --- a/src/components/datagrid/utils/ref.ts +++ b/src/components/datagrid/utils/ref.ts @@ -23,7 +23,7 @@ interface Dependencies { focusContext: DataGridFocusContextShape; cellPopoverContext: DataGridCellPopoverContextShape; sortingContext: DataGridSortingContextShape; - pagination: EuiDataGridProps['pagination']; + pagination: Required; rowCount: number; visibleColCount: number; } @@ -144,7 +144,7 @@ export const useCellLocationCheck = (rowCount: number, colCount: number) => { * paginating to that row. */ export const useSortPageCheck = ( - pagination: EuiDataGridProps['pagination'], + pagination: Required, sortedRowMap: DataGridSortingContextShape['sortedRowMap'] ) => { const findVisibleRowIndex = useCallback( diff --git a/src/components/datagrid/utils/row_count.ts b/src/components/datagrid/utils/row_count.ts index 7db5ea90834..50ef33df9a5 100644 --- a/src/components/datagrid/utils/row_count.ts +++ b/src/components/datagrid/utils/row_count.ts @@ -12,7 +12,7 @@ export const computeVisibleRows = ({ pagination, rowCount, }: { - pagination: EuiDataGridProps['pagination']; + pagination: Required; rowCount: EuiDataGridProps['rowCount']; }): EuiDataGridVisibleRows => { const startRow = diff --git a/src/components/focus_trap/__snapshots__/focus_trap.test.tsx.snap b/src/components/focus_trap/__snapshots__/focus_trap.test.tsx.snap index 585aafd24f9..7739a1b314d 100644 --- a/src/components/focus_trap/__snapshots__/focus_trap.test.tsx.snap +++ b/src/components/focus_trap/__snapshots__/focus_trap.test.tsx.snap @@ -42,22 +42,22 @@ exports[`EuiFocusTrap can be disabled 1`] = `
`; -exports[`EuiFocusTrap is rendered 1`] = ` -Array [ +exports[`EuiFocusTrap renders 1`] = ` +
, + />
-
, +
, -] + /> +
`; diff --git a/src/components/focus_trap/focus_trap.spec.tsx b/src/components/focus_trap/focus_trap.spec.tsx index 148a86b6193..eb3e68470ef 100644 --- a/src/components/focus_trap/focus_trap.spec.tsx +++ b/src/components/focus_trap/focus_trap.spec.tsx @@ -11,9 +11,11 @@ /// import React, { ComponentType, useRef, useState } from 'react'; -import { EuiFocusTrap } from './focus_trap'; + import { EuiPortal } from '../portal'; +import { EuiFocusTrap } from './focus_trap'; + describe('EuiFocusTrap', () => { describe('focus', () => { it('is set on the first focusable element by default', () => { @@ -359,6 +361,25 @@ describe('EuiFocusTrap', () => { expect(styles.getPropertyValue('padding-right')).to.equal('0px'); }); }); + + it('allows customizing gapMode via EuiProvider.componentDefaults', () => { + cy.mount(, { + providerProps: { + componentDefaults: { EuiFocusTrap: { gapMode: 'margin' } }, + }, + }); + skipIfNoScrollbars(); + cy.get('[data-test-subj="openFocusTrap"]').click(); + + cy.get('body').then(($body) => { + const styles = window.getComputedStyle($body[0]); + + const margin = parseFloat(styles.getPropertyValue('margin-right')); + expect(margin).to.be.gt(0); + + expect(styles.getPropertyValue('padding-right')).to.equal('0px'); + }); + }); }); }); }); diff --git a/src/components/focus_trap/focus_trap.stories.tsx b/src/components/focus_trap/focus_trap.stories.tsx index 3cf4971498b..dca4e061921 100644 --- a/src/components/focus_trap/focus_trap.stories.tsx +++ b/src/components/focus_trap/focus_trap.stories.tsx @@ -14,6 +14,7 @@ import { EuiFieldText } from '../form'; import { EuiSpacer } from '../spacer'; import { EuiPanel } from '../panel'; +import { EuiProvider } from '../provider'; import { EuiFocusTrap, EuiFocusTrapProps } from './focus_trap'; const meta: Meta = { @@ -21,9 +22,7 @@ const meta: Meta = { // @ts-ignore This still works for Storybook controls, even though Typescript complains component: EuiFocusTrap, argTypes: { - crossFrame: { - control: { type: 'boolean' }, - }, + returnFocus: { type: 'boolean' }, }, }; @@ -39,7 +38,11 @@ const StatefulFocusTrap = (props: Partial) => { - + setDisabled(true)} + > Focus trap is currently {disabled ? 'disabled' : 'enabled'} Button inside focus trap @@ -74,3 +77,16 @@ export const Iframe: Story = { ), args: { disabled: true, crossFrame: false }, }; + +export const EuiProviderComponentDefaults: Story = { + render: ({ ...args }) => ( + + + + This story is passing all controls and their arguments to EuiProvider's + `componentDefaults` instead of to EuiFocusTrap directly. It's primarily + useful for testing that configured defaults behave the same way as + individual props. + + ), +}; diff --git a/src/components/focus_trap/focus_trap.test.tsx b/src/components/focus_trap/focus_trap.test.tsx index e5baa0d94cd..1f177dc3abc 100644 --- a/src/components/focus_trap/focus_trap.test.tsx +++ b/src/components/focus_trap/focus_trap.test.tsx @@ -6,29 +6,26 @@ * Side Public License, v 1. */ -import React, { EventHandler } from 'react'; -import { mount } from 'enzyme'; +import React from 'react'; import { render } from '../../test/rtl'; -import { findTestSubject, takeMountedSnapshot } from '../../test'; +import { shouldRenderCustomStyles } from '../../test/internal'; -import { EuiEvent } from '../outside_click_detector/outside_click_detector'; import { EuiFocusTrap } from './focus_trap'; -import { EuiPortal } from '../portal'; describe('EuiFocusTrap', () => { - test('is rendered', () => { - const component = mount( + shouldRenderCustomStyles(Test); + + it('renders', () => { + const { container } = render(
); - expect( - takeMountedSnapshot(component, { hasArrayOutput: true }) - ).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); - test('can be disabled', () => { + it('can be disabled', () => { const { container } = render(
@@ -38,7 +35,7 @@ describe('EuiFocusTrap', () => { expect(container).toMatchSnapshot(); }); - test('accepts className and style', () => { + it('accepts className and style', () => { const { container } = render(
@@ -50,8 +47,8 @@ describe('EuiFocusTrap', () => { describe('behavior', () => { describe('focus', () => { - test('is set on the first focusable element by default', () => { - const component = mount( + it('is set on the first focusable element by default', () => { + const { getByTestSubject } = render(
@@ -63,13 +60,11 @@ describe('EuiFocusTrap', () => {
); - expect(findTestSubject(component, 'input').getDOMNode()).toBe( - document.activeElement - ); + expect(getByTestSubject('input')).toBe(document.activeElement); }); - test('will blur focus when negating `autoFocus`', () => { - mount( + it('will blur focus when negating `autoFocus`', () => { + render(
@@ -84,8 +79,8 @@ describe('EuiFocusTrap', () => { expect(document.body).toBe(document.activeElement); }); - test('is set on the element identified by `data-autofocus`', () => { - const component = mount( + it('is set on the element identified by `data-autofocus`', () => { + const { getByTestSubject } = render(
@@ -97,155 +92,7 @@ describe('EuiFocusTrap', () => {
); - expect(findTestSubject(component, 'input2').getDOMNode()).toBe( - document.activeElement - ); - }); - }); - - // skipping because react-focus-on / react-focus-lock uses two handlers, - // one on the container to record what element was clicked and a second - // on the document, checking if the event target is the same on both - // because enzyme doesn't bubble the event, it is difficult to simulate - // the browser behaviour - we can revisit these tests when we have an - // actual browser environment - describe.skip('clickOutsideDisables', () => { - // enzyme doesn't mount the components into the global jsdom `document` - // but that's where the click detector listener is, - // pass the top-level mounted component's click event on to document - const triggerDocumentMouseDown: EventHandler = ( - e: React.MouseEvent - ) => { - const event = new Event('mousedown') as EuiEvent; - event.euiGeneratedBy = ( - e.nativeEvent as unknown as EuiEvent - ).euiGeneratedBy; - document.dispatchEvent(event); - }; - - const triggerDocumentMouseUp: EventHandler = ( - e: React.MouseEvent - ) => { - const event = new Event('mousedown') as EuiEvent; - event.euiGeneratedBy = ( - e.nativeEvent as unknown as EuiEvent - ).euiGeneratedBy; - document.dispatchEvent(event); - }; - - test('trap remains enabled when false', () => { - const component = mount( -
- -
- - -
-
-
- ); - - // The existence of `data-focus-lock-disabled=false` indicates that the trap is enabled. - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - findTestSubject(component, 'outside').simulate('mousedown'); - findTestSubject(component, 'outside').simulate('mouseup'); - // `react-focus-lock` relies on real DOM events to move focus about. - // Exposed attributes are the most consistent way to attain its state. - // See https://github.com/theKashey/react-focus-lock/blob/master/_tests/FocusLock.spec.js for the lib in use - // Trap remains enabled - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - }); - - test('trap remains enabled after internal clicks', () => { - const component = mount( -
- -
- - -
-
-
- ); - - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - findTestSubject(component, 'input2').simulate('mousedown'); - findTestSubject(component, 'input2').simulate('mouseup'); - // Trap remains enabled - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - }); - - test('trap remains enabled after internal portal clicks', () => { - const component = mount( -
- -
- - - - - -
-
-
- ); - - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - findTestSubject(component, 'input3').simulate('mousedown'); - findTestSubject(component, 'input3').simulate('mouseup'); - // Trap remains enabled - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - }); - - test('trap becomes disabled on outside clicks', () => { - const component = mount( -
- -
- - -
-
-
- ); - - expect( - component.find('[data-focus-lock-disabled=false]').length - ).not.toBeLessThan(1); - findTestSubject(component, 'outside').simulate('mousedown'); - findTestSubject(component, 'outside').simulate('mouseup'); - // Trap becomes disabled - expect(component.find('[data-focus-lock-disabled=false]').length).toBe( - 0 - ); + expect(getByTestSubject('input2')).toBe(document.activeElement); }); }); }); diff --git a/src/components/focus_trap/focus_trap.tsx b/src/components/focus_trap/focus_trap.tsx index 337ea03938f..e71a892de1e 100644 --- a/src/components/focus_trap/focus_trap.tsx +++ b/src/components/focus_trap/focus_trap.tsx @@ -6,17 +6,40 @@ * Side Public License, v 1. */ -import React, { Component, CSSProperties } from 'react'; +import React, { Component, FunctionComponent, CSSProperties } from 'react'; import { FocusOn } from 'react-focus-on'; import { ReactFocusOnProps } from 'react-focus-on/dist/es5/types'; import { RemoveScrollBar } from 'react-remove-scroll-bar'; import { CommonProps } from '../common'; import { findElementBySelectorOrRef, ElementTarget } from '../../services'; +import { usePropsWithComponentDefaults } from '../provider/component_defaults'; export type FocusTarget = ElementTarget; -interface EuiFocusTrapInterface { +export type EuiFocusTrapProps = Omit< + ReactFocusOnProps, + // Inverted `disabled` prop used instead + | 'enabled' + // Omitted so that our props table & storybook actually register these props + | 'style' + | 'className' + | 'css' + // Props that differ from react-focus-on's default settings + | 'gapMode' + | 'crossFrame' + | 'scrollLock' + | 'noIsolation' + | 'returnFocus' +> & { + // For some reason, Storybook doesn't register these props if they're Pick<>'d + className?: CommonProps['className']; + css?: CommonProps['css']; + style?: CSSProperties; + /** + * @default false + */ + disabled?: boolean; /** * Whether `onClickOutside` should be called on mouseup instead of mousedown. * This flag can be used to prevent conflicts with outside toggle buttons by delaying the closing click callback. @@ -24,32 +47,54 @@ interface EuiFocusTrapInterface { closeOnMouseup?: boolean; /** * Clicking outside the trap area will disable the trap + * @default false */ clickOutsideDisables?: boolean; /** * Reference to element that will get focus when the trap is initiated */ initialFocus?: FocusTarget; - style?: CSSProperties; /** * if `scrollLock` is set to true, the body's scrollbar width will be preserved on lock * via the `gapMode` CSS property. Depending on your custom CSS, you may prefer to use * `margin` instead of `padding`. + * @default padding */ gapMode?: 'padding' | 'margin'; - disabled?: boolean; -} + /** + * Configures focus trapping between iframes. + * By default, EuiFocusTrap allows focus to leave iframes and move to elements outside of it. + * Set to `true` if you want focus to remain trapped within the iframe. + * @default false + */ + crossFrame?: ReactFocusOnProps['crossFrame']; + /** + * @default false + */ + scrollLock?: ReactFocusOnProps['scrollLock']; + /** + * @default true + */ + noIsolation?: ReactFocusOnProps['noIsolation']; + /** + * @default true + */ + returnFocus?: ReactFocusOnProps['returnFocus']; +}; -export interface EuiFocusTrapProps - extends CommonProps, - Omit, // Inverted `disabled` prop used instead - EuiFocusTrapInterface {} +export const EuiFocusTrap: FunctionComponent = (props) => { + const propsWithDefaults = usePropsWithComponentDefaults( + 'EuiFocusTrap', + props + ); + return ; +}; interface State { hasBeenDisabledByClick: boolean; } -export class EuiFocusTrap extends Component { +class EuiFocusTrapClass extends Component { static defaultProps = { clickOutsideDisables: false, disabled: false, diff --git a/src/components/portal/portal.spec.tsx b/src/components/portal/portal.spec.tsx index 3c08ac5b231..95393066d14 100644 --- a/src/components/portal/portal.spec.tsx +++ b/src/components/portal/portal.spec.tsx @@ -10,8 +10,9 @@ /// /// -import React, { useState } from 'react'; -import { EuiPortal, EuiPortalProps } from './index'; +import React, { useState, useEffect, FunctionComponent } from 'react'; + +import { EuiPortal, EuiPortalProps } from './portal'; describe('EuiPortal', () => { describe('insertion', () => { @@ -124,5 +125,80 @@ describe('EuiPortal', () => { }); }); }); + + describe('`insert` inherited from EuiProvider.componentDefaults', () => { + const sibling = document.createElement('div'); + sibling.id = 'sibling'; + + const Wrapper: FunctionComponent = ({ children }) => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + document.body.appendChild(sibling); + setMounted(true); + }, []); + + return <>{mounted && children}; + }; + + it('allows configuring the default `insert` for all EuiPortal components', () => { + cy.mount( + + Hello + World + , + { + providerProps: { + componentDefaults: { + EuiPortal: { insert: { sibling, position: 'before' } }, + }, + }, + } + ); + + // verify all portal elements were appended before the sibling + cy.get('div[data-euiportal]').then((portals) => { + cy.get('div#sibling').then((siblings) => { + expect(portals).to.have.lengthOf(2); + expect(siblings).to.have.lengthOf(1); + const beforeSibling = siblings.get(0).previousElementSibling; + expect(beforeSibling).to.equal(portals.get(1)); + expect(beforeSibling?.previousElementSibling).to.equal( + portals.get(0) + ); + }); + }); + }); + + it('still allows overriding defaults via component props', () => { + cy.mount( + + Hello + + World + + , + { + providerProps: { + componentDefaults: { + EuiPortal: { insert: { sibling: sibling, position: 'before' } }, + }, + }, + } + ); + + // verify portal elements were appended before and after the sibling + cy.get('div[data-euiportal]').then((portals) => { + cy.get('div#sibling').then((siblings) => { + expect(portals).to.have.lengthOf(2); + expect(siblings).to.have.lengthOf(1); + expect(siblings.get(0).previousElementSibling).to.equal( + portals.get(0) + ); + expect(siblings.get(0).nextElementSibling).to.equal(portals.get(1)); + }); + }); + }); + }); }); }); diff --git a/src/components/portal/portal.tsx b/src/components/portal/portal.tsx index 72cc1c52cb2..f4f24ba9d21 100644 --- a/src/components/portal/portal.tsx +++ b/src/components/portal/portal.tsx @@ -11,41 +11,50 @@ * into portals. */ -import { Component, ContextType, ReactNode } from 'react'; +import React, { + FunctionComponent, + Component, + ContextType, + ReactNode, +} from 'react'; import { createPortal } from 'react-dom'; import { EuiNestedThemeContext } from '../../services'; -import { keysOf } from '../common'; +import { usePropsWithComponentDefaults } from '../provider/component_defaults'; -interface InsertPositionsMap { - after: InsertPosition; - before: InsertPosition; -} - -export const insertPositions: InsertPositionsMap = { +const INSERT_POSITIONS = ['after', 'before'] as const; +type EuiPortalInsertPosition = (typeof INSERT_POSITIONS)[number]; +const insertPositions: Record = { after: 'afterend', before: 'beforebegin', }; -type EuiPortalInsertPosition = keyof typeof insertPositions; - -export const INSERT_POSITIONS: EuiPortalInsertPosition[] = - keysOf(insertPositions); - export interface EuiPortalProps { /** * ReactNode to render as this component's content */ children: ReactNode; - insert?: { sibling: HTMLElement; position: 'before' | 'after' }; + /** + * If not specified, `EuiPortal` will insert itself + * into the end of the `document.body` by default + */ + insert?: { sibling: HTMLElement; position: EuiPortalInsertPosition }; + /** + * Optional ref callback + */ portalRef?: (ref: HTMLDivElement | null) => void; } +export const EuiPortal: FunctionComponent = (props) => { + const propsWithDefaults = usePropsWithComponentDefaults('EuiPortal', props); + return ; +}; + interface EuiPortalState { portalNode: HTMLDivElement | null; } -export class EuiPortal extends Component { +export class EuiPortalClass extends Component { static contextType = EuiNestedThemeContext; declare context: ContextType; diff --git a/src/components/provider/component_defaults/component_defaults.test.tsx b/src/components/provider/component_defaults/component_defaults.test.tsx new file mode 100644 index 00000000000..f58a2bfbfe8 --- /dev/null +++ b/src/components/provider/component_defaults/component_defaults.test.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FunctionComponent, PropsWithChildren } from 'react'; +import { renderHook } from '../../../test/rtl'; + +import { + EuiComponentDefaultsProvider, + useComponentDefaults, + usePropsWithComponentDefaults, +} from './component_defaults'; + +describe('EuiComponentDefaultsProvider', () => { + describe('useComponentDefaults', () => { + it('allows accessing provided `componentDefaults` from anywhere', () => { + const wrapper: FunctionComponent = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(useComponentDefaults, { wrapper }); + + expect(result.current).toEqual({ + EuiPortal: { + insert: { + position: 'before', + sibling: expect.any(HTMLDivElement), + }, + }, + }); + }); + }); + + describe('usePropsWithComponentDefaults', () => { + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ); + + 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 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 new file mode 100644 index 00000000000..8a422924d2b --- /dev/null +++ b/src/components/provider/component_defaults/component_defaults.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { + createContext, + useContext, + useMemo, + FunctionComponent, + PropsWithChildren, +} from 'react'; + +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?: Pick; + /** + * Provide a global configuration for EuiFocusTrap's `gapMode` and `crossFrame` props + */ + EuiFocusTrap?: Pick; + /** + * 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. + */ + EuiTablePagination?: Pick< + EuiTablePaginationProps, + 'itemsPerPage' | 'itemsPerPageOptions' | 'showPerPageOptions' + >; +}; + +// Declaring as a static const for reference integrity/reducing rerenders +const emptyDefaults = {}; + +/* + * Context + */ +export const EuiComponentDefaultsContext = + createContext(emptyDefaults); + +/* + * Component + */ +export type EuiComponentDefaultsProviderProps = PropsWithChildren & { + componentDefaults?: EuiComponentDefaults; +}; +export const EuiComponentDefaultsProvider: FunctionComponent< + EuiComponentDefaultsProviderProps +> = ({ componentDefaults = emptyDefaults, children }) => { + return ( + + {children} + + ); +}; + +/* + * Hooks + */ +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/provider/component_defaults/index.ts b/src/components/provider/component_defaults/index.ts new file mode 100644 index 00000000000..74e16dcac4a --- /dev/null +++ b/src/components/provider/component_defaults/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './component_defaults'; diff --git a/src/components/provider/index.ts b/src/components/provider/index.ts index 796ec921a23..d8ccf6c9ad2 100644 --- a/src/components/provider/index.ts +++ b/src/components/provider/index.ts @@ -8,3 +8,9 @@ export type { EuiProviderProps } from './provider'; export { EuiProvider } from './provider'; + +export type { EuiComponentDefaultsProviderProps } from './component_defaults'; +export { + EuiComponentDefaultsProvider, + EuiComponentDefaultsContext, +} from './component_defaults'; diff --git a/src/components/provider/provider.tsx b/src/components/provider/provider.tsx index aff5a6f9931..c9e39613e70 100644 --- a/src/components/provider/provider.tsx +++ b/src/components/provider/provider.tsx @@ -24,6 +24,10 @@ import { emitEuiProviderWarning } from '../../services/theme/warning'; import { EuiThemeAmsterdam } from '../../themes'; import { EuiCacheProvider } from './cache'; import { EuiProviderNestedCheck, useIsNestedEuiProvider } from './nested'; +import { + EuiComponentDefaults, + EuiComponentDefaultsProvider, +} from './component_defaults'; const isEmotionCacheObject = ( obj: EmotionCache | Object @@ -64,6 +68,16 @@ export interface EuiProviderProps global?: EmotionCache; utility?: EmotionCache; }; + /** + * Allows configuring specified component defaults across all usages, overriding + * baseline EUI component defaults. + * + * Not all components will be supported, and configurable component defaults + * will be considered on a case-by-case basis. + * + * Individual component prop usages will always override these defaults. + */ + componentDefaults?: EuiComponentDefaults; } export const EuiProvider = ({ @@ -73,6 +87,7 @@ export const EuiProvider = ({ utilityClasses: Utilities = EuiUtilityClasses, colorMode, modify, + componentDefaults, children, }: PropsWithChildren>) => { const isNested = useIsNestedEuiProvider(); @@ -127,9 +142,11 @@ export const EuiProvider = ({ /> )} - - {children} - + + + {children} + + 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 3536c41eb82..9e5da8681c2 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,309 @@ // 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/index.ts b/src/components/table/table_pagination/index.ts index 2607fe7f966..49eb8b55eaa 100644 --- a/src/components/table/table_pagination/index.ts +++ b/src/components/table/table_pagination/index.ts @@ -8,3 +8,9 @@ export type { EuiTablePaginationProps } from './table_pagination'; export { EuiTablePagination } from './table_pagination'; + +// Not public top-level exports +export { + useEuiTablePaginationDefaults, + euiTablePaginationDefaults, +} from './table_pagination_defaults'; diff --git a/src/components/table/table_pagination/table_pagination.test.tsx b/src/components/table/table_pagination/table_pagination.test.tsx index d05e77f5972..42ad1c6b9df 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 3c94efc0cce..9d80976a178 100644 --- a/src/components/table/table_pagination/table_pagination.tsx +++ b/src/components/table/table_pagination/table_pagination.tsx @@ -20,6 +20,9 @@ import { EuiPagination, EuiPaginationProps } from '../../pagination'; import { EuiPopover } from '../../popover'; import { EuiI18n } from '../../i18n'; +import { usePropsWithComponentDefaults } from '../../provider/component_defaults'; +import { euiTablePaginationDefaults } from './table_pagination_defaults'; + export type PageChangeHandler = EuiPaginationProps['onPageClick']; export type ItemsPerPageChangeHandler = (pageSize: number) => void; @@ -27,16 +30,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 10 */ 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, 25, 50] */ itemsPerPageOptions?: number[]; /** @@ -51,16 +60,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 = euiTablePaginationDefaults.itemsPerPage, + itemsPerPageOptions = euiTablePaginationDefaults.itemsPerPageOptions, + showPerPageOptions = euiTablePaginationDefaults.showPerPageOptions, + onChangeItemsPerPage, + onChangePage, + pageCount, + ...rest + } = usePropsWithComponentDefaults('EuiTablePagination', props); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const togglePopover = useCallback(() => { @@ -105,7 +118,7 @@ export const EuiTablePagination: FunctionComponent = ({ icon={itemsPerPageOption === itemsPerPage ? 'check' : 'empty'} onClick={() => { closePopover(); - onChangeItemsPerPage(itemsPerPageOption); + onChangeItemsPerPage?.(itemsPerPageOption); }} data-test-subj={`tablePagination-${itemsPerPageOption}-rows`} > @@ -134,7 +147,10 @@ export const EuiTablePagination: FunctionComponent = ({ panelPaddingSize="none" anchorPosition="upRight" > - + ); diff --git a/src/components/table/table_pagination/table_pagination_defaults.ts b/src/components/table/table_pagination/table_pagination_defaults.ts new file mode 100644 index 00000000000..14ac6a12eb1 --- /dev/null +++ b/src/components/table/table_pagination/table_pagination_defaults.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useMemo } from 'react'; +import { useComponentDefaults } from '../../provider/component_defaults'; + +import { EuiTablePaginationProps } from './table_pagination'; + +/** + * Table pagination prop defaults live in a separate file because + * they'll be reused by basic tables and datagrids as fallbacks + */ + +export const euiTablePaginationDefaults: Required< + Pick< + EuiTablePaginationProps, + 'itemsPerPage' | 'itemsPerPageOptions' | 'showPerPageOptions' + > +> = { + itemsPerPage: 10, + itemsPerPageOptions: [10, 25, 50], + showPerPageOptions: true, +}; + +export const useEuiTablePaginationDefaults = () => { + const consumerDefaults = useComponentDefaults().EuiTablePagination; + + return useMemo( + () => ({ + ...euiTablePaginationDefaults, + ...consumerDefaults, + }), + [consumerDefaults] + ); +}; diff --git a/upcoming_changelogs/6923.md b/upcoming_changelogs/6923.md new file mode 100644 index 00000000000..e78ffcbf233 --- /dev/null +++ b/upcoming_changelogs/6923.md @@ -0,0 +1 @@ +- Added beta `componentDefaults` prop to `EuiProvider`, which will allow configuring certain default props globally. This list of components and defaults is still under consideration. diff --git a/upcoming_changelogs/6941.md b/upcoming_changelogs/6941.md new file mode 100644 index 00000000000..472e5fe66a6 --- /dev/null +++ b/upcoming_changelogs/6941.md @@ -0,0 +1 @@ +- `EuiPortal`'s `insert` prop can now be configured globally via `EuiProvider.componentDefaults` diff --git a/upcoming_changelogs/6942.md b/upcoming_changelogs/6942.md new file mode 100644 index 00000000000..1ae2f2c4d1a --- /dev/null +++ b/upcoming_changelogs/6942.md @@ -0,0 +1 @@ +- `EuiFocusTrap`'s `crossFrame` and `gapMode` props can now be configured globally via `EuiProvider.componentDefaults` diff --git a/upcoming_changelogs/6951.md b/upcoming_changelogs/6951.md new file mode 100644 index 00000000000..98f082b1149 --- /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` diff --git a/upcoming_changelogs/6993.md b/upcoming_changelogs/6993.md new file mode 100644 index 00000000000..75a11f2635f --- /dev/null +++ b/upcoming_changelogs/6993.md @@ -0,0 +1,7 @@ +- `EuiBasicTable`, `EuiInMemoryTable`, and `EuiDataGrid` now allow `pagination.pageSize` to be undefined. If undefined, `pageSize` defaults to `EuiTablePagination`'s `itemsPerPage` component default. +- `EuiBasicTable`, `EuiInMemoryTable`, and `EuiDataGrid`'s `pagination.pageSizeOptions` will now fall back to `EuiTablePagination`'s `itemsPerPageOptions` component default. + +**Breaking changes** + +- `EuiTablePagination`'s default `itemsPerPage` is now `10` (was previously `50`). This can be configured through `EuiProvider.componentDefaults`. +- `EuiTablePagination`'s default `itemsPerPageOptions` is now `[10, 25, 50]` (was previously `[10, 20, 50, 100]`). This can be configured through `EuiProvider.componentDefaults`.