diff --git a/packages/eui-docgen/src/filter_prop.ts b/packages/eui-docgen/src/filter_prop.ts index b23d15246f3..8defb27ae07 100644 --- a/packages/eui-docgen/src/filter_prop.ts +++ b/packages/eui-docgen/src/filter_prop.ts @@ -35,13 +35,16 @@ const allowedParents = [ 'RefAttributes', ]; +// components/types that should allow all props, even from external modules +const allowedComponents = ['EuiDataGridVirtualizationOptions']; + /** * Filter props to remove props from node modules while keeping those whitelisted */ export const filterProp = ( prop: PropItem, component: any, - componentExtends: string[] + componentExtends: Record ) => { if (allowedProps.includes(prop.name)) { return true; @@ -79,18 +82,24 @@ export const filterProp = ( } if (prop.parent) { - //Check if props are extended from other node module + // Check if props are extended from other node module if (allowedParents.includes(prop.parent.name)) return true; - if (!componentExtends.includes(prop.parent.name)) { - componentExtends.push(prop.parent.name); + + if (!componentExtends[component.name]) { + componentExtends[component.name] = []; } + if (!componentExtends[component.name].includes(prop.parent.name)) { + componentExtends[component.name].push(prop.parent.name); + } + if (allowedProps.includes(prop.name)) { return true; } + if (allowedComponents.includes(component.name)) return true; if (prop.parent.fileName.includes('@elastic/charts')) return true; return !prop.parent.fileName.includes('node_modules'); } return true; -} +}; diff --git a/packages/eui-docgen/src/main.ts b/packages/eui-docgen/src/main.ts index c0ba044fc70..501045e9b51 100644 --- a/packages/eui-docgen/src/main.ts +++ b/packages/eui-docgen/src/main.ts @@ -36,7 +36,7 @@ const main = async () => { for (const file of files) { const fileRelativePath = path.relative(euiSrcPath, file); - const componentExtends: Array = []; + const componentExtends: Record = {}; const parser = docgen.withCustomConfig(path.join(euiPackagePath, 'tsconfig.json'), { propFilter: (prop, component) => filterProp(prop, component, componentExtends), shouldExtractLiteralValuesFromEnum: true, @@ -51,7 +51,7 @@ const main = async () => { const processedComponent = processComponent({ componentDoc: parsedComponent, filePath: fileRelativePath, - componentExtends: componentExtends, + componentExtends: componentExtends[parsedComponent.displayName] || [], }); if (!processedComponent) { diff --git a/packages/eui/scripts/compile-eui.js b/packages/eui/scripts/compile-eui.js index 6d37b257731..41a2e55a874 100755 --- a/packages/eui/scripts/compile-eui.js +++ b/packages/eui/scripts/compile-eui.js @@ -14,6 +14,7 @@ const IGNORE_TESTS = [ '**/*.spec.tsx', '**/*.stories.ts', '**/*.stories.tsx', + '**/*.docgen.tsx', '**/**.stories.utils.ts', '**/**.stories.utils.tsx', '**/*.mdx', diff --git a/packages/eui/scripts/dtsgenerator.js b/packages/eui/scripts/dtsgenerator.js index 0b261b7a723..973020e6d56 100644 --- a/packages/eui/scripts/dtsgenerator.js +++ b/packages/eui/scripts/dtsgenerator.js @@ -39,6 +39,7 @@ const generator = dtsGenerator({ '**/__mocks__/*', 'src/test/**/*', // Separate d.ts files are generated for test utils 'src-docs/**/*', // Don't include src-docs + '**/*.docgen.tsx', // Don't include "components" generated just for react-docgen '**/*.mdx', // Don't include storybook mdx files ], resolveModuleId(params) { diff --git a/packages/eui/src-docs/src/views/datagrid/schema_columns/column_dragging.js b/packages/eui/src-docs/src/views/datagrid/schema_columns/column_dragging.js index fd3e11e4d60..8128f1175ba 100644 --- a/packages/eui/src-docs/src/views/datagrid/schema_columns/column_dragging.js +++ b/packages/eui/src-docs/src/views/datagrid/schema_columns/column_dragging.js @@ -1,8 +1,7 @@ import React, { useState, useCallback } from 'react'; +import { EuiDataGrid, EuiAvatar } from '@elastic/eui'; import { faker } from '@faker-js/faker'; -import { EuiDataGrid, EuiAvatar } from '../../../../../src/components'; - const columns = [ { id: 'avatar', @@ -69,11 +68,12 @@ export default () => { return ( data[rowIndex][columnId]} diff --git a/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx b/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx index e20aa582ffb..d43c423c333 100644 --- a/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx +++ b/packages/eui/src/components/datagrid/controls/data_grid_toolbar.stories.tsx @@ -22,15 +22,15 @@ import type { EuiDataGridToolBarVisibilityOptions, EuiDataGridToolBarAdditionalControlsOptions, } from '../data_grid_types'; +import { ToolbarStorybookComponent } from '../data_grid_types.docgen'; import { - EuiDataGridToolbarPropsComponent, StatefulDataGrid, defaultStorybookArgs, } from '../data_grid.stories.utils'; const meta: Meta = { title: 'Tabular Content/EuiDataGrid/toolbarVisibility (prop)', - component: EuiDataGridToolbarPropsComponent, + component: ToolbarStorybookComponent, }; export default meta; diff --git a/packages/eui/src/components/datagrid/data_grid.stories.utils.tsx b/packages/eui/src/components/datagrid/data_grid.stories.utils.tsx index a30a30af04b..b4a87032e76 100644 --- a/packages/eui/src/components/datagrid/data_grid.stories.utils.tsx +++ b/packages/eui/src/components/datagrid/data_grid.stories.utils.tsx @@ -8,12 +8,7 @@ /* eslint-disable storybook/default-exports, storybook/prefer-pascal-case */ -import React, { - useCallback, - useEffect, - useState, - FunctionComponent, -} from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { css } from '@emotion/react'; import { faker } from '@faker-js/faker'; @@ -29,10 +24,6 @@ import type { EuiDataGridColumnCellActionProps, EuiDataGridColumnSortingConfig, EuiDataGridProps, - EuiDataGridStyle, - EuiDataGridRowHeightsOptions, - EuiDataGridToolBarVisibilityOptions, - EuiDataGridToolBarAdditionalControlsOptions, } from './data_grid_types'; import { EuiDataGrid } from './data_grid'; @@ -405,25 +396,3 @@ export const StatefulDataGrid = (props: EuiDataGridProps) => { /> ); }; - -/* - * Components that exist purely for allowing Storybook to parse certain nested - * interfaces/types into specific example control tables. - * - * For whatever reason, they needs to be in a separate file for Storybook's - * react-typescript-docgen to parse the jsdoc comments into the controls table - */ - -export const EuiDataGridToolbarPropsComponent: FunctionComponent< - EuiDataGridProps & // We really just want toolbarVisibility and renderCustomToolbar from here, but typescript-docgen is unhappy if we Pick<> - EuiDataGridToolBarVisibilityOptions & - EuiDataGridToolBarAdditionalControlsOptions -> = () => <>; - -export const EuiDataGridStylePropsComponent: FunctionComponent< - EuiDataGridStyle -> = () => <>; - -export const EuiDataGridRowHeightsPropsComponent: FunctionComponent< - EuiDataGridRowHeightsOptions -> = () => <>; diff --git a/packages/eui/src/components/datagrid/data_grid_row_heights.stories.tsx b/packages/eui/src/components/datagrid/data_grid_row_heights.stories.tsx index 5100b100c95..ec6c553ff89 100644 --- a/packages/eui/src/components/datagrid/data_grid_row_heights.stories.tsx +++ b/packages/eui/src/components/datagrid/data_grid_row_heights.stories.tsx @@ -13,13 +13,13 @@ import { enableFunctionToggleControls } from '../../../.storybook/utils'; import { StatefulDataGrid, defaultStorybookArgs, - EuiDataGridRowHeightsPropsComponent, } from './data_grid.stories.utils'; +import { EuiDataGridRowHeightsOptions as Component } from './data_grid_types.docgen'; import type { EuiDataGridRowHeightsOptions } from './data_grid_types'; const meta: Meta = { title: 'Tabular Content/EuiDataGrid/rowHeightsOptions (prop)', - component: EuiDataGridRowHeightsPropsComponent, + component: Component, parameters: { codeSnippet: { snippet: ``, diff --git a/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx b/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx index 715fddd8799..cb6efe9826a 100644 --- a/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx +++ b/packages/eui/src/components/datagrid/data_grid_styles.stories.tsx @@ -14,13 +14,13 @@ import { enableFunctionToggleControls } from '../../../.storybook/utils'; import { StatefulDataGrid, defaultStorybookArgs, - EuiDataGridStylePropsComponent, } from './data_grid.stories.utils'; +import { EuiDataGridStyle as Component } from './data_grid_types.docgen'; import type { EuiDataGridStyle } from './data_grid_types'; const meta: Meta = { title: 'Tabular Content/EuiDataGrid/gridStyle (prop)', - component: EuiDataGridStylePropsComponent, + component: Component, }; export default meta; diff --git a/packages/eui/src/components/datagrid/data_grid_types.docgen.tsx b/packages/eui/src/components/datagrid/data_grid_types.docgen.tsx new file mode 100644 index 00000000000..373fffc8908 --- /dev/null +++ b/packages/eui/src/components/datagrid/data_grid_types.docgen.tsx @@ -0,0 +1,122 @@ +/* + * 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 } from 'react'; +import * as DataGridTypes from './data_grid_types'; +import { EuiDataGridToolbarControlProps } from './controls/data_grid_toolbar_control'; + +// This file only exists for react-typescript-docgen (used by both EUI+ and Storybook), +// which has difficulty extracting typescript definitions that aren't directly props of +// components. NOTE: This file should *NOT* be exported publicly. +// @see packages/eui-docgen + +export const EuiDataGridColumnVisibility: FunctionComponent< + DataGridTypes.EuiDataGridColumnVisibility +> = () => <>; + +export const EuiDataGridPaginationProps: FunctionComponent< + DataGridTypes.EuiDataGridPaginationProps +> = () => <>; + +export const EuiDataGridSorting: FunctionComponent< + DataGridTypes.EuiDataGridSorting +> = () => <>; + +export const EuiDataGridColumn: FunctionComponent< + DataGridTypes.EuiDataGridColumn +> = () => <>; + +export const EuiDataGridColumnActions: FunctionComponent< + DataGridTypes.EuiDataGridColumnActions +> = () => <>; + +export const EuiDataGridColumnCellActionProps: FunctionComponent< + DataGridTypes.EuiDataGridColumnCellActionProps +> = () => <>; + +export const EuiDataGridControlColumn: FunctionComponent< + DataGridTypes.EuiDataGridControlColumn +> = () => <>; + +export const EuiDataGridSchemaDetector: FunctionComponent< + DataGridTypes.EuiDataGridSchemaDetector +> = () => <>; + +export const EuiDataGridCellValueElementProps: FunctionComponent< + DataGridTypes.EuiDataGridCellValueElementProps +> = () => <>; + +export const EuiDataGridCellPopoverElementProps: FunctionComponent< + DataGridTypes.EuiDataGridCellPopoverElementProps +> = () => <>; + +export const EuiDataGridToolBarVisibilityOptions: FunctionComponent< + DataGridTypes.EuiDataGridToolBarVisibilityOptions +> = () => <>; + +export const EuiDataGridToolBarAdditionalControlsOptions: FunctionComponent< + DataGridTypes.EuiDataGridToolBarAdditionalControlsOptions +> = () => <>; + +export const EuiDataGridToolBarAdditionalControlsLeftOptions: FunctionComponent< + DataGridTypes.EuiDataGridToolBarAdditionalControlsLeftOptions +> = () => <>; + +export const EuiDataGridCustomToolbarProps: FunctionComponent< + DataGridTypes.EuiDataGridCustomToolbarProps +> = () => <>; + +export const EuiDataGridToolbarControl: FunctionComponent< + EuiDataGridToolbarControlProps +> = () => <>; + +export const EuiDataGridToolBarVisibilityColumnSelectorOptions: FunctionComponent< + DataGridTypes.EuiDataGridToolBarVisibilityColumnSelectorOptions +> = () => <>; + +export const EuiDataGridToolBarVisibilityDisplaySelectorOptions: FunctionComponent< + DataGridTypes.EuiDataGridToolBarVisibilityDisplaySelectorOptions +> = () => <>; + +export const EuiDataGridDisplaySelectorCustomRenderProps: FunctionComponent< + DataGridTypes.EuiDataGridDisplaySelectorCustomRenderProps +> = () => <>; + +export const ToolbarStorybookComponent: FunctionComponent< + DataGridTypes.EuiDataGridProps & // We really just want toolbarVisibility and renderCustomToolbar from here, but typescript-docgen is unhappy if we Pick<> + DataGridTypes.EuiDataGridToolBarVisibilityOptions & + DataGridTypes.EuiDataGridToolBarAdditionalControlsOptions +> = () => <>; + +export const EuiDataGridStyle: FunctionComponent< + DataGridTypes.EuiDataGridStyle +> = () => <>; + +export const EuiDataGridRowHeightsOptions: FunctionComponent< + DataGridTypes.EuiDataGridRowHeightsOptions +> = () => <>; + +export const EuiDataGridHeightWidthProps: FunctionComponent< + Pick +> = () => <>; + +export const EuiDataGridVirtualizationOptions: FunctionComponent< + DataGridTypes.EuiDataGridProps['virtualizationOptions'] +> = () => <>; + +export const EuiDataGridRefProps: FunctionComponent< + DataGridTypes.EuiDataGridRefProps +> = () => <>; + +export const EuiDataGridInMemory: FunctionComponent< + DataGridTypes.EuiDataGridInMemory +> = () => <>; + +export const EuiDataGridCustomBodyProps: FunctionComponent< + DataGridTypes.EuiDataGridCustomBodyProps +> = () => <>; diff --git a/packages/eui/src/components/datagrid/data_grid_types.ts b/packages/eui/src/components/datagrid/data_grid_types.ts index acac707bf34..2a8397ca549 100644 --- a/packages/eui/src/components/datagrid/data_grid_types.ts +++ b/packages/eui/src/components/datagrid/data_grid_types.ts @@ -366,23 +366,21 @@ export type CommonGridProps = CommonProps & /** * Allows customizing the underlying [react-window grid](https://react-window.vercel.app/#/api/VariableSizeGrid) props. */ - virtualizationOptions?: Partial< - Omit< - VariableSizeGridProps, - | 'children' - | 'itemData' - | 'height' - | 'width' - | 'rowCount' - | 'rowHeight' - | 'columnCount' - | 'columnWidth' - | 'ref' - | 'innerRef' - | 'outerRef' - | 'innerElementType' - | 'useIsScrolling' - > + virtualizationOptions?: Pick< + VariableSizeGridProps, + | 'className' + | 'style' + | 'direction' + | 'estimatedRowHeight' + | 'estimatedColumnWidth' + | 'overscanRowCount' + | 'overscanColumnCount' + | 'initialScrollTop' + | 'initialScrollLeft' + | 'onScroll' + | 'onItemsRendered' + | 'itemKey' + | 'outerElementType' >; /** * A #EuiDataGridRowHeightsOptions object that provides row heights options. @@ -944,12 +942,16 @@ export interface EuiDataGridToolBarVisibilityDisplaySelectorOptions { customRender?: EuiDataGridDisplaySelectorCustomRender; } -export type EuiDataGridDisplaySelectorCustomRender = (args: { +export type EuiDataGridDisplaySelectorCustomRenderProps = { densityControl: ReactNode; rowHeightControl: ReactNode; additionalDisplaySettings: ReactNode; resetButton: ReactNode; -}) => ReactNode; +}; + +export type EuiDataGridDisplaySelectorCustomRender = ( + args: EuiDataGridDisplaySelectorCustomRenderProps +) => ReactNode; export interface EuiDataGridToolBarVisibilityOptions { /** diff --git a/packages/website/docs/components/editors_and_syntax/markdown/_category_.yml b/packages/website/docs/components/editors_and_syntax/markdown/_category_.yml new file mode 100644 index 00000000000..20c97dde1e8 --- /dev/null +++ b/packages/website/docs/components/editors_and_syntax/markdown/_category_.yml @@ -0,0 +1,2 @@ +label: Markdown +collapsed: true diff --git a/packages/website/docs/components/editors_and_syntax/markdown_editor.mdx b/packages/website/docs/components/editors_and_syntax/markdown/markdown_editor.mdx similarity index 69% rename from packages/website/docs/components/editors_and_syntax/markdown_editor.mdx rename to packages/website/docs/components/editors_and_syntax/markdown/markdown_editor.mdx index 164280ed698..ca5bd651a4a 100644 --- a/packages/website/docs/components/editors_and_syntax/markdown_editor.mdx +++ b/packages/website/docs/components/editors_and_syntax/markdown/markdown_editor.mdx @@ -1,17 +1,17 @@ --- -slug: /editors-syntax/markdown-editor +slug: /editors-syntax/markdown/editor id: editors_syntax_markdown_editor --- # Markdown editor -**EuiMarkdownEditor** provides a markdown authoring experience for the user. The component consists of a toolbar, text area, and a drag-and-drop zone to accept files (if configured to do so). There are two modes: a textarea that keeps track of cursor position, and a rendered preview mode that is powered by **[EuiMarkdownFormat](/docs/editors-syntax/markdown-format/)**. State is maintained between the two and it is possible to pass changes from the preview area to the textarea and vice versa. +**EuiMarkdownEditor** provides a markdown authoring experience for the user. The component consists of a toolbar, text area, and a drag-and-drop zone to accept files (if configured to do so). There are two modes: a textarea that keeps track of cursor position, and a rendered preview mode that is powered by **[EuiMarkdownFormat](./format)**. State is maintained between the two and it is possible to pass changes from the preview area to the textarea and vice versa. ## Base editor -Use the base editor to produce technical content in markdown which can contain text, code, and images. Besides this default markdown content, the base editor comes with built-in plugins that let you add emojis, to-do lists, and tooltips. +Use the base editor to produce technical content in markdown which can contain text, code, and images. Besides this default markdown content, the base editor comes with several [default plugins](./plugins#default-plugins) that let you add emojis, to-do lists, and tooltips, which can be [configured](./plugins#configuring-the-default-plugins) or [removed](./plugins#unregistering-plugins) as needed. -Consider applying the `readOnly` prop to restrict editing during asynchronous submit events, like when submitting a [comment](/docs/display/comment-list). This will ensure users understand that the content cannot be changed while the comment is being submitted. +Consider applying the `readOnly` prop to restrict editing during asynchronous submit events, like when submitting a [comment](../../display/comment-list). This will ensure users understand that the content cannot be changed while the comment is being submitted. ```tsx interactive import React, { useCallback, useState } from 'react'; @@ -118,128 +118,9 @@ export default () => { ``` -## Unregistering plugins - -The **EuiMarkdownEditor** comes with several default plugins, demo'd below. If these defaults are unnecessary for your use-case or context, you can unregister these plugins by excluding them from the `parsingPlugins`, `processingPlugins` and `uiPlugins` options with a single `exclude` parameter passed to `getDefaultEuiMarkdownPlugins()`. This will ensure the syntax won't be identified or rendered, and no additional UI (like toolbar buttons or help syntax) will be displayed by the unregistered plugins. - -```tsx interactive -import React, { useState, useMemo } from 'react'; -import { - EuiMarkdownEditor, - getDefaultEuiMarkdownPlugins, - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, -} from '@elastic/eui'; - -const initialContent = ` -### tooltip - -When disabled, the button in the toolbar and the help syntax won't be displayed, and the following syntax will no longer works. - -!{tooltip[anchor text](Tooltip content)} - -### checkbox - -When disabled, a EuiCheckbox will no longer render. - -- [ ] TODO: Disable some default plugins - -### emoji - -When disabled, text will render instead of an emoji. - -:smile: - -### linkValidator - -When disabled, all links will render as links instead of as text. - -[This is a sus link](file://) - -### lineBreaks - -When disabled, these sentence will be in the same line. -When enabled, these sentences will be separated by a line break. - -Two blank lines in a row will create a new paragraph as usual. -`; - -export default () => { - const [value, setValue] = useState(initialContent); - - const [tooltip, excludeTooltips] = useState(false); - const [checkbox, excludeCheckboxes] = useState(true); - const [emoji, excludeEmojis] = useState(true); - const [linkValidator, excludeLinkValidator] = useState(true); - const [lineBreaks, excludeLineBreaks] = useState(false); - - const { parsingPlugins, processingPlugins, uiPlugins } = useMemo(() => { - const excludedPlugins = []; - if (!tooltip) excludedPlugins.push('tooltip'); - if (!checkbox) excludedPlugins.push('checkbox'); - if (!emoji) excludedPlugins.push('emoji'); - if (!linkValidator) excludedPlugins.push('linkValidator'); - if (!lineBreaks) excludedPlugins.push('lineBreaks'); - - return getDefaultEuiMarkdownPlugins({ - exclude: excludedPlugins, - }); - }, [tooltip, checkbox, emoji, linkValidator, lineBreaks]); - - return ( - <> - - - excludeTooltips(!tooltip)} - /> - excludeCheckboxes(!checkbox)} - /> - excludeEmojis(!emoji)} - /> - excludeLinkValidator(!linkValidator)} - /> - excludeLineBreaks(!lineBreaks)} - /> - - - - - - - ); -}; - -``` - ## Error handling and feedback -The `errors` prop allows you to pass an array of errors if syntax is malformed. The below example starts with an incomplete tooltip tag, showing this error message by default. These errors are meant to be ephemeral and part of the editing experience. They should not be a substitute for [form validation](/docs/forms/form-validation). +The `errors` prop allows you to pass an array of errors if syntax is malformed. The below example starts with an incomplete tooltip tag, showing this error message by default. These errors are meant to be ephemeral and part of the editing experience. They should not be a substitute for [form validation](../../forms/form-validation). ```tsx interactive import React, { useCallback, useState, useRef } from 'react'; diff --git a/packages/website/docs/components/editors_and_syntax/markdown_format.mdx b/packages/website/docs/components/editors_and_syntax/markdown/markdown_format.mdx similarity index 95% rename from packages/website/docs/components/editors_and_syntax/markdown_format.mdx rename to packages/website/docs/components/editors_and_syntax/markdown/markdown_format.mdx index 10cae00ca20..82bc07fec2c 100644 --- a/packages/website/docs/components/editors_and_syntax/markdown_format.mdx +++ b/packages/website/docs/components/editors_and_syntax/markdown/markdown_format.mdx @@ -1,11 +1,12 @@ --- -slug: /editors-syntax/markdown-format +slug: /editors-syntax/markdown/format id: editors_syntax_markdown_format +sidebar_position: 1 --- # Markdown format -**EuiMarkdownFormat** is a read-only way to render markdown-style content in a page. It is a peer component to **[EuiMarkdownEditor](/docs/editors-syntax/markdown-editor/)** and has the ability to be modified by additional [markdown plugins](/docs/editors-syntax/markdown-plugins). +**EuiMarkdownFormat** is a read-only way to render markdown-style content in a page. It is a peer component to **[EuiMarkdownEditor](./editor)** and has the ability to be modified by additional [markdown plugins](./plugins). ## Built in plugins @@ -46,7 +47,7 @@ export default () => { ## Text sizing and coloring -**EuiMarkdownFormat** uses [EuiText](/docs/display/text/) as a wrapper to handle all the CSS styling when rendering the HTML. It also gives the ability to control the text size and color with the `textSize` and `color` props, respectively. +**EuiMarkdownFormat** uses [EuiText](../../display/text) as a wrapper to handle all the CSS styling when rendering the HTML. It also gives the ability to control the text size and color with the `textSize` and `color` props, respectively. ```tsx interactive import React from 'react'; diff --git a/packages/website/docs/components/editors_and_syntax/markdown_plugins.mdx b/packages/website/docs/components/editors_and_syntax/markdown/markdown_plugins.mdx similarity index 81% rename from packages/website/docs/components/editors_and_syntax/markdown_plugins.mdx rename to packages/website/docs/components/editors_and_syntax/markdown/markdown_plugins.mdx index 5f60d0ed502..2c2f71746f9 100644 --- a/packages/website/docs/components/editors_and_syntax/markdown_plugins.mdx +++ b/packages/website/docs/components/editors_and_syntax/markdown/markdown_plugins.mdx @@ -1,11 +1,11 @@ --- -slug: /editors-syntax/markdown-plugins +slug: /editors-syntax/markdown/plugins id: editors_syntax_markdown_plugins --- # Markdown plugins -Both **[EuiMarkdownEditor](/docs/editors-syntax/markdown-editor/)** and **[EuiMarkdownFormat](/docs/editors-syntax/markdown-format/)** utilize the same underlying plugin architecture to transform string based syntax into React components. At a high level [Unified JS(external, opens in a new tab or window)](https://www.npmjs.com/package/unified) is used in combination with [Remark(external, opens in a new tab or window)](https://www.npmjs.com/package/remark-parse) to provide EUI's markdown components, which are separated into a **parsing** and **processing** layer. These two concepts are kept distinct in EUI components to provide concrete locations for your plugins to be injected, be it editing or rendering. Finally you provide **UI** to the component to handle interactions with the editor. +Both **[EuiMarkdownEditor](./editor)** and **[EuiMarkdownFormat](./format)** utilize the same underlying plugin architecture to transform string based syntax into React components. At a high level [Unified JS](https://www.npmjs.com/package/unified) is used in combination with [Remark](https://www.npmjs.com/package/remark-parse) to provide EUI's markdown components, which are separated into a **parsing** and **processing** layer. These two concepts are kept distinct in EUI components to provide concrete locations for your plugins to be injected, be it editing or rendering. Finally you provide **UI** to the component to handle interactions with the editor. In addition to running the full pipeline, **EuiMarkdownEditor** uses just the parsing configuration to determine the input's validity, provide messages back to the application, and allow the toolbar buttons to interact with existing markdown tags. @@ -35,13 +35,13 @@ The last set of plugin configuration - `uiPlugins` - allows toolbar buttons to b These plugin definitions can be obtained by calling `getDefaultEuiMarkdownParsingPlugins`, `getDefaultEuiMarkdownProcessingPlugins`, and `getDefaultEuiMarkdownUiPlugins` respectively. -## Configuring the default plugins +### Configuring the default plugins The above plugin utils, as well as `getDefaultEuiMarkdownPlugins`, accept an optional configuration object of: -* `exclude`: an array of default plugins to [unregister](/docs/editors-syntax/markdown-editor#unregistering-plugins) +* `exclude`: an array of default plugins to [unregister](#unregistering-plugins) * `parsingConfig`: allows overriding the configuration of any default parsing plugin -* `processingConfig`: currently only accepts a `linkProps` key, which accepts any prop that [EuiLink](/docs/navigation/link) accepts +* `processingConfig`: currently only accepts a `linkProps` key, which accepts any prop that [EuiLink](../../navigation/link) accepts The below example has the `emoji` plugin excluded, and custom configuration on the link validator parsing plugin and link processing plugin. See the **Props** table for all plugin config options. @@ -79,14 +79,131 @@ export default () => { ); }; +``` + +### Unregistering plugins + +**EuiMarkdownEditor** comes with several default plugins, demo'd below. If these defaults are unnecessary for your use-case or context, you can unregister these plugins with a single `exclude` parameter passed to `getDefaultEuiMarkdownPlugins()`. This will ensure the syntax won't be identified or rendered, and no additional UI (like toolbar buttons or help syntax) will be displayed by the unregistered plugins. + +```tsx interactive +import React, { useState, useMemo } from 'react'; +import { + EuiMarkdownEditor, + getDefaultEuiMarkdownPlugins, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, +} from '@elastic/eui'; + +const initialContent = ` +### tooltip + +When disabled, the button in the toolbar and the help syntax won't be displayed, and the following syntax will no longer works. + +!{tooltip[anchor text](Tooltip content)} + +### checkbox + +When disabled, a EuiCheckbox will no longer render. + +- [ ] TODO: Disable some default plugins + +### emoji + +When disabled, text will render instead of an emoji. + +:smile: + +### linkValidator + +When disabled, all links will render as links instead of as text. +[This is a sus link](file://) + +### lineBreaks + +When disabled, these sentence will be in the same line. +When enabled, these sentences will be separated by a line break. + +Two blank lines in a row will create a new paragraph as usual. +`; + +export default () => { + const [value, setValue] = useState(initialContent); + + const [tooltip, excludeTooltips] = useState(false); + const [checkbox, excludeCheckboxes] = useState(true); + const [emoji, excludeEmojis] = useState(true); + const [linkValidator, excludeLinkValidator] = useState(true); + const [lineBreaks, excludeLineBreaks] = useState(false); + + const { parsingPlugins, processingPlugins, uiPlugins } = useMemo(() => { + const excludedPlugins = []; + if (!tooltip) excludedPlugins.push('tooltip'); + if (!checkbox) excludedPlugins.push('checkbox'); + if (!emoji) excludedPlugins.push('emoji'); + if (!linkValidator) excludedPlugins.push('linkValidator'); + if (!lineBreaks) excludedPlugins.push('lineBreaks'); + + return getDefaultEuiMarkdownPlugins({ + exclude: excludedPlugins, + }); + }, [tooltip, checkbox, emoji, linkValidator, lineBreaks]); + + return ( + <> + + + excludeTooltips(!tooltip)} + /> + excludeCheckboxes(!checkbox)} + /> + excludeEmojis(!emoji)} + /> + excludeLinkValidator(!linkValidator)} + /> + excludeLineBreaks(!lineBreaks)} + /> + + + + + + + ); +}; ``` ## Plugin development An **EuiMarkdown plugin** is comprised of three major pieces, which are passed searpately as props. -``` +```tsx ` elements, and do not have a locate method. They can consume as much input text as desired, across multiple lines. -``` +```ts // example plugin parser function EmojiMarkdownParser() { const Parser = this.Parser; @@ -288,7 +405,7 @@ parsingList.push(EmojiMarkdownParser); After parsing the input into an AST, the nodes need to be transformed into React elements. This is performed by a list of processors, the default set converts remark AST into rehype and then into React. Plugins need to define themselves within this transformation process, identifying with the same type its parser uses in its `eat` call. -``` +```tsx // example plugin processor // receives the configuration from the parser and renders @@ -301,7 +418,7 @@ const processingList = getDefaultEuiMarkdownProcessingPlugins(); processingList[1][1].components.emojiPlugin = EmojiMarkdownRenderer; ``` -## Putting it all together: a simple chart plugin +### Putting it all together: a simple chart plugin The below example takes the concepts from above to construct a simple chart embed that is initiated from a new button in the editor toolbar. diff --git a/packages/website/docs/components/tabular_content/data_grid/_category_.yml b/packages/website/docs/components/tabular_content/data_grid/_category_.yml new file mode 100644 index 00000000000..a930daac9ab --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/_category_.yml @@ -0,0 +1,3 @@ +label: 'Data grid' +collapsed: true +position: 2 diff --git a/packages/website/docs/components/tabular_content/data_grid/_grid_configuration_wrapper.tsx b/packages/website/docs/components/tabular_content/data_grid/_grid_configuration_wrapper.tsx new file mode 100644 index 00000000000..b6e479a948e --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/_grid_configuration_wrapper.tsx @@ -0,0 +1,108 @@ +import { FunctionComponent, HTMLAttributes, ReactElement } from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import { + EuiSplitPanel, + EuiButtonGroup, + EuiButtonGroupProps, + EuiFormRow, + EuiCodeBlock, + useEuiTheme, + CommonProps, +} from '@elastic/eui'; + +type Configuration = Pick< + EuiButtonGroupProps, + 'options' | 'idSelected' | 'onChange' +> & { label: string }; + +type Props = { + children: ReactElement; + configuration: Array; + snippet: string; + wrapperProps?: Omit, 'color'> & CommonProps; +}; + +export const ConfigurationDemoWithSnippet: FunctionComponent = ({ + children, + configuration, + snippet, + wrapperProps, +}) => { + const { euiTheme } = useEuiTheme(); + return ( + + +
+ {configuration.map( + ({ label, options, idSelected, onChange, nestedConfig }) => ( + <> + + + + {nestedConfig && + nestedConfig.map( + ({ label, options, idSelected, onChange }) => ( + + + + ) + )} + + ) + )} +
+
+ {() => children} +
+
+ {snippet && ( + <> + + + {snippet} + + + + )} +
+ ); +}; + +export const objectConfigToSnippet = (config: object) => { + let snippet = JSON.stringify(config, null, 2); + return snippet.replace(/^[ ]{2,}"[^:\n\r]+(? + match.replace(/"/g, '') + ); +}; diff --git a/packages/website/docs/components/tabular_content/data_grid/_prop_snippet_table.tsx b/packages/website/docs/components/tabular_content/data_grid/_prop_snippet_table.tsx new file mode 100644 index 00000000000..77c87d6b3d1 --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/_prop_snippet_table.tsx @@ -0,0 +1,91 @@ +import React, { useMemo, FC, ReactNode } from 'react'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiMarkdownFormat, + getDefaultEuiMarkdownPlugins, + EuiCodeBlock, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; +import { ProcessedComponent } from '@elastic/eui-docgen'; + +type DataGridPropSnippetTableProps = { + propSnippetMap: Record; + linksMap?: Record; + docgen: ProcessedComponent; +}; + +const { processingPlugins, parsingPlugins } = getDefaultEuiMarkdownPlugins({ + exclude: ['lineBreaks'], +}); + +const columns: Array> = [ + { + name: 'Prop', + render: ({ propName, propDescription }) => ( + <> + {propName} + {propDescription && ( + <> + + + {propDescription} + + + )} + + ), + textOnly: true, + valign: 'top', + }, + { + field: 'snippet', + name: 'Sample snippet', + render: (snippet: string | ReactNode) => + typeof snippet === 'string' ? ( + + {snippet} + + ) : ( + snippet + ), + valign: 'top', + }, +]; + +export const DataGridPropSnippetTable: FC = ({ + propSnippetMap, + linksMap, + docgen, +}) => { + const items = useMemo( + () => + Object.entries(propSnippetMap).map(([prop, snippet]) => { + const propLink = linksMap?.[prop]; + const propName = propLink ? ( + + {prop} + + ) : ( + {prop} + ); + const propDescription = docgen.props[prop]?.description; + + return { propName, propDescription, snippet }; + }), + [propSnippetMap] + ); + + return ; +}; diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/_category_.yml b/packages/website/docs/components/tabular_content/data_grid/advanced/_category_.yml new file mode 100644 index 00000000000..438f6ebf35c --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/_category_.yml @@ -0,0 +1,2 @@ +label: 'Advanced use cases' +position: 7 diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body.mdx b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body.mdx new file mode 100644 index 00000000000..0d9a7ce76ee --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body.mdx @@ -0,0 +1,23 @@ +--- +slug: /tabular-content/data-grid/advanced/custom-grid-body +id: tabular_content_data_grid_advanced_custom_grid_body +sidebar_position: 2 +--- + +# Custom body rendering + +For **extremely** advanced use cases, the `renderCustomGridBody` prop may be used to take complete control over rendering the grid body. This may be useful for scenarios where the default [virtualized](/docs/tabular-content/data-grid#virtualization) rendering is not desired, or where custom row layouts (e.g., the conditional row details cell below) are required. + +:::warning +This prop is meant to be an **escape hatch**, and should only be used if you know exactly what you are doing. Once a custom renderer is used, you are in charge of ensuring the grid has all the correct semantic and aria labels required by the [data grid spec](https://www.w3.org/WAI/ARIA/apg/patterns/grid), and that keyboard focus and navigation still work in an accessible manner. +::: + +import CustomGridBodyDemo from './custom_grid_body/demo'; + + + +## Props + +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; + + diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/data_columns_cells.tsx b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/data_columns_cells.tsx new file mode 100644 index 00000000000..71a89d8ee3e --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/data_columns_cells.tsx @@ -0,0 +1,184 @@ +import React, { useEffect, memo } from 'react'; +import { + EuiDataGridProps, + EuiDataGridColumnCellActionProps, + EuiScreenReaderOnly, + EuiCheckbox, + EuiCallOut, + EuiButton, + EuiButtonIcon, +} from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +/** + * Mock data + */ +export const raw_data: Array<{ [key: string]: string }> = []; +for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()}`, + email: faker.internet.email(), + location: `${faker.location.city()}, ${faker.location.country()}`, + date: `${faker.date.past()}`, + amount: faker.commerce.price({ min: 1, max: 1000, dec: 2, symbol: '$' }), + feesOwed: faker.datatype.boolean() ? 'true' : '', + }); +} +const footer_data: { [key: string]: string } = { + amount: `Total: ${raw_data + .reduce((acc, { amount }) => acc + Number(amount.split('$')[1]), 0) + .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`, +}; + +/** + * Columns + */ +export const columns = [ + { + id: 'name', + displayAsText: 'Name', + cellActions: [ + ({ Component }: EuiDataGridColumnCellActionProps) => ( + alert('action')} + iconType="faceHappy" + aria-label="Some action" + > + Some action + + ), + ], + }, + { + id: 'email', + displayAsText: 'Email address', + initialWidth: 130, + }, + { + id: 'location', + displayAsText: 'Location', + }, + { + id: 'date', + displayAsText: 'Date', + }, + { + id: 'amount', + displayAsText: 'Amount', + }, +]; + +/** + * Cell component + */ +export const RenderCellValue: EuiDataGridProps['renderCellValue'] = ({ + rowIndex, + columnId, +}) => raw_data[rowIndex][columnId]; + +/** + * Row details component + */ +// eslint-disable-next-line local/forward-ref +const RenderRowDetails: EuiDataGridProps['renderCellValue'] = memo( + ({ setCellProps, rowIndex }) => { + setCellProps({ style: { width: '100%', height: 'auto' } }); + + // Mock data + const firstName = raw_data[rowIndex].name.split(', ')[1]; + const hasFees = !!raw_data[rowIndex].feesOwed; + + return ( + + {hasFees && ( + + Send an email reminder + + )} + + ); + } +); + +/** + * Control columns + */ +export const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = + [ + { + id: 'selection', + width: 32, + headerCellRender: () => ( + {}} + /> + ), + rowCellRender: ({ rowIndex }) => ( + {}} + /> + ), + }, + ]; +export const trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = + [ + { + id: 'actions', + width: 40, + headerCellRender: () => ( + + Actions + + ), + rowCellRender: () => ( + + ), + }, + // The custom row details is actually a trailing control column cell with + // a hidden header. This is important for accessibility and markup reasons + // @see https://fuschia-stretch.glitch.me/ for more + { + id: 'row-details', + + // The header cell should be visually hidden, but available to screen readers + width: 0, + headerCellRender: () => <>Row details, + headerCellProps: { className: 'euiScreenReaderOnly' }, + + // The footer cell can be hidden to both visual & SR users, as it does not contain meaningful information + footerCellProps: { style: { display: 'none' } }, + + // When rendering this custom cell, we'll want to override + // the automatic width/heights calculated by EuiDataGrid + rowCellRender: RenderRowDetails, + }, + ]; + +/** + * Footer cell component + */ +export const RenderFooterCellValue: EuiDataGridProps['renderFooterCellValue'] = + ({ columnId, setCellProps }) => { + const value = footer_data[columnId]; + + useEffect(() => { + // Turn off the cell expansion button if the footer cell is empty + if (!value) setCellProps({ isExpandable: false }); + }, [value, setCellProps, columnId]); + + return value || null; + }; diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/data_grid.tsx b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/data_grid.tsx new file mode 100644 index 00000000000..1cb89aa1f4b --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/data_grid.tsx @@ -0,0 +1,68 @@ +import React, { useCallback, useState } from 'react'; +import { + EuiDataGrid, + EuiDataGridProps, + EuiDataGridPaginationProps, + EuiDataGridSorting, + EuiDataGridColumnSortingConfig, +} from '@elastic/eui'; + +import { + raw_data, + columns, + leadingControlColumns, + trailingControlColumns, + RenderCellValue, + RenderFooterCellValue, +} from './data_columns_cells'; + +export default (props: Partial>) => { + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }) => id) + ); + + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const onChangePage = useCallback( + (pageIndex) => { + setPagination((pagination) => ({ ...pagination, pageIndex })); + }, + [] + ); + const onChangePageSize = useCallback< + EuiDataGridPaginationProps['onChangeItemsPerPage'] + >((pageSize) => { + setPagination((pagination) => ({ ...pagination, pageSize })); + }, []); + + // Sorting + const [sortingColumns, setSortingColumns] = useState< + EuiDataGridColumnSortingConfig[] + >([]); + const onSort = useCallback((sortingColumns) => { + setSortingColumns(sortingColumns); + }, []); + + return ( + + ); +}; diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/demo.tsx b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/demo.tsx new file mode 100644 index 00000000000..063b245c269 --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/demo.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +// @ts-ignore - missing types? +import BrowserOnly from '@docusaurus/BrowserOnly'; +import { + EuiSplitPanel, + EuiFlexGroup, + EuiSwitch, + EuiTabbedContent, + EuiCodeBlock, +} from '@elastic/eui'; + +import virtualizedSource from '!!raw-loader!./virtualized_body'; +import VirtualizedBody from './virtualized_body'; + +import unvirtualizedSource from '!!raw-loader!./unvirtualized_body'; +import UnvirtualizedBody from './unvirtualized_body'; + +import dataSource from '!!raw-loader!./data_columns_cells'; +import dataGridSource from '!!raw-loader!./data_grid'; + +export default () => { + const [virtualized, setVirtualized] = useState(false); + const [autoHeight, setAutoHeight] = useState(true); + + return ( + + + + setVirtualized(!virtualized)} + /> + setAutoHeight(!autoHeight)} + /> + + + + + {() => + virtualized ? ( + + ) : ( + + ) + } + + + + + {virtualized ? virtualizedSource : unvirtualizedSource} + + ), + }, + { + id: 'cell', + name: 'Cell components', + content: {dataSource}, + }, + { + id: 'datagrid', + name: 'Data grid component', + content: ( + {dataGridSource} + ), + }, + ]} + /> + + + ); +}; + +const CodeSnippetWrapper = ({ children }) => ( + + {children} + +); diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/unvirtualized_body.tsx b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/unvirtualized_body.tsx new file mode 100644 index 00000000000..1181972b61c --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/unvirtualized_body.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useRef, memo } from 'react'; +import { css } from '@emotion/react'; +import { + EuiDataGridCustomBodyProps, + useEuiTheme, + logicalCSS, +} from '@elastic/eui'; + +import { raw_data } from './data_columns_cells'; +import CustomEuiDataGrid from './data_grid'; + +export const CustomUnvirtualizedGridBody = memo( + ({ + Cell, + visibleColumns, + visibleRowData, + setCustomGridBodyProps, + headerRow, + footerRow, + }: EuiDataGridCustomBodyProps) => { + // Ensure we're displaying correctly-paginated rows + const visibleRows = raw_data.slice( + visibleRowData.startRow, + visibleRowData.endRow + ); + + // Add styling needed for custom grid body rows + const { euiTheme } = useEuiTheme(); + const styles = { + row: css` + ${logicalCSS('width', 'fit-content')}; + ${logicalCSS('border-bottom', euiTheme.border.thin)}; + background-color: ${euiTheme.colors.emptyShade}; + `, + rowCellsWrapper: css` + display: flex; + `, + rowDetailsWrapper: css` + /* Extra specificity needed to override EuiDataGrid's default styles */ + && .euiDataGridRowCell__content { + display: block; + padding: 0; + } + `, + }; + + // Set custom props onto the grid body wrapper + const bodyRef = useRef(null); + useEffect(() => { + setCustomGridBodyProps({ + ref: bodyRef, + onScroll: () => console.debug('scrollTop:', bodyRef.current?.scrollTop), + }); + }, [setCustomGridBodyProps]); + + return ( + <> + {headerRow} + {visibleRows.map((row, rowIndex) => ( +
+
+ {visibleColumns.map((column, colIndex) => { + // Skip the row details cell - we'll render it manually outside of the flex wrapper + if (column.id !== 'row-details') { + return ( + + ); + } + })} +
+
+ +
+
+ ))} + {footerRow} + + ); + } +); +CustomUnvirtualizedGridBody.displayName = 'CustomUnvirtualizedGridBody'; + +export default ({ autoHeight }: { autoHeight: boolean }) => ( + +); diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/virtualized_body.tsx b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/virtualized_body.tsx new file mode 100644 index 00000000000..132457b30a4 --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/custom_grid_body/virtualized_body.tsx @@ -0,0 +1,215 @@ +import React, { + useEffect, + useCallback, + useRef, + useMemo, + memo, + forwardRef, + PropsWithChildren, + CSSProperties, +} from 'react'; +import { VariableSizeList } from 'react-window'; +import { css } from '@emotion/react'; +import { + EuiDataGridCustomBodyProps, + EuiAutoSizer, + useEuiTheme, + logicalCSS, +} from '@elastic/eui'; + +import { raw_data } from './data_columns_cells'; +import CustomEuiDataGrid from './data_grid'; + +type CustomTimelineDataGridSingleRowProps = { + rowIndex: number; + setRowHeight: (index: number, height: number) => void; + maxWidth: number | undefined; +} & Pick; + +const Row = memo( + ({ + rowIndex, + visibleColumns, + setRowHeight, + Cell, + }: CustomTimelineDataGridSingleRowProps) => { + const { euiTheme } = useEuiTheme(); + const styles = { + row: css` + ${logicalCSS('width', 'fit-content')}; + ${logicalCSS('border-bottom', euiTheme.border.thin)}; + background-color: ${euiTheme.colors.emptyShade}; + `, + rowCellsWrapper: css` + display: flex; + `, + rowDetailsWrapper: css` + /* Extra specificity needed to override EuiDataGrid's default styles */ + && .euiDataGridRowCell__content { + display: block; + padding: 0; + } + `, + }; + const rowRef = useRef(null); + + useEffect(() => { + if (rowRef.current) { + setRowHeight(rowIndex, rowRef.current.offsetHeight); + } + }, [Cell, rowIndex, setRowHeight]); + + return ( +
+
+ {visibleColumns.map((column, colIndex) => { + // Skip the row details cell - we'll render it manually outside of the flex wrapper + if (column.id !== 'row-details') { + return ( + + ); + } + })} +
+
+ +
+
+ ); + } +); +Row.displayName = 'Row'; + +export const CustomVirtualizedGridBody = memo( + ({ + Cell, + visibleColumns, + visibleRowData, + setCustomGridBodyProps, + headerRow, + footerRow, + }: EuiDataGridCustomBodyProps) => { + // Ensure we're displaying correctly-paginated rows + const visibleRows = raw_data.slice( + visibleRowData.startRow, + visibleRowData.endRow + ); + + // Set custom props onto the grid body wrapper + const bodyRef = useRef(null); + useEffect(() => { + setCustomGridBodyProps({ + ref: bodyRef, + onScroll: () => console.debug('scrollTop:', bodyRef.current?.scrollTop), + }); + }, [setCustomGridBodyProps]); + + const listRef = useRef>(null); + const rowHeights = useRef([]); + + const setRowHeight = useCallback((index: number, height: number) => { + if (rowHeights.current[index] === height) return; + listRef.current?.resetAfterIndex(index); + + rowHeights.current[index] = height; + }, []); + + const getRowHeight = useCallback((index: number) => { + return rowHeights.current[index] ?? 100; + }, []); + + const outer = useMemo( + () => + forwardRef>( + ({ children, ...rest }, ref) => { + return ( +
+ {headerRow} + {children} + {footerRow} +
+ ); + } + ), + [headerRow, footerRow] + ); + + const inner = useMemo( + () => + forwardRef>( + ({ children, style, ...rest }, ref) => { + return ( +
+ {children} +
+ ); + } + ), + [] + ); + + return ( + + {({ height }: { height: number }) => { + return ( + `${height}-${visibleRows.length}-${index}`} + outerElementType={outer} + innerElementType={inner} + overscanCount={0} + layout="vertical" + > + {({ index: rowIndex, style }) => { + return ( +
+ +
+ ); + }} +
+ ); + }} +
+ ); + } +); +CustomVirtualizedGridBody.displayName = 'CustomVirtualizedGridBody'; + +export default () => ( + +); diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/in_memory.mdx b/packages/website/docs/components/tabular_content/data_grid/advanced/in_memory.mdx new file mode 100644 index 00000000000..7443245be30 --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/in_memory.mdx @@ -0,0 +1,566 @@ +--- +slug: /tabular-content/data-grid/advanced/in-memory +id: tabular_content_data_grid_advanced_in_memory +--- + +# Data grid in-memory + +:::note What is the difference in the examples? + +These examples show the same grid built with the four available `inMemory` settings. While they may look the same, look at the source to see how they require different levels of data management in regards to sorting and pagination. + +::: + +The grid has levels of **in-memory** settings that can be set. It is in the consuming application's best interest to put as much of the data grid in memory as performance allows. Try to use the highest level `inMemory="sorting"` whenever possible. The following values are available. + +* **undefined (default)**: When not in use the grid will not autodetect schemas. The sorting and pagination is the responsibility of the consuming application. +* **enhancements**: Provides no in-memory operations. If set, the grid will try to autodetect schemas only based on the content currently available (the current page of data). +* **pagination**: Schema detection works as above and pagination is performed in-memory. The pagination callbacks are still triggered on user interactions, but the row updates are performed by the grid. +* **sorting (suggested)**: Schema detection and pagination are performed as above, and sorting is applied in-memory too. The onSort callback is still called and the application must own the column sort state, but data sorting is done by the grid based on the defined and/or detected schemas. + +When enabled, **in-memory** renders cell data off-screen and uses those values to detect schemas and perform sorting. This detaches the user experience from the raw data; the data grid never has access to the backing data, only what is returned by `renderCellValue`. + +### When in-memory is not used + +When `inMemory` is not in use the grid will not autodetect schemas. In the below example only the `amount` column has a schema because it is manually set. Sorting and pagination data management is the responsibility of the consuming application. Column sorting in particular is going to be imprecise because there is no backend service to call, and data grid instead defaults to naively applying JavaScript's default array sort which doesn't work well with numeric data and doesn't sort React elements such as the links. This is a good example of what happens when you **don't** utilize schemas for complex data. + +```tsx interactive +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiDataGrid, EuiLink } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const columns = [ + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'location', + }, + { + id: 'account', + }, + { + id: 'date', + }, + { + id: 'amount', + schema: 'currency', + }, + { + id: 'phone', + }, + { + id: 'version', + }, +]; + +const raw_data = []; + +for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: {faker.internet.email()}, + location: ( + <> + {`${faker.location.city()}, `} + {faker.location.country()} + + ), + date: `${faker.date.past()}`, + account: faker.finance.accountNumber(), + amount: faker.commerce.price(), + phone: faker.phone.number(), + version: faker.system.semver(), + }); +} + +export default () => { + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); + + // Sort data + let data = useMemo(() => { + return [...raw_data].sort((a, b) => { + for (let i = 0; i < sortingColumns.length; i++) { + const column = sortingColumns[i]; + const aValue = a[column.id]; + const bValue = b[column.id]; + + if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; + if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + } + + return 0; + }); + }, [sortingColumns]); + + // Pagination + data = useMemo(() => { + const rowStart = pagination.pageIndex * pagination.pageSize; + const rowEnd = Math.min(rowStart + pagination.pageSize, data.length); + return data.slice(rowStart, rowEnd); + }, [data, pagination]); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }) => { + let adjustedRowIndex = rowIndex; + + // If we are doing the pagination (instead of leaving that to the grid) + // then the row index must be adjusted as `data` has already been pruned to the page size + adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + return data.hasOwnProperty(adjustedRowIndex) + ? data[adjustedRowIndex][columnId] + : null; + }; + }, [data, pagination.pageIndex, pagination.pageSize]); + + return ( + + ); +}; +``` + +### Enhancements only in-memory + +With `inMemory={{ level: 'enhancements' }}` the grid will now autodetect schemas based on the content it has available on the currently viewed page. Notice that the field list under Sort fields has detected the type of data each column contains. + +```tsx interactive +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiDataGrid, EuiLink } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const columns = [ + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'location', + }, + { + id: 'account', + }, + { + id: 'date', + }, + { + id: 'amount', + }, + { + id: 'phone', + }, + { + id: 'version', + }, +]; + +const raw_data = []; + +for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: {faker.internet.email()}, + location: ( + <> + {`${faker.location.city()}, `} + {faker.location.country()} + + ), + date: `${faker.date.past()}`, + account: faker.finance.accountNumber(), + amount: faker.commerce.price(), + phone: faker.phone.number(), + version: faker.system.semver(), + }); +} + +export default () => { + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); + + // Sort data + let data = useMemo(() => { + // the grid itself is responsible for sorting if inMemory is `sorting` + + return [...raw_data].sort((a, b) => { + for (let i = 0; i < sortingColumns.length; i++) { + const column = sortingColumns[i]; + const aValue = a[column.id]; + const bValue = b[column.id]; + + if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; + if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + } + + return 0; + }); + }, [sortingColumns]); + + // Pagination + data = useMemo(() => { + const rowStart = pagination.pageIndex * pagination.pageSize; + const rowEnd = Math.min(rowStart + pagination.pageSize, data.length); + return data.slice(rowStart, rowEnd); + }, [data, pagination]); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }) => { + // Because inMemory is not set for pagination, we need to manage it + // The row index must be adjusted as `data` has already been pruned to the page size + const adjustedRowIndex = + rowIndex - pagination.pageIndex * pagination.pageSize; + + return data.hasOwnProperty(adjustedRowIndex) + ? data[adjustedRowIndex][columnId] + : null; + }; + }, [data, pagination.pageIndex, pagination.pageSize]); + + return ( +
+ +
+ ); +}; + +``` + +### Pagination only in-memory + +With `inMemory={{ level: 'pagination' }}` the grid will now take care of managing the data cleanup for pagination. Like before it will autodetect schemas when possible. + +```tsx interactive +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiDataGrid, EuiLink } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const columns = [ + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'location', + }, + { + id: 'account', + }, + { + id: 'date', + }, + { + id: 'amount', + }, + { + id: 'phone', + }, + { + id: 'version', + }, +]; + +const raw_data = []; + +for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: {faker.internet.email()}, + location: ( + <> + {`${faker.location.city()}, `} + {faker.location.country()} + + ), + date: `${faker.date.past()}`, + account: faker.finance.accountNumber(), + amount: faker.commerce.price(), + phone: faker.phone.number(), + version: faker.system.semver(), + }); +} + +export default () => { + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); + + // Because inMemory's level is set to `pagination` we still need to sort the data, but no longer need to chunk it for pagination + const data = useMemo(() => { + return [...raw_data].sort((a, b) => { + for (let i = 0; i < sortingColumns.length; i++) { + const column = sortingColumns[i]; + const aValue = a[column.id]; + const bValue = b[column.id]; + + if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; + if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + } + + return 0; + }); + }, [sortingColumns]); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }) => { + return data.hasOwnProperty(rowIndex) ? data[rowIndex][columnId] : null; + }; + }, [data]); + + return ( + + ); +}; + +``` + +### Sorting and pagination in-memory + +With `inMemory={{ level: 'sorting' }}` the grid will now take care of managing the data cleanup for sorting as well as pagination. Like before it will autodetect schemas when possible. + +```tsx interactive +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiDataGrid, EuiLink } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const columns = [ + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'location', + }, + { + id: 'account', + }, + { + id: 'date', + }, + { + id: 'amount', + }, + { + id: 'phone', + }, + { + id: 'version', + }, +]; + +const raw_data = []; + +for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: {faker.internet.email()}, + location: ( + <> + {`${faker.location.city()}, `} + {faker.location.country()} + + ), + date: `${faker.date.past()}`, + account: faker.finance.accountNumber(), + amount: faker.commerce.price(), + phone: faker.phone.number(), + version: faker.system.semver(), + }); +} + +export default () => { + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }) => { + return raw_data.hasOwnProperty(rowIndex) + ? raw_data[rowIndex][columnId] + : null; + }; + }, []); + + return ( + + ); +}; +``` + +## Props + +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; + + + + diff --git a/packages/website/docs/components/tabular_content/data_grid/advanced/ref.mdx b/packages/website/docs/components/tabular_content/data_grid/advanced/ref.mdx new file mode 100644 index 00000000000..15859046ade --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/advanced/ref.mdx @@ -0,0 +1,311 @@ +--- +slug: /tabular-content/data-grid/advanced/ref +id: tabular_content_data_grid_advanced_ref +sidebar_position: 1 +--- + +# Ref methods + +For advanced use cases, and particularly for data grids that manage associated modals/flyouts and need to manually control their grid cell popovers & focus states, we expose certain internal methods via the `ref` prop of **EuiDataGrid**. These methods are: + +* `setIsFullScreen(isFullScreen)` - controls the fullscreen state of the data grid. Accepts a true/false boolean flag. +* `setFocusedCell({ rowIndex, colIndex })` - focuses the specified cell in the grid. + * Using this method is an **accessibility requirement** if your data grid toggles a modal or flyout. Your modal or flyout should restore focus into the grid on close to prevent keyboard or screen reader users from being stranded. +* `openCellPopover({ rowIndex, colIndex })` - opens the specified cell's popover contents. +* `closeCellPopover()` - closes any currently open cell popover. + +:::note Handling cell location + +When using `setFocusedCell` or `openCellPopover`, keep in mind: + +* `colIndex` is affected by the user reordering or hiding columns. +* If the passed cell indices are outside the data grid's total row count or visible column count, an error will be thrown. +* If the data grid is paginated or sorted, the grid will handle automatically finding specified row index's correct location for you. + +::: + +### react-window methods + +`EuiDataGrid` also exposes several underlying methods from [react-window's `VariableSizeGrid` imperative API](https://react-window.vercel.app/#/api/VariableSizeGrid) via its `ref`: + +* `scrollTo({ scrollLeft: number, scrollTop: number })` - scrolls the grid to the specified horizontal and vertical pixel offsets. +* `scrollToItem({ align: string = "auto", columnIndex?: number, rowIndex?: number })` - scrolls the grid to the specified row and columns indices + +:::note react-window vs. EUI + +Unlike EUI's ref APIs, `rowIndex` here refers to the **visible** `rowIndex` when passed to a method of a native `react-window` API. + +For example: `scrollToItem({ rowIndex: 50, columnIndex: 0 })` will always scroll to 51st visible row on the currently visible page, regardless of the content in the cell. In contrast, `setFocusedCell({ rowIndex: 50, colIndex: 0 })` will scroll to the 51st row in your data, which may not be the 51st visible row in the grid if it is paginated or sorted. + +::: + +The below example shows how to use the internal APIs for a data grid that opens a modal via cell actions, that scroll to specific cells, and that can be put into full-screen mode. + + + ```tsx interactive + import React, { useCallback, useMemo, useState, useRef } from 'react'; + import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiFieldNumber, + EuiButton, + EuiDataGrid, + EuiDataGridRefProps, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + EuiDataGridColumnCellAction, + EuiDataGridColumnSortingConfig, + EuiDataGridPaginationProps, + EuiDataGridSorting, + RenderCellValue, + } from '@elastic/eui'; + import { faker } from '@faker-js/faker'; + + const raw_data: Array<{ [key: string]: string }> = []; + for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()}`, + email: faker.internet.email(), + location: `${faker.location.city()}, ${faker.location.country()}`, + account: faker.finance.accountNumber(), + date: `${faker.date.past()}`, + }); + } + + const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => + raw_data[rowIndex][columnId]; + + export default () => { + const dataGridRef = useRef(null); + + // Modal + const [isModalVisible, setIsModalVisible] = useState(false); + const [lastFocusedCell, setLastFocusedCell] = useState({ + rowIndex: 0, + colIndex: 0, + }); + + const closeModal = useCallback(() => { + setIsModalVisible(false); + dataGridRef.current!.setFocusedCell(lastFocusedCell); // Set the data grid focus back to the cell that opened the modal + }, [lastFocusedCell]); + + const showModal = useCallback( + ({ rowIndex, colIndex }: { rowIndex: number; colIndex: number }) => { + setIsModalVisible(true); + dataGridRef.current!.closeCellPopover(); // Close any open cell popovers + setLastFocusedCell({ rowIndex, colIndex }); // Store the cell that opened this modal + }, + [] + ); + + const openModalAction = useCallback( + ({ Component, rowIndex, colIndex }) => { + return ( + showModal({ rowIndex, colIndex })} + iconType="faceHappy" + aria-label="Open modal" + > + Open modal + + ); + }, + [showModal] + ); + + // Columns + const columns = useMemo( + () => [ + { + id: 'name', + displayAsText: 'Name', + cellActions: [openModalAction], + }, + { + id: 'email', + displayAsText: 'Email address', + initialWidth: 130, + cellActions: [openModalAction], + }, + { + id: 'location', + displayAsText: 'Location', + cellActions: [openModalAction], + }, + { + id: 'account', + displayAsText: 'Account', + cellActions: [openModalAction], + }, + { + id: 'date', + displayAsText: 'Date', + cellActions: [openModalAction], + }, + ], + [openModalAction] + ); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }) => id) + ); + + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const onChangePage = useCallback( + (pageIndex) => { + setPagination((pagination) => ({ ...pagination, pageIndex })); + }, + [] + ); + const onChangePageSize = useCallback< + EuiDataGridPaginationProps['onChangeItemsPerPage'] + >((pageSize) => { + setPagination((pagination) => ({ ...pagination, pageSize })); + }, []); + + // Sorting + const [sortingColumns, setSortingColumns] = useState< + EuiDataGridColumnSortingConfig[] + >([]); + const onSort = useCallback((sortingColumns) => { + setSortingColumns(sortingColumns); + }, []); + + // Manual cell focus + const [rowIndexAction, setRowIndexAction] = useState(0); + const [colIndexAction, setColIndexAction] = useState(0); + + return ( + <> + + + + setRowIndexAction(Number(e.target.value))} + compressed + /> + + + + + setColIndexAction(Number(e.target.value))} + compressed + /> + + + + + dataGridRef.current!.setFocusedCell({ + rowIndex: rowIndexAction, + colIndex: colIndexAction, + }) + } + > + Set cell focus + + + + + dataGridRef.current!.scrollToItem?.({ + rowIndex: rowIndexAction, + columnIndex: colIndexAction, + align: 'center', + }) + } + > + Scroll to cell + + + + + dataGridRef.current!.openCellPopover({ + rowIndex: rowIndexAction, + colIndex: colIndexAction, + }) + } + > + Open cell popover + + + + dataGridRef.current!.setIsFullScreen(true)} + > + Set grid to fullscreen + + + + + + + {isModalVisible && ( + + + Example modal + + + + +

+ When closed, this modal should re-focus into the cell that + toggled it. +

+
+
+ + + + Close + + +
+ )} + + ); + }; + ``` +
+ +## Props + +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; + + diff --git a/packages/website/docs/components/tabular_content/data_grid/data_grid.mdx b/packages/website/docs/components/tabular_content/data_grid/data_grid.mdx new file mode 100644 index 00000000000..9ed6ece708e --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid.mdx @@ -0,0 +1,457 @@ +--- +slug: /tabular-content/data-grid +id: tabular_content_data_grid +--- + +# Data grid + +**EuiDataGrid** is for displaying large amounts of tabular data. It is a better choice over [EUI tables](../tables/) when there are many columns, the data in those columns is fairly uniform, and when schemas and sorting are important for comparison. Although it is similar to traditional spreedsheet software, EuiDataGrid's current strengths are in rendering rather than creating content. + +## Core concepts + +* The grid allows you to optionally define an [in memory level](./advanced#data-grid-in-memory) to have the grid automatically handle updating your columns. Depending upon the level chosen, you may need to manage the content order separately from the grid. +* [Schemas](./schema-and-columns) allow you to tailor the render and sort methods for each column. The component ships with a few automatic schema detection and types, but you can also pass in custom ones. +* Unlike tables, the data grid **forces truncation**. To display more content your can customize [popovers](./cells-and-popovers#conditionally-customizing-cell-popover-content) to display more content and actions into popovers. +* [Grid styling](./style-display#grid-style) can be controlled by the engineer, but augmented by user preference depending upon the features you enable. +* [Control columns](./schema-and-columns#control-columns) allow you to add repeatable actions and controls like checkboxes or buttons to your grid. +* The [toolbar](./toolbar) offers the user ways to manipulate the display and order of the data. + +```tsx interactive +import React, { + useCallback, + useEffect, + useState, + createContext, + useContext, + useRef, + createRef, +} from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiCode, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiDataGrid, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiLink, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiPopover, + EuiScreenReaderOnly, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { Link } from 'react-router-dom'; +import { faker } from '@faker-js/faker'; + +const gridRef = createRef(); +const DataContext = createContext(); +const raw_data = []; + +for (let i = 1; i < 100; i++) { + const email = faker.internet.email(); + const name = `${faker.person.lastName()}, ${faker.person.firstName()}`; + const suffix = faker.person.suffix(); + raw_data.push({ + name: { + formatted: `${name} ${suffix}`, + raw: name, + }, + email: { + formatted: {faker.internet.email()}, + raw: email, + }, + location: ( + <> + {`${faker.location.city()}, `} + {faker.location.country()} + + ), + date: `${faker.date.past()}`, + account: faker.finance.accountNumber(), + amount: faker.commerce.price(), + phone: faker.phone.number(), + version: faker.system.semver(), + }); +} + +const RenderCellValue = ({ rowIndex, columnId, setCellProps }) => { + const data = useContext(DataContext); + useEffect(() => { + if (columnId === 'amount') { + if (data.hasOwnProperty(rowIndex)) { + const numeric = parseFloat( + data[rowIndex][columnId].match(/\d+\.\d+/)[0] + ); + setCellProps({ + style: { + backgroundColor: `rgba(0, 255, 0, ${numeric * 0.0002})`, + }, + }); + } + } + }, [rowIndex, columnId, setCellProps, data]); + + function getFormatted() { + return data[rowIndex][columnId].formatted + ? data[rowIndex][columnId].formatted + : data[rowIndex][columnId]; + } + + return data.hasOwnProperty(rowIndex) + ? getFormatted(rowIndex, columnId) + : null; +}; + +const columns = [ + { + id: 'name', + displayAsText: 'Name', + defaultSortDirection: 'asc', + cellActions: [ + ({ rowIndex, columnId, Component }) => { + const data = useContext(DataContext); + return ( + alert(`Hi ${data[rowIndex][columnId].raw}`)} + iconType="heart" + aria-label={`Say hi to ${data[rowIndex][columnId].raw}!`} + > + Say hi + + ); + }, + ({ rowIndex, columnId, Component }) => { + const data = useContext(DataContext); + return ( + alert(`Bye ${data[rowIndex][columnId].raw}`)} + iconType="moon" + aria-label={`Say bye to ${data[rowIndex][columnId].raw}!`} + > + Say bye + + ); + }, + ], + }, + { + id: 'email', + displayAsText: 'Email address', + initialWidth: 130, + cellActions: [ + ({ rowIndex, columnId, Component }) => { + const data = useContext(DataContext); + return ( + alert(data[rowIndex][columnId].raw)} + iconType="email" + aria-label={`Send email to ${data[rowIndex][columnId].raw}`} + > + Send email + + ); + }, + ], + }, + { + id: 'location', + displayAsText: 'Location', + }, + { + id: 'account', + displayAsText: 'Account', + actions: { + showHide: { label: 'Custom hide label' }, + showMoveLeft: false, + showMoveRight: false, + additional: [ + { + label: 'Custom action', + onClick: () => {}, + iconType: 'cheer', + size: 'xs', + color: 'text', + }, + ], + }, + cellActions: [ + ({ rowIndex, columnId, Component, isExpanded }) => { + const data = useContext(DataContext); + const onClick = isExpanded + ? () => + alert(`Sent money to ${data[rowIndex][columnId]} when expanded`) + : () => + alert( + `Sent money to ${data[rowIndex][columnId]} when not expanded` + ); + return ( + + Send money + + ); + }, + ], + }, + { + id: 'date', + displayAsText: 'Date', + defaultSortDirection: 'desc', + }, + { + id: 'amount', + displayAsText: 'Amount', + }, + { + id: 'phone', + displayAsText: 'Phone', + isSortable: false, + }, + { + id: 'version', + displayAsText: 'Version', + defaultSortDirection: 'desc', + initialWidth: 70, + isResizable: false, + actions: false, + }, +]; + +const trailingControlColumns = [ + { + id: 'actions', + width: 40, + headerCellRender: () => ( + + Controls + + ), + rowCellRender: function RowCellRender({ rowIndex, colIndex }) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + const closePopover = () => setIsPopoverVisible(false); + + const [isModalVisible, setIsModalVisible] = useState(false); + const closeModal = () => { + setIsModalVisible(false); + gridRef.current.setFocusedCell({ rowIndex, colIndex }); + }; + const showModal = () => { + closePopover(); + setIsModalVisible(true); + }; + + let modal; + + if (isModalVisible) { + modal = ( + + + A typical modal + + + + +

+ + EuiModal + {' '} + components have a higher z-index than{' '} + EuiDataGrid components, even in fullscreen + mode. This ensures that modals will never appear behind the + data grid. +

+
+
+ + + + Close + + +
+ ); + } + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => { + setIsFlyoutVisible(false); + gridRef.current.setFocusedCell({ rowIndex, colIndex }); + }; + const showFlyout = () => { + closePopover(); + setIsFlyoutVisible(true); + }; + + let flyout; + + if (isFlyoutVisible) { + flyout = ( + + + +

A typical flyout

+
+
+ + + +

+ + EuiFlyout + {' '} + components have a higher z-index than{' '} + EuiDataGrid components, even in fullscreen + mode. This ensures that flyouts will never appear behind the + data grid. +

+ +

+ Flyouts are also styled with a vertical offset that accounts + for the presence of fixed headers. However, when the data grid + is in fullscreen mode, these offset styles are ignored to + allow the flyout to correctly appear at the top of the + viewport. +

+
+
+ + + + Close + + +
+ ); + } + + const actions = [ + + Modal example + , + + Flyout example + , + ]; + + return ( + <> + setIsPopoverVisible(!isPopoverVisible)} + /> + } + closePopover={closePopover} + > + + + + {modal} + + {flyout} + + ); + }, + }, +]; + +export default () => { + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) // initialize to the full set of columns + ); + + const onColumnResize = useRef((eventData) => { + console.log(eventData); + }); + + return ( + + + + ); +}; +``` + +## Top level props + +The below table contains a list of all top level **EuiDataGrid** props and sample snippets used to configure or customize them. + +For a full list of all data grid types, including lower level configurations, see the [/datagrid/data\_grid\_types.ts](https://github.com/elastic/eui/tree/main/packages/eui/src/components/datagrid/data_grid_types.ts) definition file, or view the props tables of individual datagrid sub-pages. + +import TopLevelProps from './data_grid_props'; + + diff --git a/packages/website/docs/components/tabular_content/data_grid_cells_and_popovers.mdx b/packages/website/docs/components/tabular_content/data_grid/data_grid_cells_and_popovers.mdx similarity index 99% rename from packages/website/docs/components/tabular_content/data_grid_cells_and_popovers.mdx rename to packages/website/docs/components/tabular_content/data_grid/data_grid_cells_and_popovers.mdx index 35a1340ad3f..1b52d47c476 100644 --- a/packages/website/docs/components/tabular_content/data_grid_cells_and_popovers.mdx +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_cells_and_popovers.mdx @@ -1,9 +1,10 @@ --- slug: /tabular-content/data-grid/cells-and-popovers id: tabular_content_data_grid_cells_popovers +sidebar_position: 2 --- -# Data grid cells & popovers +# Cells & popovers Data grid cells are rendered using the `renderCellValue` prop. This prop accepts a function which is treated as a React component behind the scenes. This means the data grid passes cell-specific props (e.g. row index, column/schema info, etc.) to your render function, which can ouput anything from a string to any JSX element. @@ -180,7 +181,6 @@ export default () => { /> ); }; - ``` ## Visible cell actions and cell popovers @@ -291,7 +291,6 @@ export default () => { /> ); }; - ``` ## Conditionally customizing cell popover content @@ -395,7 +394,6 @@ export default () => { /> ); }; - ``` ## Completely customizing cell popover rendering @@ -598,7 +596,6 @@ export default () => { /> ); }; - ``` ## Disabling cell expansion popovers @@ -697,7 +694,6 @@ export default () => { /> ); }; - ``` ## Focus @@ -1042,5 +1038,11 @@ export default () => { ); }; - ``` + +## Props + +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; + + + diff --git a/packages/website/docs/components/tabular_content/data_grid.mdx b/packages/website/docs/components/tabular_content/data_grid/data_grid_container_constraints.mdx similarity index 52% rename from packages/website/docs/components/tabular_content/data_grid.mdx rename to packages/website/docs/components/tabular_content/data_grid/data_grid_container_constraints.mdx index 1a80ddb2b24..8f776502fe9 100644 --- a/packages/website/docs/components/tabular_content/data_grid.mdx +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_container_constraints.mdx @@ -1,460 +1,14 @@ --- -slug: /tabular-content/data-grid -id: tabular_content_data_grid +slug: /tabular-content/data-grid/container-constraints +id: tabular_content_data_grid_container_constraints +sidebar_position: 5 --- -# Data grid - -**EuiDataGrid** is for displaying large amounts of tabular data. It is a better choice over [EUI tables](/docs/tabular-content/tables/) when there are many columns, the data in those columns is fairly uniform, and when schemas and sorting are important for comparison. Although it is similar to traditional spreedsheet software, EuiDataGrid's current strengths are in rendering rather than creating content. - -## Core concepts - -* The grid allows you to optionally define an [in memory level](/docs/tabular-content/data-grid-advanced#data-grid-in-memory) to have the grid automatically handle updating your columns. Depending upon the level chosen, you may need to manage the content order separately from the grid. -* [Schemas](/docs/tabular-content/data-grid-schema-columns) allow you to tailor the render and sort methods for each column. The component ships with a few automatic schema detection and types, but you can also pass in custom ones. -* Unlike tables, the data grid **forces truncation**. To display more content your can customize [popovers](/docs/tabular-content/data-grid/cells-and-popovers#conditionally-customizing-cell-popover-content) to display more content and actions into popovers. -* [Grid styling](/docs/tabular-content/data-grid-style-display#grid-style) can be controlled by the engineer, but augmented by user preference depending upon the features you enable. -* [Control columns](/docs/tabular-content/data-grid-schema-columns#control-columns) allow you to add repeatable actions and controls like checkboxes or buttons to your grid. -* The [toolbar](/docs/tabular-content/data-grid-toolbar) offers the user ways to manipulate the display and order of the data. - -```tsx interactive -import React, { - Fragment, - useCallback, - useEffect, - useState, - createContext, - useContext, - useRef, - createRef, -} from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiCode, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiDataGrid, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiLink, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiPopover, - EuiScreenReaderOnly, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { Link } from 'react-router-dom'; -import { faker } from '@faker-js/faker'; - -const gridRef = createRef(); -const DataContext = createContext(); -const raw_data = []; - -for (let i = 1; i < 100; i++) { - const email = faker.internet.email(); - const name = `${faker.person.lastName()}, ${faker.person.firstName()}`; - const suffix = faker.person.suffix(); - raw_data.push({ - name: { - formatted: `${name} ${suffix}`, - raw: name, - }, - email: { - formatted: {faker.internet.email()}, - raw: email, - }, - location: ( - - {`${faker.location.city()}, `} - {faker.location.country()} - - ), - date: `${faker.date.past()}`, - account: faker.finance.accountNumber(), - amount: faker.commerce.price(), - phone: faker.phone.number(), - version: faker.system.semver(), - }); -} - -const RenderCellValue = ({ rowIndex, columnId, setCellProps }) => { - const data = useContext(DataContext); - useEffect(() => { - if (columnId === 'amount') { - if (data.hasOwnProperty(rowIndex)) { - const numeric = parseFloat( - data[rowIndex][columnId].match(/\d+\.\d+/)[0] - ); - setCellProps({ - style: { - backgroundColor: `rgba(0, 255, 0, ${numeric * 0.0002})`, - }, - }); - } - } - }, [rowIndex, columnId, setCellProps, data]); - - function getFormatted() { - return data[rowIndex][columnId].formatted - ? data[rowIndex][columnId].formatted - : data[rowIndex][columnId]; - } - - return data.hasOwnProperty(rowIndex) - ? getFormatted(rowIndex, columnId) - : null; -}; - -const columns = [ - { - id: 'name', - displayAsText: 'Name', - defaultSortDirection: 'asc', - cellActions: [ - ({ rowIndex, columnId, Component }) => { - const data = useContext(DataContext); - return ( - alert(`Hi ${data[rowIndex][columnId].raw}`)} - iconType="heart" - aria-label={`Say hi to ${data[rowIndex][columnId].raw}!`} - > - Say hi - - ); - }, - ({ rowIndex, columnId, Component }) => { - const data = useContext(DataContext); - return ( - alert(`Bye ${data[rowIndex][columnId].raw}`)} - iconType="moon" - aria-label={`Say bye to ${data[rowIndex][columnId].raw}!`} - > - Say bye - - ); - }, - ], - }, - { - id: 'email', - displayAsText: 'Email address', - initialWidth: 130, - cellActions: [ - ({ rowIndex, columnId, Component }) => { - const data = useContext(DataContext); - return ( - alert(data[rowIndex][columnId].raw)} - iconType="email" - aria-label={`Send email to ${data[rowIndex][columnId].raw}`} - > - Send email - - ); - }, - ], - }, - { - id: 'location', - displayAsText: 'Location', - }, - { - id: 'account', - displayAsText: 'Account', - actions: { - showHide: { label: 'Custom hide label' }, - showMoveLeft: false, - showMoveRight: false, - additional: [ - { - label: 'Custom action', - onClick: () => {}, - iconType: 'cheer', - size: 'xs', - color: 'text', - }, - ], - }, - cellActions: [ - ({ rowIndex, columnId, Component, isExpanded }) => { - const data = useContext(DataContext); - const onClick = isExpanded - ? () => - alert(`Sent money to ${data[rowIndex][columnId]} when expanded`) - : () => - alert( - `Sent money to ${data[rowIndex][columnId]} when not expanded` - ); - return ( - - Send money - - ); - }, - ], - }, - { - id: 'date', - displayAsText: 'Date', - defaultSortDirection: 'desc', - }, - { - id: 'amount', - displayAsText: 'Amount', - }, - { - id: 'phone', - displayAsText: 'Phone', - isSortable: false, - }, - { - id: 'version', - displayAsText: 'Version', - defaultSortDirection: 'desc', - initialWidth: 70, - isResizable: false, - actions: false, - }, -]; - -const trailingControlColumns = [ - { - id: 'actions', - width: 40, - headerCellRender: () => ( - - Controls - - ), - rowCellRender: function RowCellRender({ rowIndex, colIndex }) { - const [isPopoverVisible, setIsPopoverVisible] = useState(false); - const closePopover = () => setIsPopoverVisible(false); - - const [isModalVisible, setIsModalVisible] = useState(false); - const closeModal = () => { - setIsModalVisible(false); - gridRef.current.setFocusedCell({ rowIndex, colIndex }); - }; - const showModal = () => { - closePopover(); - setIsModalVisible(true); - }; - - let modal; - - if (isModalVisible) { - modal = ( - - - A typical modal - - - - -

- - EuiModal - {' '} - components have a higher z-index than{' '} - EuiDataGrid components, even in fullscreen - mode. This ensures that modals will never appear behind the - data grid. -

-
-
- - - - Close - - -
- ); - } - - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const closeFlyout = () => { - setIsFlyoutVisible(false); - gridRef.current.setFocusedCell({ rowIndex, colIndex }); - }; - const showFlyout = () => { - closePopover(); - setIsFlyoutVisible(true); - }; - - let flyout; - - if (isFlyoutVisible) { - flyout = ( - - - -

A typical flyout

-
-
- - - -

- - EuiFlyout - {' '} - components have a higher z-index than{' '} - EuiDataGrid components, even in fullscreen - mode. This ensures that flyouts will never appear behind the - data grid. -

- -

- Flyouts are also styled with a vertical offset that accounts - for the presence of fixed headers. However, when the data grid - is in fullscreen mode, these offset styles are ignored to - allow the flyout to correctly appear at the top of the - viewport. -

-
-
- - - - Close - - -
- ); - } - - const actions = [ - - Modal example - , - - Flyout example - , - ]; - - return ( - <> - setIsPopoverVisible(!isPopoverVisible)} - /> - } - closePopover={closePopover} - > - - - - {modal} - - {flyout} - - ); - }, - }, -]; - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const onChangeItemsPerPage = useCallback( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Sorting - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) // initialize to the full set of columns - ); - - const onColumnResize = useRef((eventData) => { - console.log(eventData); - }); - - return ( - - - - ); -}; - -``` - -## Top level props - -Please check the [props section](#props) below for more explanation on the lower level object types. The majority of the types are defined in the [/datagrid/data\_grid\_types.ts](https://github.com/elastic/eui/tree/main/packages/eui/src/components/datagrid/data_grid_types.ts) file. +# Container constraints ## Data grid adapts to its container -When wrapped inside a container, like a dashboard panel, the grid will start hiding controls and adopt a more strict flex layout. Use the`minSizeForControls` prop to control the min width to enables/disables grid controls based on available width. +When wrapped inside a container, like a dashboard panel, the grid will start hiding controls and adopt a more strict flex layout. Use the `minSizeForControls` prop to control the min width to enables/disables grid controls based on available width. ```tsx interactive import React, { useState, useCallback } from 'react'; @@ -539,7 +93,6 @@ export default () => { ); }; - ``` When placed within an [**EuiFlexGroup** and **EuiFlexItem**](/docs/layout/flex), the data grid will have trouble shrinking to fit. To fix this, you will need to manually add a style of `min-width: 0` to the **EuiFlexItem**. @@ -644,7 +197,6 @@ export default () => { ); }; - ``` ## Virtualization @@ -659,7 +211,6 @@ Similar to React's rule of not switching between a controlled and uncontrolled i ```tsx interactive import React, { - Fragment, useCallback, useState, createContext, @@ -732,12 +283,12 @@ function RenderCellValue({ rowIndex, columnId }) { name: `${name} ${suffix}`, email: {email}, location: ( - + <> {`${faker.location.city()}, `} {faker.location.country()} - + ), date: `${faker.date.past()}`, account: faker.finance.accountNumber(), @@ -861,14 +412,12 @@ export default () => { ); }; - ``` ### Constrained by DOM ```tsx interactive import React, { - Fragment, useCallback, useState, createContext, @@ -939,12 +488,12 @@ function RenderCellValue({ rowIndex, columnId }) { name: `${name} ${suffix}`, email: {email}, location: ( - + <> {`${faker.location.city()}, `} {faker.location.country()} - + ), date: `${faker.date.past()}`, account: faker.finance.accountNumber(), @@ -1039,11 +588,11 @@ export default () => { ); }; - ``` ## Props -import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid.json'; +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; - + + diff --git a/packages/website/docs/components/tabular_content/data_grid/data_grid_props.tsx b/packages/website/docs/components/tabular_content/data_grid/data_grid_props.tsx new file mode 100644 index 00000000000..bfa13553832 --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_props.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid.json'; + +import { DataGridPropSnippetTable } from './_prop_snippet_table'; + +export const topLevelPropSnippets = { + rowCount: 'rowCount={200}', + columns: `columns={[ + { + id: 'A', // required + display: <>Column A , // optional column header rendering + displayAsText: 'Column A', // column header as plain text + displayHeaderCellProps: { className: 'eui-textCenter' }, // optional column header cell props + initialWidth: 150, // starting width of 150px + isResizable: false, // prevents the user from resizing width + isExpandable: false, // doesn't allow clicking in to see the content in a popup + isSortable: false, // prevents the user from sorting the data grid by this column + defaultSortDirection: 'asc', // sets the default sort direction + schema: 'franchise', // custom schema later defined under schemaDetectors + actions: false, // no column header actions are displayed + actions: { showMoveLeft: false, showMoveRight: false }, // doesn't show move actions in column header + cellActions: [ // provides one additional cell action that triggers an alert once clicked + ({ Component }) => alert('test')}>Custom action, + ], + visibleCellActions: 2, // configures the number of cell action buttons immediately visible on a cell + }, +]}`, + leadingControlColumns: `leadingControlColumns={[ + { + id: 'selection', + width: 31, + headerCellRender: () => Select a row, + headerCellProps: { className: 'eui-textCenter' }, + rowCellRender: () =>
, + footerCellRender: () => Select a row, + footerCellProps: { className: 'eui-textCenter' }, + }, +]}`, + trailingControlColumns: `trailingControlColumns={[ + { + id: 'actions', + width: 40, + headerCellRender: () => 'Actions', + headerCellProps: { className: 'euiScreenReaderOnly' }, + rowCellRender: MyGridActionsComponent, + footerCellRender: () => null, + footerCellProps: { data-test-subj: 'emptyFooterCell' }, + }, +]}`, + columnVisibility: `columnVisibility={{ + visibleColumns: ['A'], + setVisibleColumns: () => {}, + canDragAndDropColumns: true, +}}`, + onColumnResize: 'onColumnResize={({columnId, width}) => {}}', + schemaDetectors: ( + + See Schemas & columns for full details. + + ), + renderCellValue: 'renderCellValue={({ rowIndex, columnId }) => {}}', + cellContext: `cellContext={{ + // Will be passed to your \`renderCellValue\` function/component as a prop + yourData, +}} +renderCellValue={({ rowIndex, columnId, yourData }) => {}}`, + renderCellPopover: `renderCellPopover={({ children, cellActions }) => ( + <> + I'm a custom popover! + {children} + {cellActions} + +)}`, + renderFooterCellValue: + 'renderFooterCellValue={({ rowIndex, columnId }) => {}}', + renderCustomToolbar: + 'renderCustomToolbar={({ displayControl }) =>
Custom toolbar content {displayControl}
}', + renderCustomGridBody: `// Optional; advanced usage only. This render function is an escape hatch for consumers who need to opt out of virtualization or otherwise need total custom control over how data grid cells are rendered. + +renderCustomGridBody={({ visibleColumns, visibleRowData, Cell }) => ( + +)}`, + pagination: `pagination={{ + pageIndex: 1, + pageSize: 100, // If not specified, defaults to EuiTablePagination.itemsPerPage + pageSizeOptions: [50, 100, 200], // If not specified, defaults to EuiTablePagination.itemsPerPageOptions + onChangePage: () => {}, + onChangeItemsPerPage: () => {}, +}}`, + sorting: `sorting={{ + columns: [{ id: 'A', direction: 'asc' }], + onSort: () => {}, +}}`, + inMemory: `// Will try to autodectect schemas and do sorting and pagination in memory. +inMemory={{ level: 'sorting' }}`, + toolbarVisibility: `toolbarVisibility={{ + showColumnSelector: false, + showDisplaySelector: false, + showSortSelector: false, + showKeyboardShortcuts: false, + showFullScreenSelector: false, + additionalControls: { + left: , + right: , + }, +}}`, + minSizeForControls: 'minSizeForControls={500}', + gridStyle: `gridStyle={{ + border: 'none', + stripes: true, + rowHover: 'highlight', + header: 'underline', + // If showDisplaySelector.allowDensity={true} from toolbarVisibility, fontSize and cellPadding will be superceded by what the user decides. + cellPadding: 'm', + fontSize: 'm', + footer: 'overline' +}}`, + rowHeightsOptions: `rowHeightsOptions={{ + defaultHeight: { + lineCount: 3 // default every row to 3 lines of text + }, + lineHeight: '2em', // default every cell line-height to 2em + rowHeights: { + 1: { + lineCount: 5, // row at index 1 will show 5 lines + }, + 4: 200, // row at index 4 will adjust the height to 200px + 6: 'auto', // row at index 6 will automatically adjust the height + }, + scrollAnchorRow: 'start', // compensate for layout shift when auto-sized rows are scrolled into view +}}`, + ref: `// Optional. For advanced control of internal data grid state, passes back an object of imperative API methods +ref={dataGridRef}`, + virtualizationOptions: `// Optional. For advanced control of the underlying react-window virtualization grid. +virtualizationOptions={{ + className: 'virtualizedGridClass', + style: {}, + direction: 'ltr', + estimatedRowHeight: 50, + overscanColumnCount: 1, + overscanRowCount: 1, + initialScrollLeft: 0, + initialScrollTop: 0, + onScroll: () => {}, + onItemsRendered: () => {}, + itemKey: () => {}, + outerElementType: 'div', +}} +// Properties not listed above are used by EuiDataGrid internals and cannot be overridden. +`, +}; + +const propLinks = { + schemaDetectors: '../data-grid/schema-and-columns#schemas', + onColumnResize: '../data-grid/schema-and-columns/#column-widths', + leadingControlColumns: '../data-grid/schema-and-columns#control-columns', + trailingControlColumns: '../data-grid/schema-and-columns#control-columns', + renderFooterCellValue: '../data-grid/schema-and-columns#footer-row', + renderCellPopover: + '../data-grid/cells-and-popovers#completely-customizing-cell-popover-rendering', + cellContext: '../data-grid/cells-and-popovers#cell-context', + rowHeightsOptions: '../data-grid/style-and-display#row-heights-options', + gridStyle: '../data-grid/style-and-display#grid-style', + inMemory: '../data-grid/advanced#data-grid-in-memory', + ref: '../data-grid/advanced#ref-methods', + renderCustomGridBody: '../data-grid/advanced#custom-body-renderer', + renderCustomToolbar: + '../data-grid/toolbar#completely-custom-toolbar-rendering', + toolbarVisibility: '../data-grid/toolbar#toolbar-visibility', + minSizeForControls: '../data-grid/container-constraints', + virtualizationOptions: '../data-grid/container-constraints/#virtualization', +}; + +export default () => ( + +); diff --git a/packages/website/docs/components/tabular_content/data_grid_schema_and_columns.mdx b/packages/website/docs/components/tabular_content/data_grid/data_grid_schema_and_columns.mdx similarity index 89% rename from packages/website/docs/components/tabular_content/data_grid_schema_and_columns.mdx rename to packages/website/docs/components/tabular_content/data_grid/data_grid_schema_and_columns.mdx index e3ad27a9aaf..5074b13b604 100644 --- a/packages/website/docs/components/tabular_content/data_grid_schema_and_columns.mdx +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_schema_and_columns.mdx @@ -1,9 +1,10 @@ --- slug: /tabular-content/data-grid/schema-and-columns id: tabular_content_data_grid_schema_columns +sidebar_position: 1 --- -# Data grid schema & columns +# Schema & columns ## Schemas @@ -11,10 +12,11 @@ Schemas are data types you pass to grid columns to affect how the columns and ex ### Defining custom schemas -Custom schemas are passed as an array to `schemaDetectors` and are constructed against the **EuiDataGridSchemaDetector** interface. You can see an example of a simple custom schema used on the last column below. In addition to schemas being useful to map against for cell and expansion rendering, any schema will also add a`className="euiDataGridRowCell--schemaName"` to each matching cell. +Custom schemas are passed as an array to `schemaDetectors` and are constructed against the **EuiDataGridSchemaDetector** interface. You can see an example of a simple custom schema used on the last column below. In addition to schemas being useful to map against for cell and expansion rendering, any schema will also add a `className="euiDataGridRowCell--schemaName"` to each matching cell. ```tsx interactive import React, { useState } from 'react'; +import { css } from '@emotion/react'; import { EuiDataGrid } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -143,10 +145,18 @@ export default () => { color: '#800080', }, ]} + css={css` + .euiDataGridRowCell--favoriteFranchise { + background-color: rgba(128, 0, 128, 0.05); + } + + .euiDataGridHeaderCell--favoriteFranchise { + background-color: rgba(128, 0, 128, 0.1); + } + `} /> ); }; - ``` ## Column widths @@ -242,7 +252,6 @@ export default () => { /> ); }; - ``` ## Column actions @@ -361,7 +370,100 @@ export default () => { /> ); }; +``` + +## Draggable columns + +To reorder columns directly instead of via the actions menu popover, you can enable draggable **EuiDataGrid** header columns via the `columnVisibility.canDragAndDropColumns` prop. This will allow you to reorder the column by dragging them. + +```tsx interactive +import React, { useState, useCallback } from 'react'; +import { EuiDataGrid, EuiAvatar } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const columns = [ + { + id: 'avatar', + initialWidth: 40, + isResizable: false, + }, + { + id: 'name', + initialWidth: 100, + }, + { + id: 'email', + }, + { + id: 'city', + }, + { + id: 'country', + }, + { + id: 'account', + }, +]; +const data = []; + +for (let i = 1; i < 5; i++) { + data.push({ + avatar: ( + + ), + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: faker.internet.email(), + city: faker.location.city(), + country: faker.location.country(), + account: faker.finance.accountNumber(), + }); +} + +export default () => { + const [pagination, setPagination] = useState({ pageIndex: 0 }); + + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const setPageIndex = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [] + ); + const setPageSize = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [] + ); + + return ( + data[rowIndex][columnId]} + pagination={{ + ...pagination, + onChangeItemsPerPage: setPageSize, + onChangePage: setPageIndex, + }} + /> + ); +}; ``` ## Control columns @@ -379,7 +481,6 @@ import React, { useCallback, useReducer, useState, - Fragment, } from 'react'; import { EuiDataGrid, @@ -564,10 +665,10 @@ const FlyoutRowCell = (rowIndex) => { const details = Object.entries(rowData).map(([key, value]) => { return ( - + <> {key} {value} - + ); }); @@ -588,7 +689,7 @@ const FlyoutRowCell = (rowIndex) => { } return ( - + <> { onClick={() => setIsFlyoutOpen(!isFlyoutOpen)} /> {flyout} - + ); }; @@ -751,7 +852,6 @@ export default function DataGrid() { ); } - ``` ## Footer row @@ -958,5 +1058,15 @@ export default () => { ); }; - ``` + +## Props + +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; + + + + + + + diff --git a/packages/website/docs/components/tabular_content/data_grid/data_grid_style_and_display.mdx b/packages/website/docs/components/tabular_content/data_grid/data_grid_style_and_display.mdx new file mode 100644 index 00000000000..79a4aa78fb4 --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_style_and_display.mdx @@ -0,0 +1,611 @@ +--- +slug: /tabular-content/data-grid/style-and-display +id: tabular_content_data_grid_style_display +sidebar_position: 4 +--- + +# Style & display + +## Grid style + +Styling can be passed down to the grid through the `gridStyle` prop. It accepts an object that allows for customization. + +:::tip +To reduce grid re-renders, memoize or define your grid styles as a constant outside of components. +::: + +import GridStyleToggles from './data_grid_style_toggles'; + + + +:::note Density selector and grid styles + +The `showDisplaySelector.allowDensity` setting in `toolbarVisibility` means the user has the ability to override the padding and font size passed into `gridStyle` by the engineer. + +The font size overriding only works with text or elements that can inherit the parent font size or elements that use units relative to the parent container. + +::: + +## Grid row classes + +Specific rows can be highlighted or otherwise have custom styling passed to them via the`gridStyle.rowClasses` prop. It accepts an object associating the row's index with a class name string. + +The example below sets a custom striped class on the 3rd row and dynamically updates the `rowClasses` map when rows are selected. + +```tsx interactive +import React, { + createContext, + useContext, + useReducer, + useState, + useMemo, +} from 'react'; +import { css } from '@emotion/react'; +import { + EuiDataGrid, + EuiCheckbox, + EuiButtonEmpty, + useEuiTheme, +} from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +/** + * Data + */ +const columns = [ + { id: 'name' }, + { id: 'email' }, + { id: 'city' }, + { id: 'country' }, + { id: 'account' }, +]; + +const data = Array.from({ length: 5 }).map(() => ({ + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: faker.internet.email(), + city: faker.location.city(), + country: faker.location.country(), + account: faker.finance.accountNumber(), +})); + +const DEMO_ROW = 2; +data[DEMO_ROW].account = 'OVERDUE'; + +/** + * Selection + */ +const SelectionContext = createContext>([ + undefined, + () => {}, +]); + +const SelectionButton = () => { + const [selectedRows] = useContext(SelectionContext); + const hasSelection = selectedRows.size > 0; + return hasSelection ? ( + window.alert('This is not a real control.')} + > + {selectedRows.size} {selectedRows.size > 1 ? 'items' : 'item'} selected + + ) : null; +}; + +const SelectionHeaderCell = () => { + const [selectedRows, updateSelectedRows] = useContext(SelectionContext); + const isIndeterminate = + selectedRows.size > 0 && selectedRows.size < data.length; + return ( + 0} + onChange={(e) => { + if (isIndeterminate) { + // clear selection + updateSelectedRows({ action: 'clear' }); + } else { + if (e.target.checked) { + // select everything + updateSelectedRows({ action: 'selectAll' }); + } else { + // clear selection + updateSelectedRows({ action: 'clear' }); + } + } + }} + /> + ); +}; + +const SelectionRowCell = ({ rowIndex }) => { + const [selectedRows, updateSelectedRows] = useContext(SelectionContext); + const isChecked = selectedRows.has(rowIndex); + return ( + <> + { + if (e.target.checked) { + updateSelectedRows({ action: 'add', rowIndex }); + } else { + updateSelectedRows({ action: 'delete', rowIndex }); + } + }} + /> + + ); +}; + +const leadingControlColumns = [ + { + id: 'selection', + width: 32, + headerCellRender: SelectionHeaderCell, + rowCellRender: SelectionRowCell, + }, +]; + +/** + * Data grid + */ +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const rowSelection = useReducer((rowSelection, { action, rowIndex }) => { + if (action === 'add') { + const nextRowSelection = new Set(rowSelection); + nextRowSelection.add(rowIndex); + return nextRowSelection; + } else if (action === 'delete') { + const nextRowSelection = new Set(rowSelection); + nextRowSelection.delete(rowIndex); + return nextRowSelection; + } else if (action === 'clear') { + return new Set(); + } else if (action === 'selectAll') { + return new Set(data.map((_, index) => index)); + } + return rowSelection; + }, new Set()); + + const { euiTheme } = useEuiTheme(); + const rowStyles = useMemo(() => { + return css` + .euiDataGridRow--rowClassesDemo { + background-color: ${euiTheme.colors.highlight}; + } + + .euiDataGridRow--rowClassesDemoSelected { + background-color: ${euiTheme.focus.backgroundColor}; + } + `; + }, [euiTheme]); + + const rowClasses = useMemo(() => { + const rowClasses = { + [DEMO_ROW]: 'euiDataGridRow--rowClassesDemo', + }; + rowSelection[0].forEach((rowIndex) => { + rowClasses[rowIndex] = 'euiDataGridRow--rowClassesDemoSelected'; + }); + return rowClasses; + }, [rowSelection]); + + return ( + + data[rowIndex][columnId]} + leadingControlColumns={leadingControlColumns} + toolbarVisibility={{ + additionalControls: , + }} + gridStyle={{ rowClasses, rowHover: 'none' }} + css={rowStyles} + /> + + ); +}; +``` + +## Row heights options + +By default, all rows get a height of **34 pixels**, but there are scenarios where you might want to adjust the height to fit more content. To do that, you can pass an object to the `rowHeightsOptions` prop. This object accepts the following properties: + +* `defaultHeight` + * Defines the default size for all rows + * Can be configured with an exact pixel height, a line count, or `"auto"` to fit all content +* `rowHeights` + * Overrides the height for a specific row + * Can be configured with an exact pixel height, a line count, or `"auto"` to fit all content +* `lineHeight` + * Sets a default line height for all cells, which is used to calculate row height + * Accepts any value that the `line-height` CSS property normally takes (e.g. px, ems, rems, or unitless) +* `onChange` + * Optional callback when the user changes the data grid's internal `rowHeightsOptions` (e.g., via the toolbar display selector). + * Can be used to store and preserve user display preferences on page refresh - see this [data grid styling and control example](/docs/tabular-content/data-grid-style-display#adjusting-your-grid-to-usertoolbar-changes). +* `scrollAnchorRow` + * Optional indicator of the row that should be used as an anchor for vertical layout shift compensation. + * Can be set to the default `undefined`,`"start"`, or`"center"`. + * If set to `"start"`, the topmost visible row will monitor for unexpected changes to its vertical position and try to compensate for these by scrolling the grid scroll container such that the topmost row position remains stable. + * If set to `"center"`, the middle visible row will monitor for unexpected changes to its vertical position and try to compensate for these by scrolling the grid scroll container such that the middle row position remains stable. + * This is particularly useful when the grid contains`auto` sized rows. Since these rows are measured as they appear in the overscan, they can cause surprising shifts of the vertical position of all following rows when their measured height is different from the estimated height. + +:::warning Rows have minimum height requirements + +Rows must be at least **34 pixels** tall so they can render at least one line of text. If you provide a smaller height the row will default to **34 pixels**. + +::: + +## Setting a default height and line height for rows + +You can change the default height for all rows via the `defaultHeight` property. Note that the `showDisplaySelector.allowRowHeight` setting in `toolbarVisibility` means the user has the ability to override this default height. Users will be able to toggle between single rows, a configurable line count, or `"auto"`. + +You can also customize the line height of all cells with the `lineHeight` property. However, if you wrap your cell content with CSS that overrides/sets line-height (e.g. in an `EuiText`), your row heights will not be calculated correctly - make sure to match the passed `lineHeight` property to the actual cell content line height. + +```tsx interactive +import React, { useState } from 'react'; +import { EuiDataGrid, RenderCellValue } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const raw_data = Array.from({ length: 5 }).map((_, i) => ({ + index: i, + productName: faker.commerce.productName(), + productDescription: `${faker.commerce.productDescription()}. ${faker.lorem.lines( + { min: 0, max: 5 } + )}`, +})); + +const columns = [ + { + id: 'index', + displayAsText: 'Index', + isExpandable: false, + initialWidth: 52, + }, + { + id: 'productName', + displayAsText: 'Product name', + isExpandable: false, + }, + { + id: 'productDescription', + displayAsText: 'Description', + }, +]; + +const RenderCell: RenderCellValue = ({ rowIndex, columnId }) => { + return raw_data[rowIndex][columnId]; +}; + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) // initialize to the full set of columns + ); + + return ( + + ); +}; +``` + +## Overriding specific row heights + +You can override the default height of a specific row by passing a`rowHeights` object associating the row's index with a specific height configuration. + +:::warning Disabling the row height toolbar control + +Individual row heights will be overridden by the toolbar display controls. If you do not want users to be able to override specific row heights, set `toolbarVisibility.showDisplaySelector.allowRowHeight` to `false`. + +::: + +```tsx interactive +import React, { useState } from 'react'; +import { EuiDataGrid, RenderCellValue } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const raw_data = Array.from({ length: 5 }).map((_, i) => ({ + index: i, + productName: faker.commerce.productName(), + productImage: ( + Mock product image + ), +})); + +const columns = [ + { + id: 'index', + displayAsText: 'Index', + isExpandable: false, + initialWidth: 52, + }, + { + id: 'productName', + displayAsText: 'Product name', + isExpandable: false, + }, + { + id: 'productImage', + displayAsText: 'Image', + }, +]; + +const RenderCell: RenderCellValue = ({ rowIndex, columnId }) => { + return raw_data[rowIndex][columnId]; +}; + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) // initialize to the full set of columns + ); + + return ( + + ); +}; +``` + +## Auto heights for rows + +To enable automatically fitting rows to their content you can set `defaultHeight="auto"`. This ensures every row automatically adjusts its height to fit the contents. + +You can also override the height of a specific row by passing a`rowHeights` object associating the row's index with an `"auto"` value. + +```tsx interactive +import React, { useCallback, useState } from 'react'; +import { EuiDataGrid, RenderCellValue, EuiTitle } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const raw_data = Array.from({ length: 100 }).map((_, i) => ({ + index: i, + productName: faker.commerce.productName(), + productDescription: `${faker.commerce.productDescription()}. ${faker.lorem.lines( + { min: 0, max: 5 } + )}`, +})); + +const columns = [ + { + id: 'index', + displayAsText: 'Index', + isExpandable: false, + initialWidth: 52, + }, + { + id: 'productName', + displayAsText: 'Product name', + isExpandable: false, + }, + { + id: 'productDescription', + displayAsText: 'Description', + }, +]; + +const RenderCell: RenderCellValue = ({ rowIndex, columnId }) => { + const value = raw_data[rowIndex][columnId]; + + if (columnId === 'productName') { + return ( + +

{value}

+
+ ); + } else if (columnId === 'productDescription' && rowIndex === 2) { + return ( + Mock product image + ); + } else { + return value; + } +}; + +export default () => { + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => + setPagination((pagination) => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) // initialize to the full set of columns + ); + + return ( + + ); +}; +``` + +## Adjusting your grid to user/toolbar changes[](/docs/tabular-content/data-grid-style-display#adjusting-your-grid-to-usertoolbar-changes) + +You can use the optional `gridStyle.onChange` and `rowHeightsOptions.onChange` callbacks to adjust your data grid based on user density or row height changes. + +For example, if the user changes the grid density to compressed, you may want to adjust a cell's content sizing in response. Or you could store user settings in localStorage or other database to preserve display settings on page refresh, like the below example does. + +```tsx interactive +import React, { useState, useCallback, useMemo } from 'react'; +import { EuiDataGrid, EuiIcon } from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +const columns = [ + { id: 'name' }, + { id: 'email' }, + { id: 'city' }, + { id: 'country' }, + { id: 'account' }, +]; +const data = []; +for (let i = 1; i <= 5; i++) { + data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + email: faker.internet.email(), + city: faker.location.city(), + country: faker.location.country(), + account: faker.finance.accountNumber(), + }); +} + +const GRID_STYLES_KEY = 'euiDataGridStyles'; +const INITIAL_STYLES = JSON.stringify({ stripes: true }); + +const ROW_HEIGHTS_KEY = 'euiDataGridRowHeightsOptions'; +const INITIAL_ROW_HEIGHTS = JSON.stringify({}); + +export default () => { + const [densitySize, setDensitySize] = useState(''); + const responsiveIcon = useCallback( + () => , + [densitySize] + ); + const responsiveIconWidth = useMemo(() => { + if (densitySize === 'l') return 44; + if (densitySize === 's') return 24; + return 32; + }, [densitySize]); + const leadingControlColumns = useMemo( + () => [ + { + id: 'icon', + width: responsiveIconWidth, + headerCellRender: responsiveIcon, + rowCellRender: responsiveIcon, + }, + ], + [responsiveIcon, responsiveIconWidth] + ); + + const storedRowHeightsOptions = useMemo( + () => + JSON.parse(localStorage.getItem(ROW_HEIGHTS_KEY) || INITIAL_ROW_HEIGHTS), + [] + ); + const storeRowHeightsOptions = useCallback((updatedRowHeights) => { + console.log(updatedRowHeights); + localStorage.setItem(ROW_HEIGHTS_KEY, JSON.stringify(updatedRowHeights)); + }, []); + + const storedGridStyles = useMemo( + () => JSON.parse(localStorage.getItem(GRID_STYLES_KEY) || INITIAL_STYLES), + [] + ); + const storeGridStyles = useCallback((updatedStyles) => { + console.log(updatedStyles); + localStorage.setItem(GRID_STYLES_KEY, JSON.stringify(updatedStyles)); + setDensitySize(updatedStyles.fontSize); + }, []); + + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + return ( + data[rowIndex][columnId]} + /> + ); +}; +``` + +## Props + +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; + + + diff --git a/packages/website/docs/components/tabular_content/data_grid/data_grid_style_toggles.tsx b/packages/website/docs/components/tabular_content/data_grid/data_grid_style_toggles.tsx new file mode 100644 index 00000000000..58eb06f173a --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_style_toggles.tsx @@ -0,0 +1,246 @@ +import { useState, useCallback, useMemo } from 'react'; +import { css } from '@emotion/react'; +import { + EuiDataGrid, + EuiAvatar, + EuiDataGridStyle, + useEuiTheme, +} from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +import { + ConfigurationDemoWithSnippet, + objectConfigToSnippet, +} from './_grid_configuration_wrapper'; + +const data = Array.from({ length: 5 }).map((_) => ({ + avatar: ( + + ), + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + account: faker.finance.accountNumber(), +})); +const footerCellValues = { + account: '5 accounts', +}; +const renderFooterCellValue = ({ columnId }) => + footerCellValues[columnId] || null; + +const columns = [ + { id: 'avatar', initialWidth: 40 }, + { id: 'name' }, + { id: 'account', schema: 'numeric' }, +]; + +const borderOptions = [ + { id: 'all', label: 'All' }, + { id: 'horizontal', label: 'Horizontal only' }, + { id: 'none', label: 'None' }, +]; + +const fontSizeOptions = [ + { id: 's', label: 'Small' }, + { id: 'm', label: 'Medium' }, + { id: 'l', label: 'Large' }, +]; + +const cellPaddingOptions = [ + { id: 's', label: 'Small' }, + { id: 'm', label: 'Medium' }, + { id: 'l', label: 'Large' }, +]; + +const stripeOptions = [ + { id: 'true', label: 'True' }, + { id: 'false', label: 'False' }, +]; + +const rowHoverOptions = [ + { id: 'none', label: 'None' }, + { id: 'highlight', label: 'Highlight' }, +]; + +const headerOptions = [ + { id: 'shade', label: 'Shade' }, + { id: 'underline', label: 'Underline' }, +]; + +export default () => { + const { euiTheme } = useEuiTheme(); + const [borderSelected, setBorderSelected] = useState('none'); + const [fontSizeSelected, setFontSizeSelected] = useState('m'); + const [cellPaddingSelected, setCellPaddingSelected] = useState('m'); + const [stripesSelected, setStripesSelected] = useState('true'); + const [rowHoverSelected, setRowHoverSelected] = useState('highlight'); + const [headerSelected, setHeaderSelected] = useState('underline'); + const [footerSelected, setFooterSelected] = useState('overline'); + + const [gridStyle, snippet] = useMemo(() => { + const gridStyle = { + border: borderSelected, + fontSize: fontSizeSelected, + cellPadding: cellPaddingSelected, + stripes: stripesSelected === 'true', + rowHover: rowHoverSelected, + header: headerSelected, + footer: footerSelected, + } as EuiDataGridStyle; + + const snippet = `const gridStyle = ${objectConfigToSnippet(gridStyle)}; + +`; + + return [gridStyle, snippet]; + }, [ + borderSelected, + fontSizeSelected, + cellPaddingSelected, + stripesSelected, + rowHoverSelected, + headerSelected, + footerSelected, + ]); + + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + const handleVisibleColumns = (visibleColumns) => + setVisibleColumns(visibleColumns); + + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const setPageIndex = useCallback((pageIndex) => { + setPagination((pagination) => ({ ...pagination, pageIndex })); + }, []); + const setPageSize = useCallback((pageSize) => { + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })); + }, []); + + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => setSortingColumns(sortingColumns), + [setSortingColumns] + ); + + return ( + + data[rowIndex][columnId]} + renderFooterCellValue={renderFooterCellValue} + rowCount={data.length} + height="auto" + gridStyle={gridStyle} + /> + + ); +}; diff --git a/packages/website/docs/components/tabular_content/data_grid_toolbar.mdx b/packages/website/docs/components/tabular_content/data_grid/data_grid_toolbar.mdx similarity index 64% rename from packages/website/docs/components/tabular_content/data_grid_toolbar.mdx rename to packages/website/docs/components/tabular_content/data_grid/data_grid_toolbar.mdx index 4ce83dabb05..e80a6faa747 100644 --- a/packages/website/docs/components/tabular_content/data_grid_toolbar.mdx +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_toolbar.mdx @@ -1,192 +1,18 @@ --- slug: /tabular-content/data-grid/toolbar id: tabular_content_data_grid_toolbar +sidebar_position: 3 --- -# Data grid toolbar +# Toolbar ## Toolbar visibility -The `toolbarVisibility` prop when used as a boolean controls the visibility of the entire toolbar displayed above the grid. Using the prop as a shape, allows setting the visibility of the individual controls within. +The `toolbarVisibility` prop, when used as a boolean, controls the visibility of the entire toolbar displayed above the grid. Using the prop's object configuration allows setting the visibility of the individual toolbar controls. -```tsx interactive -import React, { useState, useCallback, useMemo } from 'react'; -import { EuiDataGrid, EuiAvatar, EuiFormRow, EuiRange } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { - id: 'avatar', - initialWidth: 40, - }, - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'city', - }, - { - id: 'country', - }, - { - id: 'account', - }, -]; - -const data = []; - -for (let i = 1; i < 6; i++) { - data.push({ - avatar: ( - - ), - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: faker.internet.email(), - city: faker.location.city(), - country: faker.location.country(), - account: faker.finance.accountNumber(), - }); -} - -const DataGridStyle = ({ - toolbarType, - showColumnSelector, - showSortSelector, - showDisplaySelector, - showKeyboardShortcuts, - showFullScreenSelector, - allowDensity, - allowRowHeight, - allowResetButton, - additionalDisplaySettings, - allowHideColumns, - allowOrderingColumns, -}) => { - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const setPageIndex = useCallback((pageIndex) => { - setPagination((pagination) => ({ ...pagination, pageIndex })); - }, []); - - const setPageSize = useCallback((pageSize) => { - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })); - }, []); - - const handleVisibleColumns = (visibleColumns) => - setVisibleColumns(visibleColumns); - - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => setSortingColumns(sortingColumns), - [setSortingColumns] - ); - - const toggleColumnSelector = useMemo(() => { - if ( - showColumnSelector === true && - (allowHideColumns === false || allowOrderingColumns === false) - ) { - return { - allowHide: allowHideColumns, - allowReorder: allowOrderingColumns, - }; - } else { - return showColumnSelector; - } - }, [showColumnSelector, allowHideColumns, allowOrderingColumns]); - - const toggleDisplaySelector = useMemo(() => { - if ( - showDisplaySelector === true && - (allowDensity === false || - allowRowHeight === false || - allowResetButton === false || - additionalDisplaySettings) - ) { - const customDisplaySetting = additionalDisplaySettings && ( - - - - ); - return { - allowDensity, - allowRowHeight, - allowResetButton, - additionalDisplaySettings: customDisplaySetting, - }; - } else { - return showDisplaySelector; - } - }, [ - showDisplaySelector, - allowDensity, - allowRowHeight, - allowResetButton, - additionalDisplaySettings, - ]); - - const toolbarVisibilityOptions = { - showColumnSelector: toggleColumnSelector, - showSortSelector: showSortSelector, - showDisplaySelector: toggleDisplaySelector, - showKeyboardShortcuts: showKeyboardShortcuts, - showFullScreenSelector: showFullScreenSelector, - }; +import ToolbarVisibilityToggles from './data_grid_toolbar_visibility_toggles'; - let toolbarConfig; - - if (toolbarType === 'object') { - toolbarConfig = toolbarVisibilityOptions; - } else { - toolbarConfig = toolbarType === 'true'; - } - - return ( - data[rowIndex][columnId]} - pagination={{ - ...pagination, - onChangeItemsPerPage: setPageSize, - onChangePage: setPageIndex, - }} - /> - ); -}; - -export default DataGridStyle; - -``` + ## Additional controls in the toolbar @@ -202,7 +28,7 @@ Passing a single node to `additionalControls` will default to being placed in th Although any node is allowed, the recommendation is to use `` for the left-side of the toolbar and `` for the right-side of the toolbar. ```tsx interactive -import React, { useState, useCallback, Fragment } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiDataGrid, EuiDataGridToolbarControl, @@ -219,6 +45,8 @@ import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, + EuiFormRow, + EuiRange, EuiDataGridPaginationProps, RenderCellValue, } from '@elastic/eui'; @@ -257,6 +85,29 @@ for (let i = 1; i < 20; i++) { const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => data[rowIndex][columnId]; +// Some additional custom settings to show in the Display popover +const AdditionalDisplaySettings = () => { + const [exampleSettingValue, setExampleSettingValue] = useState(10); + + return ( + + { + setExampleSettingValue(Number(event.currentTarget.value)); + }} + /> + + ); +}; + export default () => { const [pagination, setPagination] = useState({ pageIndex: 0 }); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); @@ -389,7 +240,7 @@ export default () => { ), }, right: ( - + <> { onClick={() => setIsFlyoutVisible(true)} /> - + ), }, + showDisplaySelector: { + allowResetButton: false, + additionalDisplaySettings: , + }, }} /> {flyout} ); }; - ``` ## Completely custom toolbar rendering @@ -434,19 +288,25 @@ If using multiple datagrid instances across your app, users will typically want ::: ```tsx interactive -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiDataGrid, EuiDataGridSorting, EuiDataGridColumnSortingConfig, EuiDataGridToolbarProps, EuiDataGridToolbarControl, + EuiDataGridStyle, + EuiDataGridStyleBorders, + EuiDataGridDisplaySelectorCustomRender, + EuiSpacer, + EuiHorizontalRule, EuiFormRow, - EuiRange, + EuiButtonGroup, EuiFlexGroup, EuiFlexItem, euiScreenReaderOnly, RenderCellValue, + EuiSwitch, } from '@elastic/eui'; import { css } from '@emotion/react'; import { faker } from '@faker-js/faker'; @@ -492,6 +352,7 @@ const renderCustomToolbar: EuiDataGridToolbarProps['renderCustomToolbar'] = ({ justifyContent="spaceBetween" alignItems="center" css={mobileStyles} + className="euiDataGrid__controls" > {hasRoomForGridControls && ( @@ -521,29 +382,6 @@ const renderCustomToolbar: EuiDataGridToolbarProps['renderCustomToolbar'] = ({ const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => raw_data[rowIndex][columnId]; -// Some additional custom settings to show in the Display popover -const AdditionalDisplaySettings = () => { - const [exampleSettingValue, setExampleSettingValue] = useState(10); - - return ( - - { - setExampleSettingValue(Number(event.currentTarget.value)); - }} - /> - - ); -}; - export default () => { // Column visibility const [visibleColumns, setVisibleColumns] = useState(() => @@ -558,6 +396,51 @@ export default () => { setSortingColumns(sortingColumns); }, []); + // Custom display settings + const [borders, setGridBorders] = useState('none'); + const [rowStripes, setRowStripes] = useState(false); + const gridStyle: EuiDataGridStyle = useMemo( + () => ({ + border: borders, + header: borders === 'none' ? 'underline' : 'shade', + stripes: rowStripes, + }), + [borders, rowStripes] + ); + const customDisplayControls: EuiDataGridDisplaySelectorCustomRender = + useCallback( + ({ densityControl, rowHeightControl }) => { + return ( + <> + + setRowStripes(!rowStripes)} + /> + + {densityControl} + + setGridBorders(id as EuiDataGridStyleBorders)} + /> + + {rowHeightControl} + + ); + }, + [borders, rowStripes] + ); + return ( { sorting={{ columns: sortingColumns, onSort }} rowCount={raw_data.length} renderCellValue={renderCellValue} - gridStyle={{ border: 'none', header: 'underline' }} + gridStyle={gridStyle} renderCustomToolbar={renderCustomToolbar} toolbarVisibility={{ showDisplaySelector: { allowResetButton: false, - additionalDisplaySettings: , + customRender: customDisplayControls, }, }} /> ); }; - ``` -## Toolbar props - -### `EuiDataGridToolBarVisibilityOptions` - -This table contains 6 rows. -| -Prop - - | - -Sample snippet - - | -| --- | --- | -| - -**showSortSelector** - -Allows the ability for the user to sort rows based upon column values - - | - -``` -showSortSelector: false -``` - - | -| - -**additionalControls** - -If passed a `ReactNode`, appends the passed custom control into the left side of the toolbar, after the column & sort controls. Or use **EuiDataGridToolBarAdditionalControlsOptions** to customize the location of your control. - - | - -``` -additionalControls: { - left: , - right: , -} -``` - - | -| - -**showColumnSelector** - -Allows the ability for the user to hide fields and sort columns, boolean or a **EuiDataGridToolBarVisibilityColumnSelectorOptions** - - | - -``` -showColumnSelector: { - allowHide: false; - allowReorder: false; -} -``` - - | -| - -**showDisplaySelector** - -Allows the ability for the user to customize display settings such as grid density and row heights. User changes will override what is provided in **EuiDataGridStyle** and **EuiDataGridRowHeightsOptions** - - | - -``` -showDisplaySelector: { - allowDensity: false; - allowRowHeight: false; - allowResetButton: false; - additionalDisplaySettings: ; -} -``` - - | -| - -**showFullScreenSelector** - -Allows user to be able to fullscreen the data grid. If set to `false` make sure your grid fits within a large enough panel to still show the other controls. - - | - -``` -showFullScreenSelector: false -``` - - | -| - -**showKeyboardShortcuts** - -Displays a popover listing all keyboard controls and shortcuts for the data grid. If set to `false`, the toggle will be visually hidden, but still focusable by keyboard and screen reader users. +## Props - | +import docgen from '@elastic/eui-docgen/dist/components/datagrid/data_grid_types.docgen.json'; - | + + + + + + + + diff --git a/packages/website/docs/components/tabular_content/data_grid/data_grid_toolbar_visibility_toggles.tsx b/packages/website/docs/components/tabular_content/data_grid/data_grid_toolbar_visibility_toggles.tsx new file mode 100644 index 00000000000..68bf8ab292f --- /dev/null +++ b/packages/website/docs/components/tabular_content/data_grid/data_grid_toolbar_visibility_toggles.tsx @@ -0,0 +1,290 @@ +import { useState, useCallback, useMemo } from 'react'; +import { + EuiDataGrid, + EuiAvatar, + EuiFormRow, + EuiRange, + useEuiTheme, +} from '@elastic/eui'; +import { faker } from '@faker-js/faker'; + +import { + ConfigurationDemoWithSnippet, + objectConfigToSnippet, +} from './_grid_configuration_wrapper'; + +const data = Array.from({ length: 5 }).map((_) => ({ + avatar: ( + + ), + name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, + account: faker.finance.accountNumber(), +})); + +const columns = [ + { id: 'avatar', initialWidth: 40 }, + { id: 'name' }, + { id: 'account', schema: 'numeric' }, +]; + +const toolbarBooleanOptions = [ + { id: 'true', label: 'True' }, + { id: 'false', label: 'False' }, +]; +const toolbarBooleanOrObjectOptions = [ + { id: 'true', label: 'True' }, + { id: 'false', label: 'False' }, + { id: 'object', label: 'Object' }, +]; + +export default () => { + const { euiTheme } = useEuiTheme(); + const [showSortSelector, setShowSortSelector] = useState('true'); + const [showDisplaySelector, setShowDisplaySelector] = useState('true'); + const [allowDensity, setAllowDensity] = useState('true'); + const [allowRowHeight, setAllowRowHeight] = useState('true'); + const [allowResetButton, setAllowResetButton] = useState('true'); + const [additionalDisplaySettings, setAdditionalDisplaySettings] = + useState('false'); + const [showColumnSelector, setShowColumnSelector] = useState('true'); + const [allowHideColumns, setAllowHideColumns] = useState('true'); + const [allowOrderingColumns, setAllowOrderingColumns] = useState('true'); + const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState('true'); + const [showFullScreenSelector, setShowFullScreenSelector] = useState('true'); + const [toolbarType, setToolbarType] = useState('true'); + + const [configuration, toolbarVisibility, snippet] = useMemo(() => { + const getConfiguration = () => { + const toolbarVisibility = { + label: 'Toolbar visibility', + options: toolbarBooleanOrObjectOptions, + idSelected: toolbarType, + onChange: setToolbarType, + }; + if (toolbarType !== 'object') return [toolbarVisibility]; + + return [ + toolbarVisibility, + { + label: 'Show column selector', + options: toolbarBooleanOrObjectOptions, + idSelected: showColumnSelector, + onChange: setShowColumnSelector, + nestedConfig: + showColumnSelector === 'object' + ? [ + { + label: 'Allow ordering', + options: toolbarBooleanOptions, + idSelected: allowOrderingColumns, + onChange: setAllowOrderingColumns, + }, + { + label: 'Allow hiding', + options: toolbarBooleanOptions, + idSelected: allowHideColumns, + onChange: setAllowHideColumns, + }, + ] + : undefined, + }, + { + label: 'Show sort selector', + options: toolbarBooleanOptions, + idSelected: showSortSelector, + onChange: setShowSortSelector, + }, + { + label: 'Show display selector', + options: toolbarBooleanOrObjectOptions, + idSelected: showDisplaySelector, + onChange: setShowDisplaySelector, + nestedConfig: + showDisplaySelector === 'object' + ? [ + { + label: 'Show density', + options: toolbarBooleanOptions, + idSelected: allowDensity, + onChange: setAllowDensity, + }, + { + label: 'Show row height', + options: toolbarBooleanOptions, + idSelected: allowRowHeight, + onChange: setAllowRowHeight, + }, + { + label: 'Show reset button', + options: toolbarBooleanOptions, + idSelected: allowResetButton, + onChange: setAllowResetButton, + }, + { + label: 'Additional display settings', + options: toolbarBooleanOptions, + idSelected: additionalDisplaySettings, + onChange: setAdditionalDisplaySettings, + }, + ] + : undefined, + }, + { + label: 'Show keyboard shortcuts', + options: toolbarBooleanOptions, + idSelected: showKeyboardShortcuts, + onChange: setShowKeyboardShortcuts, + }, + { + label: 'Show fullscreen toggle', + options: toolbarBooleanOptions, + idSelected: showFullScreenSelector, + onChange: setShowFullScreenSelector, + }, + ]; + }; + + const getToolbarVisibility = () => { + if (toolbarType === 'true') return true; + if (toolbarType === 'false') return false; + return { + showColumnSelector: + showColumnSelector === 'object' + ? { + allowReorder: allowOrderingColumns === 'true', + allowHide: allowHideColumns === 'true', + } + : showColumnSelector === 'true', + showSortSelector: showSortSelector === 'true', + showDisplaySelector: + showDisplaySelector === 'object' + ? { + allowDensity: allowDensity === 'true', + allowRowHeight: allowRowHeight === 'true', + allowResetButton: allowResetButton === 'true', + additionalDisplaySettings: + additionalDisplaySettings === 'true' ? ( + + + + ) : undefined, + } + : showDisplaySelector === 'true', + showKeyboardShortcuts: showKeyboardShortcuts === 'true', + showFullScreenSelector: showFullScreenSelector === 'true', + }; + }; + + const getSnippet = () => { + let snippet = toolbarType; + + if (toolbarType === 'object') { + const objectConfig = getToolbarVisibility() as any; + + // Workaround for custom ReactNode + if ( + showDisplaySelector === 'object' && + additionalDisplaySettings === 'true' + ) { + objectConfig.showDisplaySelector.additionalDisplaySettings = true; + } + + snippet = objectConfigToSnippet(objectConfig).replace( + 'additionalDisplaySettings: true', + 'additionalDisplaySettings: <>' + ); + } + + return `const toolbarVisibility = ${snippet}; + +`; + }; + + return [getConfiguration(), getToolbarVisibility(), getSnippet()]; + }, [ + toolbarType, + showColumnSelector, + allowOrderingColumns, + allowHideColumns, + showSortSelector, + showDisplaySelector, + allowDensity, + allowRowHeight, + allowResetButton, + additionalDisplaySettings, + showKeyboardShortcuts, + showFullScreenSelector, + ]); + + // Required data grid state + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + const handleVisibleColumns = (visibleColumns) => + setVisibleColumns(visibleColumns); + + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const setPageIndex = useCallback((pageIndex) => { + setPagination((pagination) => ({ ...pagination, pageIndex })); + }, []); + const setPageSize = useCallback((pageSize) => { + setPagination((pagination) => ({ + ...pagination, + pageSize, + pageIndex: 0, + })); + }, []); + + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (sortingColumns) => setSortingColumns(sortingColumns), + [setSortingColumns] + ); + + return ( + + data[rowIndex][columnId]} + rowCount={data.length} + height="auto" + toolbarVisibility={toolbarVisibility} + /> + + ); +}; diff --git a/packages/website/docs/components/tabular_content/data_grid_advanced.mdx b/packages/website/docs/components/tabular_content/data_grid_advanced.mdx deleted file mode 100644 index 54237e9fc8b..00000000000 --- a/packages/website/docs/components/tabular_content/data_grid_advanced.mdx +++ /dev/null @@ -1,1187 +0,0 @@ ---- -slug: /tabular-content/data-grid/advanced -id: tabular_content_data_grid_advanced ---- - -# Data grid advanced - -## Ref methods - -For advanced use cases, and particularly for data grids that manage associated modals/flyouts and need to manually control their grid cell popovers & focus states, we expose certain internal methods via the `ref` prop of **EuiDataGrid**. These methods are: - -* `setIsFullScreen(isFullScreen)` - controls the fullscreen state of the data grid. Accepts a true/false boolean flag. -* `setFocusedCell({ rowIndex, colIndex })` - focuses the specified cell in the grid. - * Using this method is an **accessibility requirement** if your data grid toggles a modal or flyout. Your modal or flyout should restore focus into the grid on close to prevent keyboard or screen reader users from being stranded. -* `openCellPopover({ rowIndex, colIndex })` - opens the specified cell's popover contents. -* `closeCellPopover()` - closes any currently open cell popover. - - -:::note Handling cell location - -When using `setFocusedCell` or `openCellPopover`, keep in mind: - -* `colIndex` is affected by the user reordering or hiding columns. -* If the passed cell indices are outside the data grid's total row count or visible column count, an error will be thrown. -* If the data grid is paginated or sorted, the grid will handle automatically finding specified row index's correct location for you. - -::: - -### react-window methods - -`EuiDataGrid` also exposes several underlying methods from [react-window's `VariableSizeGrid` imperative API](https://react-window.vercel.app/#/api/VariableSizeGrid) via its `ref`: - -* `scrollTo({ scrollLeft: number, scrollTop: number })` - scrolls the grid to the specified horizontal and vertical pixel offsets. - -* `scrollToItem({ align: string = "auto", columnIndex?: number, rowIndex?: number })` - scrolls the grid to the specified row and columns indices - - -:::note react-window vs. EUI - -Unlike EUI's ref APIs, `rowIndex` here refers to the **visible** `rowIndex` when passed to a method of a native `react-window` API. - -For example: `scrollToItem({ rowIndex: 50, columnIndex: 0 })` will always scroll to 51st visible row on the currently visible page, regardless of the content in the cell. In contrast, `setFocusedCell({ rowIndex: 50, colIndex: 0 })` will scroll to the 51st row in your data, which may not be the 51st visible row in the grid if it is paginated or sorted. - -::: - -The below example shows how to use the internal APIs for a data grid that opens a modal via cell actions, that scroll to specific cells, and that can be put into full-screen mode. - -```tsx interactive -import React, { useCallback, useMemo, useState, useRef } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFormRow, - EuiFieldNumber, - EuiButton, - EuiDataGrid, - EuiDataGridRefProps, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiText, - EuiDataGridColumnCellAction, - EuiDataGridColumnSortingConfig, - EuiDataGridPaginationProps, - EuiDataGridSorting, - RenderCellValue, -} from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const raw_data: Array<{ [key: string]: string }> = []; -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()}`, - email: faker.internet.email(), - location: `${faker.location.city()}, ${faker.location.country()}`, - account: faker.finance.accountNumber(), - date: `${faker.date.past()}`, - }); -} - -const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => - raw_data[rowIndex][columnId]; - -export default () => { - const dataGridRef = useRef(null); - - // Modal - const [isModalVisible, setIsModalVisible] = useState(false); - const [lastFocusedCell, setLastFocusedCell] = useState({ - rowIndex: 0, - colIndex: 0, - }); - - const closeModal = useCallback(() => { - setIsModalVisible(false); - dataGridRef.current!.setFocusedCell(lastFocusedCell); // Set the data grid focus back to the cell that opened the modal - }, [lastFocusedCell]); - - const showModal = useCallback( - ({ rowIndex, colIndex }: { rowIndex: number; colIndex: number }) => { - setIsModalVisible(true); - dataGridRef.current!.closeCellPopover(); // Close any open cell popovers - setLastFocusedCell({ rowIndex, colIndex }); // Store the cell that opened this modal - }, - [] - ); - - const openModalAction = useCallback( - ({ Component, rowIndex, colIndex }) => { - return ( - showModal({ rowIndex, colIndex })} - iconType="faceHappy" - aria-label="Open modal" - > - Open modal - - ); - }, - [showModal] - ); - - // Columns - const columns = useMemo( - () => [ - { - id: 'name', - displayAsText: 'Name', - cellActions: [openModalAction], - }, - { - id: 'email', - displayAsText: 'Email address', - initialWidth: 130, - cellActions: [openModalAction], - }, - { - id: 'location', - displayAsText: 'Location', - cellActions: [openModalAction], - }, - { - id: 'account', - displayAsText: 'Account', - cellActions: [openModalAction], - }, - { - id: 'date', - displayAsText: 'Date', - cellActions: [openModalAction], - }, - ], - [openModalAction] - ); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState(() => - columns.map(({ id }) => id) - ); - - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const onChangePage = useCallback( - (pageIndex) => { - setPagination((pagination) => ({ ...pagination, pageIndex })); - }, - [] - ); - const onChangePageSize = useCallback< - EuiDataGridPaginationProps['onChangeItemsPerPage'] - >((pageSize) => { - setPagination((pagination) => ({ ...pagination, pageSize })); - }, []); - - // Sorting - const [sortingColumns, setSortingColumns] = useState< - EuiDataGridColumnSortingConfig[] - >([]); - const onSort = useCallback((sortingColumns) => { - setSortingColumns(sortingColumns); - }, []); - - // Manual cell focus - const [rowIndexAction, setRowIndexAction] = useState(0); - const [colIndexAction, setColIndexAction] = useState(0); - - return ( - <> - - - - setRowIndexAction(Number(e.target.value))} - compressed - /> - - - - - setColIndexAction(Number(e.target.value))} - compressed - /> - - - - - dataGridRef.current!.setFocusedCell({ - rowIndex: rowIndexAction, - colIndex: colIndexAction, - }) - } - > - Set cell focus - - - - - dataGridRef.current!.scrollToItem?.({ - rowIndex: rowIndexAction, - columnIndex: colIndexAction, - align: 'center', - }) - } - > - Scroll to cell - - - - - dataGridRef.current!.openCellPopover({ - rowIndex: rowIndexAction, - colIndex: colIndexAction, - }) - } - > - Open cell popover - - - - dataGridRef.current!.setIsFullScreen(true)} - > - Set grid to fullscreen - - - - - - - {isModalVisible && ( - - - Example modal - - - - -

- When closed, this modal should re-focus into the cell that - toggled it. -

-
-
- - - - Close - - -
- )} - - ); -}; - -``` - -## Data grid in-memory - -:::note What is the difference in the examples? - -These examples show the same grid built with the four available `inMemory` settings. While they may look the same, look at the source to see how they require different levels of data management in regards to sorting and pagination. - -::: - -The grid has levels of **in-memory** settings that can be set. It is in the consuming application's best interest to put as much of the data grid in memory as performance allows. Try to use the highest level `inMemory="sorting"` whenever possible. The following values are available. - -* **undefined (default)**: When not in use the grid will not autodetect schemas. The sorting and pagination is the responsibility of the consuming application. -* **enhancements**: Provides no in-memory operations. If set, the grid will try to autodetect schemas only based on the content currently available (the current page of data). -* **pagination**: Schema detection works as above and pagination is performed in-memory. The pagination callbacks are still triggered on user interactions, but the row updates are performed by the grid. -* **sorting (suggested)**: Schema detection and pagination are performed as above, and sorting is applied in-memory too. The onSort callback is still called and the application must own the column sort state, but data sorting is done by the grid based on the defined and/or detected schemas. - -When enabled, **in-memory** renders cell data off-screen and uses those values to detect schemas and perform sorting. This detaches the user experience from the raw data; the data grid never has access to the backing data, only what is returned by `renderCellValue`. - -## When in-memory is not used - -When `inMemory` is not in use the grid will not autodetect schemas. In the below example only the `amount` column has a schema because it is manually set. Sorting and pagination data management is the responsibility of the consuming application. Column sorting in particular is going to be imprecise because there is no backend service to call, and data grid instead defaults to naively applying JavaScript's default array sort which doesn't work well with numeric data and doesn't sort React elements such as the links. This is a good example of what happens when you **don't** utilize schemas for complex data. - -```tsx interactive -import React, { Fragment, useCallback, useMemo, useState } from 'react'; -import { EuiDataGrid, EuiLink } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'location', - }, - { - id: 'account', - }, - { - id: 'date', - }, - { - id: 'amount', - schema: 'currency', - }, - { - id: 'phone', - }, - { - id: 'version', - }, -]; - -const raw_data = []; - -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: {faker.internet.email()}, - location: ( - - {`${faker.location.city()}, `} - {faker.location.country()} - - ), - date: `${faker.date.past()}`, - account: faker.finance.accountNumber(), - amount: faker.commerce.price(), - phone: faker.phone.number(), - version: faker.system.semver(), - }); -} - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); - const onChangeItemsPerPage = useCallback( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Sorting - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - // Sort data - let data = useMemo(() => { - return [...raw_data].sort((a, b) => { - for (let i = 0; i < sortingColumns.length; i++) { - const column = sortingColumns[i]; - const aValue = a[column.id]; - const bValue = b[column.id]; - - if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; - if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; - } - - return 0; - }); - }, [sortingColumns]); - - // Pagination - data = useMemo(() => { - const rowStart = pagination.pageIndex * pagination.pageSize; - const rowEnd = Math.min(rowStart + pagination.pageSize, data.length); - return data.slice(rowStart, rowEnd); - }, [data, pagination]); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }) => { - let adjustedRowIndex = rowIndex; - - // If we are doing the pagination (instead of leaving that to the grid) - // then the row index must be adjusted as `data` has already been pruned to the page size - adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - return data.hasOwnProperty(adjustedRowIndex) - ? data[adjustedRowIndex][columnId] - : null; - }; - }, [data, pagination.pageIndex, pagination.pageSize]); - - return ( - - ); -}; - -``` - -## Enhancements only in-memory - -With `inMemory={{ level: 'enhancements' }}` the grid will now autodetect schemas based on the content it has available on the currently viewed page. Notice that the field list under Sort fields has detected the type of data each column contains. - -```tsx interactive -import React, { Fragment, useCallback, useMemo, useState } from 'react'; -import { EuiDataGrid, EuiLink } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'location', - }, - { - id: 'account', - }, - { - id: 'date', - }, - { - id: 'amount', - }, - { - id: 'phone', - }, - { - id: 'version', - }, -]; - -const raw_data = []; - -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: {faker.internet.email()}, - location: ( - - {`${faker.location.city()}, `} - {faker.location.country()} - - ), - date: `${faker.date.past()}`, - account: faker.finance.accountNumber(), - amount: faker.commerce.price(), - phone: faker.phone.number(), - version: faker.system.semver(), - }); -} - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); - const onChangeItemsPerPage = useCallback( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Sorting - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - // Sort data - let data = useMemo(() => { - // the grid itself is responsible for sorting if inMemory is `sorting` - - return [...raw_data].sort((a, b) => { - for (let i = 0; i < sortingColumns.length; i++) { - const column = sortingColumns[i]; - const aValue = a[column.id]; - const bValue = b[column.id]; - - if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; - if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; - } - - return 0; - }); - }, [sortingColumns]); - - // Pagination - data = useMemo(() => { - const rowStart = pagination.pageIndex * pagination.pageSize; - const rowEnd = Math.min(rowStart + pagination.pageSize, data.length); - return data.slice(rowStart, rowEnd); - }, [data, pagination]); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }) => { - // Because inMemory is not set for pagination, we need to manage it - // The row index must be adjusted as `data` has already been pruned to the page size - const adjustedRowIndex = - rowIndex - pagination.pageIndex * pagination.pageSize; - - return data.hasOwnProperty(adjustedRowIndex) - ? data[adjustedRowIndex][columnId] - : null; - }; - }, [data, pagination.pageIndex, pagination.pageSize]); - - return ( -
- -
- ); -}; - -``` - -## Pagination only in-memory - -With `inMemory={{ level: 'pagination' }}` the grid will now take care of managing the data cleanup for pagination. Like before it will autodetect schemas when possible. - -```tsx interactive -import React, { Fragment, useCallback, useMemo, useState } from 'react'; -import { EuiDataGrid, EuiLink } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'location', - }, - { - id: 'account', - }, - { - id: 'date', - }, - { - id: 'amount', - }, - { - id: 'phone', - }, - { - id: 'version', - }, -]; - -const raw_data = []; - -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: {faker.internet.email()}, - location: ( - - {`${faker.location.city()}, `} - {faker.location.country()} - - ), - date: `${faker.date.past()}`, - account: faker.finance.accountNumber(), - amount: faker.commerce.price(), - phone: faker.phone.number(), - version: faker.system.semver(), - }); -} - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const onChangeItemsPerPage = useCallback( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Sorting - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - // Because inMemory's level is set to `pagination` we still need to sort the data, but no longer need to chunk it for pagination - const data = useMemo(() => { - return [...raw_data].sort((a, b) => { - for (let i = 0; i < sortingColumns.length; i++) { - const column = sortingColumns[i]; - const aValue = a[column.id]; - const bValue = b[column.id]; - - if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; - if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; - } - - return 0; - }); - }, [sortingColumns]); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }) => { - return data.hasOwnProperty(rowIndex) ? data[rowIndex][columnId] : null; - }; - }, [data]); - - return ( - - ); -}; - -``` - -## Sorting and pagination in-memory - -With `inMemory={{ level: 'sorting' }}` the grid will now take care of managing the data cleanup for sorting as well as pagination. Like before it will autodetect schemas when possible. - -```tsx interactive -import React, { Fragment, useCallback, useMemo, useState } from 'react'; -import { EuiDataGrid, EuiLink } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'location', - }, - { - id: 'account', - }, - { - id: 'date', - }, - { - id: 'amount', - }, - { - id: 'phone', - }, - { - id: 'version', - }, -]; - -const raw_data = []; - -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: {faker.internet.email()}, - location: ( - - {`${faker.location.city()}, `} - {faker.location.country()} - - ), - date: `${faker.date.past()}`, - account: faker.finance.accountNumber(), - amount: faker.commerce.price(), - phone: faker.phone.number(), - version: faker.system.semver(), - }); -} - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const onChangeItemsPerPage = useCallback( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Sorting - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }) => { - return raw_data.hasOwnProperty(rowIndex) - ? raw_data[rowIndex][columnId] - : null; - }; - }, []); - - return ( - - ); -}; - -``` - -## Custom body renderer - -For **extremely** advanced use cases, the `renderCustomGridBody` prop may be used to take complete control over rendering the grid body. This may be useful for scenarios where the default [virtualized](/docs/tabular-content/data-grid#virtualization) rendering is not desired, or where custom row layouts (e.g., the conditional row details cell below) are required. - -Please note that this prop is meant to be an **escape hatch**, and should only be used if you know exactly what you are doing. Once a custom renderer is used, you are in charge of ensuring the grid has all the correct semantic and aria labels required by the [data grid spec](https://www.w3.org/WAI/ARIA/apg/patterns/grid), and that keyboard focus and navigation still work in an accessible manner. - -```tsx interactive -import React, { useEffect, useCallback, useState, useRef } from 'react'; -import { - EuiDataGrid, - EuiDataGridProps, - EuiDataGridCustomBodyProps, - EuiDataGridColumnCellActionProps, - EuiScreenReaderOnly, - EuiCheckbox, - EuiButtonIcon, - EuiIcon, - EuiFlexGroup, - EuiSwitch, - EuiSpacer, - useEuiTheme, - logicalCSS, - EuiDataGridPaginationProps, - EuiDataGridSorting, - EuiDataGridColumnSortingConfig, - RenderCellValue, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import { faker } from '@faker-js/faker'; - -const raw_data: Array<{ [key: string]: string }> = []; -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()}`, - email: faker.internet.email(), - location: `${faker.location.city()}, ${faker.location.country()}`, - date: `${faker.date.past()}`, - amount: faker.commerce.price({ min: 1, max: 1000, dec: 2, symbol: '$' }), - }); -} - -const columns = [ - { - id: 'name', - displayAsText: 'Name', - cellActions: [ - ({ Component }: EuiDataGridColumnCellActionProps) => ( - alert('action')} - iconType="faceHappy" - aria-label="Some action" - > - Some action - - ), - ], - }, - { - id: 'email', - displayAsText: 'Email address', - initialWidth: 130, - }, - { - id: 'location', - displayAsText: 'Location', - }, - { - id: 'date', - displayAsText: 'Date', - }, - { - id: 'amount', - displayAsText: 'Amount', - }, -]; - -const checkboxRowCellRender: RenderCellValue = ({ rowIndex }) => ( - {}} - /> -); - -const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [ - { - id: 'selection', - width: 32, - headerCellRender: () => ( - {}} - /> - ), - rowCellRender: checkboxRowCellRender, - }, -]; - -const trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = [ - { - id: 'actions', - width: 40, - headerCellRender: () => ( - - Actions - - ), - rowCellRender: () => ( - - ), - }, -]; - -const RowCellRender: RenderCellValue = ({ setCellProps, rowIndex }) => { - setCellProps({ style: { width: '100%', height: 'auto' } }); - - const firstName = raw_data[rowIndex].name.split(', ')[1]; - const isGood = faker.datatype.boolean(); - return ( - <> - {firstName}'s account has {isGood ? 'no' : ''} outstanding fees.{' '} - - - ); -}; - -// The custom row details is actually a trailing control column cell with -// a hidden header. This is important for accessibility and markup reasons -// @see https://fuschia-stretch.glitch.me/ for more -const rowDetails: EuiDataGridProps['trailingControlColumns'] = [ - { - id: 'row-details', - - // The header cell should be visually hidden, but available to screen readers - width: 0, - headerCellRender: () => <>Row details, - headerCellProps: { className: 'euiScreenReaderOnly' }, - - // The footer cell can be hidden to both visual & SR users, as it does not contain meaningful information - footerCellProps: { style: { display: 'none' } }, - - // When rendering this custom cell, we'll want to override - // the automatic width/heights calculated by EuiDataGrid - rowCellRender: RowCellRender, - }, -]; - -const footerCellValues: { [key: string]: string } = { - amount: `Total: ${raw_data - .reduce((acc, { amount }) => acc + Number(amount.split('$')[1]), 0) - .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`, -}; - -const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => - raw_data[rowIndex][columnId]; - -const RenderFooterCellValue: RenderCellValue = ({ columnId, setCellProps }) => { - const value = footerCellValues[columnId]; - - useEffect(() => { - // Turn off the cell expansion button if the footer cell is empty - if (!value) setCellProps({ isExpandable: false }); - }, [value, setCellProps, columnId]); - - return value || null; -}; - -export default () => { - const [autoHeight, setAutoHeight] = useState(true); - const [showRowDetails, setShowRowDetails] = useState(false); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState(() => - columns.map(({ id }) => id) - ); - - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const onChangePage = useCallback( - (pageIndex) => { - setPagination((pagination) => ({ ...pagination, pageIndex })); - }, - [] - ); - const onChangePageSize = useCallback< - EuiDataGridPaginationProps['onChangeItemsPerPage'] - >((pageSize) => { - setPagination((pagination) => ({ ...pagination, pageSize })); - }, []); - - // Sorting - const [sortingColumns, setSortingColumns] = useState< - EuiDataGridColumnSortingConfig[] - >([]); - const onSort = useCallback((sortingColumns) => { - setSortingColumns(sortingColumns); - }, []); - - const { euiTheme } = useEuiTheme(); - - // Custom grid body renderer - const RenderCustomGridBody = useCallback( - ({ - Cell, - visibleColumns, - visibleRowData, - setCustomGridBodyProps, - }: EuiDataGridCustomBodyProps) => { - // Ensure we're displaying correctly-paginated rows - const visibleRows = raw_data.slice( - visibleRowData.startRow, - visibleRowData.endRow - ); - - // Add styling needed for custom grid body rows - const styles = { - row: css` - ${logicalCSS('width', 'fit-content')}; - ${logicalCSS('border-bottom', euiTheme.border.thin)}; - background-color: ${euiTheme.colors.emptyShade}; - `, - rowCellsWrapper: css` - display: flex; - `, - rowDetailsWrapper: css` - text-align: center; - background-color: ${euiTheme.colors.body}; - `, - }; - - // Set custom props onto the grid body wrapper - const bodyRef = useRef(null); - useEffect(() => { - setCustomGridBodyProps({ - ref: bodyRef, - onScroll: () => - console.debug('scrollTop:', bodyRef.current?.scrollTop), - }); - }, [setCustomGridBodyProps]); - - return ( - <> - {visibleRows.map((row, rowIndex) => ( -
-
- {visibleColumns.map((column, colIndex) => { - // Skip the row details cell - we'll render it manually outside of the flex wrapper - if (column.id !== 'row-details') { - return ( - - ); - } - })} -
- {showRowDetails && ( -
- -
- )} -
- ))} - - ); - }, - [showRowDetails, euiTheme] - ); - - return ( - <> - - setAutoHeight(!autoHeight)} - /> - setShowRowDetails(!showRowDetails)} - /> - - - - - ); -}; - -``` diff --git a/packages/website/docs/components/tabular_content/data_grid_style_and_display.mdx b/packages/website/docs/components/tabular_content/data_grid_style_and_display.mdx deleted file mode 100644 index a8c95f72651..00000000000 --- a/packages/website/docs/components/tabular_content/data_grid_style_and_display.mdx +++ /dev/null @@ -1,1157 +0,0 @@ ---- -slug: /tabular-content/data-grid/style-and-display -id: tabular_content_data_grid_style_display ---- - -# Data grid style & display - -## Grid style - -Styling can be passed down to the grid through the `gridStyle` prop. It accepts an object that allows for customization. - -With the default settings, the `showDisplaySelector.allowDensity` setting in `toolbarVisibility` means the user has the ability to override the padding and font size passed into `gridStyle` by the engineer. The font size overriding only works with text or elements that can inherit the parent font size or elements that use units relative to the parent container. - -```tsx interactive -import React, { useState, useCallback } from 'react'; -import { EuiDataGrid, EuiAvatar } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { - id: 'avatar', - initialWidth: 40, - }, - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'city', - }, - { - id: 'country', - }, - { - id: 'account', - }, -]; - -const data = []; - -for (let i = 1; i < 6; i++) { - data.push({ - avatar: ( - - ), - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: faker.internet.email(), - city: faker.location.city(), - country: faker.location.country(), - account: faker.finance.accountNumber(), - }); -} - -const footerCellValues = { - name: '5 accounts', -}; - -const renderFooterCellValue = ({ columnId }) => - footerCellValues[columnId] || null; - -const DataGridStyle = ({ - border = 'none', - fontSize = 'm', - cellPadding = 'm', - stripes = true, - rowHover = 'highlight', - header = 'underline', - footer = 'overline', -}) => { - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const setPageIndex = useCallback((pageIndex) => { - setPagination((pagination) => ({ ...pagination, pageIndex })); - }, []); - - const setPageSize = useCallback((pageSize) => { - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })); - }, []); - - const handleVisibleColumns = (visibleColumns) => - setVisibleColumns(visibleColumns); - - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback( - (sortingColumns) => setSortingColumns(sortingColumns), - [setSortingColumns] - ); - - return ( - data[rowIndex][columnId]} - renderFooterCellValue={renderFooterCellValue} - pagination={{ - ...pagination, - onChangeItemsPerPage: setPageSize, - onChangePage: setPageIndex, - }} - /> - ); -}; - -export default DataGridStyle; - -``` - -## Grid row classes - -Specific rows can be highlighted or otherwise have custom styling passed to them via the`gridStyle.rowClasses` prop. It accepts an object associating the row's index with a class name string. - -The example below sets a custom striped class on the 3rd row and dynamically updates the `rowClasses` map when rows are selected. - -```tsx interactive -import React, { - createContext, - useContext, - useReducer, - useState, - useMemo, -} from 'react'; -import { EuiDataGrid, EuiCheckbox, EuiButtonEmpty } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -/** - * Data - */ -const columns = [ - { id: 'name' }, - { id: 'email' }, - { id: 'city' }, - { id: 'country' }, - { id: 'account' }, -]; - -const DEMO_ROW = 2; - -const data = []; -for (let i = 1; i <= 10; i++) { - data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: faker.internet.email(), - city: faker.location.city(), - country: faker.location.country(), - account: faker.finance.accountNumber(), - }); -} -data[DEMO_ROW].account = 'OVERDUE'; - -/** - * Selection - */ -const SelectionContext = createContext(); - -const SelectionButton = () => { - const [selectedRows] = useContext(SelectionContext); - const hasSelection = selectedRows.size > 0; - return hasSelection ? ( - window.alert('This is not a real control.')} - > - {selectedRows.size} {selectedRows.size > 1 ? 'items' : 'item'} selected - - ) : null; -}; - -const SelectionHeaderCell = () => { - const [selectedRows, updateSelectedRows] = useContext(SelectionContext); - const isIndeterminate = - selectedRows.size > 0 && selectedRows.size < data.length; - return ( - 0} - onChange={(e) => { - if (isIndeterminate) { - // clear selection - updateSelectedRows({ action: 'clear' }); - } else { - if (e.target.checked) { - // select everything - updateSelectedRows({ action: 'selectAll' }); - } else { - // clear selection - updateSelectedRows({ action: 'clear' }); - } - } - }} - /> - ); -}; - -const SelectionRowCell = ({ rowIndex }) => { - const [selectedRows, updateSelectedRows] = useContext(SelectionContext); - const isChecked = selectedRows.has(rowIndex); - return ( - <> - { - if (e.target.checked) { - updateSelectedRows({ action: 'add', rowIndex }); - } else { - updateSelectedRows({ action: 'delete', rowIndex }); - } - }} - /> - - ); -}; - -const leadingControlColumns = [ - { - id: 'selection', - width: 32, - headerCellRender: SelectionHeaderCell, - rowCellRender: SelectionRowCell, - }, -]; - -/** - * Data grid - */ -export default () => { - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - const rowSelection = useReducer((rowSelection, { action, rowIndex }) => { - if (action === 'add') { - const nextRowSelection = new Set(rowSelection); - nextRowSelection.add(rowIndex); - return nextRowSelection; - } else if (action === 'delete') { - const nextRowSelection = new Set(rowSelection); - nextRowSelection.delete(rowIndex); - return nextRowSelection; - } else if (action === 'clear') { - return new Set(); - } else if (action === 'selectAll') { - return new Set(data.map((_, index) => index)); - } - return rowSelection; - }, new Set()); - - const rowClasses = useMemo(() => { - const rowClasses = { - [DEMO_ROW]: 'euiDataGridRow--rowClassesDemo', - }; - rowSelection[0].forEach((rowIndex) => { - rowClasses[rowIndex] = 'euiDataGridRow--rowClassesDemoSelected'; - }); - return rowClasses; - }, [rowSelection]); - - return ( - - data[rowIndex][columnId]} - leadingControlColumns={leadingControlColumns} - toolbarVisibility={{ - additionalControls: , - }} - gridStyle={{ rowClasses, rowHover: 'none' }} - /> - - ); -}; - -``` - -## Row heights options - -By default, all rows get a height of **34 pixels**, but there are scenarios where you might want to adjust the height to fit more content. To do that, you can pass an object to the `rowHeightsOptions` prop. This object accepts the following properties: - -* `defaultHeight` - * Defines the default size for all rows - * Can be configured with an exact pixel height, a line count, or `"auto"` to fit all content -* `rowHeights` - * Overrides the height for a specific row - * Can be configured with an exact pixel height, a line count, or `"auto"` to fit all content -* `lineHeight` - * Sets a default line height for all cells, which is used to calculate row height - * Accepts any value that the `line-height` CSS property normally takes (e.g. px, ems, rems, or unitless) -* `onChange` - * Optional callback when the user changes the data grid's internal `rowHeightsOptions` (e.g., via the toolbar display selector). - * Can be used to store and preserve user display preferences on page refresh - see this [data grid styling and control example](/docs/tabular-content/data-grid-style-display#adjusting-your-grid-to-usertoolbar-changes). -* `scrollAnchorRow` - * Optional indicator of the row that should be used as an anchor for vertical layout shift compensation. - * Can be set to the default `undefined`,`"start"`, or`"center"`. - * If set to `"start"`, the topmost visible row will monitor for unexpected changes to its vertical position and try to compensate for these by scrolling the grid scroll container such that the topmost row position remains stable. - * If set to `"center"`, the middle visible row will monitor for unexpected changes to its vertical position and try to compensate for these by scrolling the grid scroll container such that the middle row position remains stable. - * This is particularly useful when the grid contains`auto` sized rows. Since these rows are measured as they appear in the overscan, they can cause surprising shifts of the vertical position of all following rows when their measured height is different from the estimated height. - -:::warning Rows have minimum height requirements - -Rows must be at least **34 pixels** tall so they can render at least one line of text. If you provide a smaller height the row will default to **34 pixels**. - -::: - -## Setting a default height and line height for rows - -You can change the default height for all rows via the `defaultHeight` property. Note that the `showDisplaySelector.allowRowHeight` setting in `toolbarVisibility` means the user has the ability to override this default height. Users will be able to toggle between single rows, a configurable line count, or `"auto"`. - -You can also customize the line height of all cells with the `lineHeight` property. However, if you wrap your cell content with CSS that overrides/sets line-height (e.g. in an `EuiText`), your row heights will not be calculated correctly - make sure to match the passed `lineHeight` property to the actual cell content line height. - -```tsx interactive -import React, { - useCallback, - useState, - createContext, - useContext, - useMemo, - ReactNode, -} from 'react'; -import { - RenderCellValue as RenderCellValueType, - EuiDataGrid, - EuiLink, - EuiAvatar, - EuiBadge, - EuiMarkdownFormat, - EuiText, - EuiSpacer, - formatDate, - EuiDataGridSorting, - EuiDataGridColumnSortingConfig, - EuiDataGridPaginationProps, -} from '@elastic/eui'; -import githubData from '../_row_auto_height_data.json'; - -interface DataShape { - html_url: string; - title: string; - user: { - login: string; - avatar_url: string; - }; - labels: Array<{ - name: string; - color: string; - }>; - comments: number; - created_at: string; - body: null | string; -} - -type DataContextShape = - | undefined - | { - data: DataShape[]; - }; -const DataContext = createContext(undefined); - -const columns = [ - { - id: 'index', - displayAsText: 'Index', - isExpandable: false, - initialWidth: 80, - }, - { - id: 'issue', - displayAsText: 'Issue', - isExpandable: false, - }, - { - id: 'body', - displayAsText: 'Description', - }, -]; - -// it is expensive to compute 10000 rows of fake data -// instead of loading up front, generate entries on the fly -const raw_data: DataShape[] = githubData; - -const RenderCellValue: RenderCellValueType = ({ - rowIndex, - columnId, - isDetails, -}) => { - const { data } = useContext(DataContext)!; - - const item = data[rowIndex]; - let content: ReactNode = ''; - - if (columnId === 'index') { - content = <>{rowIndex}; - } else if (columnId === 'issue') { - content = ( - <> - -

- - {item.title} - - {' '} - {item.labels.map(({ name, color }) => ( - - {name} - - ))} -

-
- - - - - - Opened by{' '} - {' '} - {item.user.login} on{' '} - {formatDate(new Date(item.created_at), 'dobLong')} - - - - - - {item.comments === 1 && ( - - {`${item.comments} comment`} - - )} - - {item.comments >= 2 && ( - - {`${item.comments} comments`} - - )} - - ); - } else if (columnId === 'body') { - if (isDetails) { - // expanded in a popover - content = {item.body ?? ''}; - } else { - // a full issue description is a *lot* to shove into a cell - content = ( - - {(item.body ?? '').slice(0, 300)} - - ); - } - } - - return content; -}; - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); - - // Sorting - const [sortingColumns, setSortingColumns] = useState< - EuiDataGridColumnSortingConfig[] - >([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - const onChangeItemsPerPage = useCallback< - EuiDataGridPaginationProps['onChangeItemsPerPage'] - >( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - // matches the snippet example - const rowHeightsOptions = useMemo( - () => ({ - defaultHeight: 'auto' as const, - }), - [] - ); - - const dataContext = useMemo( - () => ({ - data: raw_data, - }), - [] - ); - - return ( - - - - ); -}; - -``` - -## Overriding specific row heights - -You can override the default height of a specific row by passing a`rowHeights` object associating the row's index with a specific height configuration. - -:::warning Disabling the row height toolbar control - -Individual row heights will be overridden by the toolbar display controls. If you do not want users to be able to override specific row heights, set `toolbarVisibility.showDisplaySelector.allowRowHeight` to `false`. - -::: - -```tsx interactive -import React, { - useCallback, - useState, - createContext, - useContext, - useMemo, - ReactNode, -} from 'react'; -import { - RenderCellValue as RenderCellValueType, - EuiDataGrid, - EuiLink, - EuiAvatar, - EuiBadge, - EuiMarkdownFormat, - EuiText, - EuiSpacer, - formatDate, - EuiDataGridSorting, - EuiDataGridColumnSortingConfig, - EuiDataGridPaginationProps, -} from '@elastic/eui'; -import githubData from '../_row_auto_height_data.json'; - -interface DataShape { - html_url: string; - title: string; - user: { - login: string; - avatar_url: string; - }; - labels: Array<{ - name: string; - color: string; - }>; - comments: number; - created_at: string; - body: null | string; -} - -type DataContextShape = - | undefined - | { - data: DataShape[]; - }; -const DataContext = createContext(undefined); - -const columns = [ - { - id: 'index', - displayAsText: 'Index', - isExpandable: false, - initialWidth: 80, - }, - { - id: 'issue', - displayAsText: 'Issue', - isExpandable: false, - }, - { - id: 'body', - displayAsText: 'Description', - }, -]; - -// it is expensive to compute 10000 rows of fake data -// instead of loading up front, generate entries on the fly -const raw_data: DataShape[] = githubData; - -const RenderCellValue: RenderCellValueType = ({ - rowIndex, - columnId, - isDetails, -}) => { - const { data } = useContext(DataContext)!; - - const item = data[rowIndex]; - let content: ReactNode = ''; - - if (columnId === 'index') { - content = <>{rowIndex}; - } else if (columnId === 'issue') { - content = ( - <> - -

- - {item.title} - - {' '} - {item.labels.map(({ name, color }) => ( - - {name} - - ))} -

-
- - - - - - Opened by{' '} - {' '} - {item.user.login} on{' '} - {formatDate(new Date(item.created_at), 'dobLong')} - - - - - - {item.comments === 1 && ( - - {`${item.comments} comment`} - - )} - - {item.comments >= 2 && ( - - {`${item.comments} comments`} - - )} - - ); - } else if (columnId === 'body') { - if (isDetails) { - // expanded in a popover - content = {item.body ?? ''}; - } else { - // a full issue description is a *lot* to shove into a cell - content = ( - - {(item.body ?? '').slice(0, 300)} - - ); - } - } - - return content; -}; - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); - - // Sorting - const [sortingColumns, setSortingColumns] = useState< - EuiDataGridColumnSortingConfig[] - >([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - const onChangeItemsPerPage = useCallback< - EuiDataGridPaginationProps['onChangeItemsPerPage'] - >( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - // matches the snippet example - const rowHeightsOptions = useMemo( - () => ({ - defaultHeight: 140, - rowHeights: { - 1: { - lineCount: 5, - }, - 4: 200, - 5: 80, - }, - }), - [] - ); - - const dataContext = useMemo( - () => ({ - data: raw_data, - }), - [] - ); - - return ( - - - - ); -}; - -``` - -## Auto heights for rows - -To enable automatically fitting rows to their content you can set `defaultHeight="auto"`. This ensures every row automatically adjusts its height to fit the contents. - -You can also override the height of a specific row by passing a`rowHeights` object associating the row's index with an `"auto"` value. - -```tsx interactive -import React, { - useCallback, - useState, - createContext, - useContext, - useMemo, - ReactNode, -} from 'react'; -import { - RenderCellValue as RenderCellValueType, - EuiDataGrid, - EuiLink, - EuiAvatar, - EuiBadge, - EuiMarkdownFormat, - EuiText, - EuiSpacer, - formatDate, - EuiDataGridSorting, - EuiDataGridColumnSortingConfig, - EuiDataGridPaginationProps, -} from '@elastic/eui'; -import githubData from '../_row_auto_height_data.json'; - -interface DataShape { - html_url: string; - title: string; - user: { - login: string; - avatar_url: string; - }; - labels: Array<{ - name: string; - color: string; - }>; - comments: number; - created_at: string; - body: null | string; -} - -type DataContextShape = - | undefined - | { - data: DataShape[]; - }; -const DataContext = createContext(undefined); - -const columns = [ - { - id: 'index', - displayAsText: 'Index', - isExpandable: false, - initialWidth: 80, - }, - { - id: 'issue', - displayAsText: 'Issue', - isExpandable: false, - }, - { - id: 'body', - displayAsText: 'Description', - }, -]; - -// it is expensive to compute 10000 rows of fake data -// instead of loading up front, generate entries on the fly -const raw_data: DataShape[] = githubData; - -const RenderCellValue: RenderCellValueType = ({ - rowIndex, - columnId, - isDetails, -}) => { - const { data } = useContext(DataContext)!; - - const item = data[rowIndex]; - let content: ReactNode = ''; - - if (columnId === 'index') { - content = <>{rowIndex}; - } else if (columnId === 'issue') { - content = ( - <> - -

- - {item.title} - - {' '} - {item.labels.map(({ name, color }) => ( - - {name} - - ))} -

-
- - - - - - Opened by{' '} - {' '} - {item.user.login} on{' '} - {formatDate(new Date(item.created_at), 'dobLong')} - - - - - - {item.comments === 1 && ( - - {`${item.comments} comment`} - - )} - - {item.comments >= 2 && ( - - {`${item.comments} comments`} - - )} - - ); - } else if (columnId === 'body') { - if (isDetails) { - // expanded in a popover - content = {item.body ?? ''}; - } else { - // a full issue description is a *lot* to shove into a cell - content = ( - - {(item.body ?? '').slice(0, 300)} - - ); - } - } - - return content; -}; - -export default () => { - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); - - // Sorting - const [sortingColumns, setSortingColumns] = useState< - EuiDataGridColumnSortingConfig[] - >([]); - const onSort = useCallback( - (sortingColumns) => { - setSortingColumns(sortingColumns); - }, - [setSortingColumns] - ); - - const onChangeItemsPerPage = useCallback< - EuiDataGridPaginationProps['onChangeItemsPerPage'] - >( - (pageSize) => - setPagination((pagination) => ({ - ...pagination, - pageSize, - pageIndex: 0, - })), - [setPagination] - ); - - const onChangePage = useCallback( - (pageIndex) => - setPagination((pagination) => ({ ...pagination, pageIndex })), - [setPagination] - ); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - // matches the snippet example - const rowHeightsOptions = useMemo( - () => ({ - defaultHeight: 'auto' as const, - }), - [] - ); - - const dataContext = useMemo( - () => ({ - data: raw_data, - }), - [] - ); - - return ( - - - - ); -}; - -``` - -## Adjusting your grid to user/toolbar changes[](/docs/tabular-content/data-grid-style-display#adjusting-your-grid-to-usertoolbar-changes) - -You can use the optional `gridStyle.onChange` and `rowHeightsOptions.onChange` callbacks to adjust your data grid based on user density or row height changes. - -For example, if the user changes the grid density to compressed, you may want to adjust a cell's content sizing in response. Or you could store user settings in localStorage or other database to preserve display settings on page refresh, like the below example does. - -```tsx interactive -import React, { useState, useCallback, useMemo } from 'react'; -import { EuiDataGrid, EuiIcon } from '@elastic/eui'; -import { faker } from '@faker-js/faker'; - -const columns = [ - { id: 'name' }, - { id: 'email' }, - { id: 'city' }, - { id: 'country' }, - { id: 'account' }, -]; -const data = []; -for (let i = 1; i <= 5; i++) { - data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()} ${faker.person.suffix()}`, - email: faker.internet.email(), - city: faker.location.city(), - country: faker.location.country(), - account: faker.finance.accountNumber(), - }); -} - -const GRID_STYLES_KEY = 'euiDataGridStyles'; -const INITIAL_STYLES = JSON.stringify({ stripes: true }); - -const ROW_HEIGHTS_KEY = 'euiDataGridRowHeightsOptions'; -const INITIAL_ROW_HEIGHTS = JSON.stringify({}); - -export default () => { - const [densitySize, setDensitySize] = useState(''); - const responsiveIcon = useCallback( - () => , - [densitySize] - ); - const responsiveIconWidth = useMemo(() => { - if (densitySize === 'l') return 44; - if (densitySize === 's') return 24; - return 32; - }, [densitySize]); - const leadingControlColumns = useMemo( - () => [ - { - id: 'icon', - width: responsiveIconWidth, - headerCellRender: responsiveIcon, - rowCellRender: responsiveIcon, - }, - ], - [responsiveIcon, responsiveIconWidth] - ); - - const storedRowHeightsOptions = useMemo( - () => - JSON.parse(localStorage.getItem(ROW_HEIGHTS_KEY) || INITIAL_ROW_HEIGHTS), - [] - ); - const storeRowHeightsOptions = useCallback((updatedRowHeights) => { - console.log(updatedRowHeights); - localStorage.setItem(ROW_HEIGHTS_KEY, JSON.stringify(updatedRowHeights)); - }, []); - - const storedGridStyles = useMemo( - () => JSON.parse(localStorage.getItem(GRID_STYLES_KEY) || INITIAL_STYLES), - [] - ); - const storeGridStyles = useCallback((updatedStyles) => { - console.log(updatedStyles); - localStorage.setItem(GRID_STYLES_KEY, JSON.stringify(updatedStyles)); - setDensitySize(updatedStyles.fontSize); - }, []); - - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); - - return ( - data[rowIndex][columnId]} - /> - ); -}; - -``` diff --git a/packages/website/docs/components/tabular_content/tables/_category_.yml b/packages/website/docs/components/tabular_content/tables/_category_.yml new file mode 100644 index 00000000000..39d5e5dc902 --- /dev/null +++ b/packages/website/docs/components/tabular_content/tables/_category_.yml @@ -0,0 +1,5 @@ +position: 1 +collapsed: true +link: + type: doc + id: tabular_content_tables diff --git a/packages/website/docs/components/tabular_content/tables.mdx b/packages/website/docs/components/tabular_content/tables/basic_tables.mdx similarity index 51% rename from packages/website/docs/components/tabular_content/tables.mdx rename to packages/website/docs/components/tabular_content/tables/basic_tables.mdx index 0cda90a267e..e98a58d7e22 100644 --- a/packages/website/docs/components/tabular_content/tables.mdx +++ b/packages/website/docs/components/tabular_content/tables/basic_tables.mdx @@ -1,30 +1,17 @@ --- -slug: /tabular-content/tables -id: tabular_content_tables +slug: /tabular-content/tables/basic +id: tabular_content_tables_basic +sidebar_position: 1 --- -# Tables - -:::tip EUI provides opinionated and non-opinionated ways to build tables - -Tables can get complicated very fast. If you're just looking for a basic table with pagination, sorting, checkbox selection, and actions then you should use **EuiBasicTable**. It's a **high level component** that removes the need to worry about constructing individual components together. You simply arrange your data in the format it asks for. - -However if your table is more complicated, you can still use the individual table components like rows, headers, and pagination separately to do what you need. Find examples for that **at the bottom of this page**. - -::: - -## A basic table +# Basic tables **EuiBasicTable** is an opinionated high level component that standardizes both display and injection. At its most simple it only accepts two properties: * `items` are an array of objects that should be displayed in the table; one item per row. The exact item data that will be rendered in each cell in these rows is determined by the `columns` property. You can define `rowProps` and `cellProps` props which can either be objects or functions that return objects. The returned objects will be applied as props to the rendered rows and row cells, respectively. * `columns` defines what columns the table has and how to extract item data to display each cell in each row. -This example shows the most basic form of the **EuiBasicTable**. It is configured with the required `items` and `columns` properties. It shows how each column defines the data it needs to display per item. Some columns display the value as is (e.g. `firstName` and `lastName` fields for the user column). Other columns customize the display of the data before it is injected. This customization can be done in two (non-mutual exclusive) ways: - -* Provide a hint about the type of data (e.g. the "Date of Birth" column indicates that the data it shows is of type `date`). Providing data type hints will cause built-in display components to be adjusted (e.g. numbers will become right aligned, just like Excel). -* Provide a `render` function that given the value (and the item as a second argument) returns the React node that should be displayed as the content of the cell. This can be as simple as formatting values (e.g. the "Date of Birth" column) to utilizing more complex React components (e.g. the "Online", "Github", and "Nationality" columns as seen below). - **Note:** the basic table will treat any cells that use a `render` function as being `textOnly: false`. This may cause unnecessary word breaks. Apply `textOnly: true` to ensure it breaks properly. +This example shows the most basic form of the **EuiBasicTable**. It is configured with the required `items` and `columns` properties, with certain display configurations per-column: ```tsx interactive import React from 'react'; @@ -45,10 +32,6 @@ type User = { github: string; dateOfBirth: Date; online: boolean; - location: { - city: string; - country: string; - }; }; const users: User[] = []; @@ -61,10 +44,6 @@ for (let i = 0; i < 10; i++) { github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), - location: { - city: faker.location.city(), - country: faker.location.country(), - }, }); } @@ -102,22 +81,14 @@ export default () => { {username} ), + truncateText: true, }, { field: 'dateOfBirth', name: 'Date of Birth', dataType: 'date', render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - }, - { - field: 'location', - name: 'Location', - truncateText: true, - textOnly: true, - render: (location: User['location']) => { - return `${location.city}, ${location.country}`; - }, + formatDate(dateOfBirth, 'dobShort'), }, { field: 'online', @@ -164,36 +135,60 @@ export default () => { /> ); }; - ``` -## Adding pagination to a table +In the above example, some columns displayed the value as-is (e.g. `firstName` and `lastName` fields). Other columns customized the display of the data before it was injected. This customization can be done in two (non-mutual exclusive) ways: + +- Provide a hint about the type of data (e.g. the "Date of Birth" column indicates that the data it shows is of type `date`). Providing data type hints will cause built-in display components to be adjusted (e.g. numbers will become right aligned, like in Excel). +- Provide a `render` function that given the value (and the item as a second argument) returns the React node that should be displayed as the content of the cell. This can be as simple as formatting values (e.g. the "Date of Birth" column), to utilizing more complex React components (e.g. the "Online" and "Github" columns). + - **Note:** the basic table will treat any cells that use a `render` function as being `textOnly: false`. This may cause unnecessary word breaks. Apply `textOnly: true` to ensure it breaks properly. + +## Row selection + +The following example shows how to configure row selection via the `selection` property. For uncontrolled usage, where selection changes are determined entirely by the user, you can set items to be selected initially by passing an array of items to `selection.initialSelected`. You can also use `selected.onSelectionChange` to track or respond to the items that users select. + +To completely control table selection, use `selection.selected` instead (which requires passing `selected.onSelectionChange`). This can be useful if you want to handle table selections based on user interaction with another part of the UI. + +import BasicTableSelection from './table_selection'; + + + +## Row actions + +The following example demonstrates "actions" columns. These are special columns where you define per-row, item level actions. The most basic action you might define is a type `button` or `icon` though you can always make your own custom actions as well. + +Actions enforce some strict UI/UX guidelines: -The following example shows how to configure pagination via the `pagination`property. +* There can only be up to 2 actions visible per row. When more than two actions are defined, the first 2 `isPrimary` actions will stay visible, an ellipses icon button will hold all actions in a single popover. +* Actions change opacity when user hovers over the row with the mouse. When more than 2 actions are supplied, only the ellipses icon button stays visible at all times. +* When one or more table row(s) are selected, all item actions are disabled. Users should be expected to use some bulk action outside the individual table rows instead. ```tsx interactive -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { formatDate, + Comparators, EuiBasicTable, EuiBasicTableColumn, + EuiTableSelectionType, + EuiTableSortingType, Criteria, - EuiCode, + DefaultItemAction, + CustomItemAction, EuiLink, EuiHealth, - EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, EuiSwitch, - EuiHorizontalRule, - EuiText, + EuiSpacer, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; type User = { - id: string; + id: number; firstName: string | null | undefined; lastName: string; - github: string; - dateOfBirth: Date; online: boolean; location: { city: string; @@ -203,13 +198,11 @@ type User = { const users: User[] = []; -for (let i = 0; i < 20; i++) { +for (let i = 0; i < 5; i++) { users.push({ - id: faker.string.uuid(), + id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), - dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { city: faker.location.city(), @@ -218,11 +211,29 @@ for (let i = 0; i < 20; i++) { }); } +const cloneUserbyId = (id: number) => { + const index = users.findIndex((user) => user.id === id); + if (index >= 0) { + const user = users[index]; + users.splice(index, 0, { ...user, id: users.length }); + } +}; + +const deleteUsersByIds = (...ids: number[]) => { + ids.forEach((id) => { + const index = users.findIndex((user) => user.id === id); + if (index >= 0) { + users.splice(index, 1); + } + }); +}; + const columns: Array> = [ { field: 'firstName', name: 'First Name', truncateText: true, + sortable: true, mobileOptions: { render: (user: User) => ( <> @@ -243,22 +254,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - }, { field: 'location', name: 'Location', @@ -277,119 +272,221 @@ const columns: Array> = [ const label = online ? 'Online' : 'Offline'; return {label}; }, + sortable: true, }, ]; export default () => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - const [showPerPageOptions, setShowPerPageOptions] = useState(true); + /** + * Actions + */ + const [multiAction, setMultiAction] = useState(false); + const [customAction, setCustomAction] = useState(false); - const onTableChange = ({ page }: Criteria) => { - if (page) { - const { index: pageIndex, size: pageSize } = page; - setPageIndex(pageIndex); - setPageSize(pageSize); - } + const deleteUser = (user: User) => { + deleteUsersByIds(user.id); + setSelectedItems([]); }; - const togglePerPageOptions = () => setShowPerPageOptions(!showPerPageOptions); - - // Manually handle pagination of data - const findUsers = (users: User[], pageIndex: number, pageSize: number) => { - let pageOfItems; + const cloneUser = (user: User) => { + cloneUserbyId(user.id); + setSelectedItems([]); + }; - if (!pageIndex && !pageSize) { - pageOfItems = users; + const actions = useMemo(() => { + if (customAction) { + let actions: Array> = [ + { + render: (user: User) => { + return ( + deleteUser(user)} color="danger"> + Delete + + ); + }, + }, + ]; + if (multiAction) { + actions = [ + { + ...actions[0], + isPrimary: true, + showOnHover: true, + }, + { + render: (user: User) => { + return ( + cloneUser(user)}> + Clone + + ); + }, + }, + { + render: () => { + return {}}>Edit; + }, + }, + ]; + } + return actions; } else { - const startIndex = pageIndex * pageSize; - pageOfItems = users.slice( - startIndex, - Math.min(startIndex + pageSize, users.length) - ); + let actions: Array> = [ + { + name: 'User profile', + description: ({ firstName, lastName }) => + `Visit ${firstName} ${lastName}'s profile`, + icon: 'editorLink', + color: 'primary', + type: 'icon', + enabled: ({ online }) => !!online, + href: ({ id }) => `${window.location.href}?id=${id}`, + target: '_self', + 'data-test-subj': 'action-outboundlink', + }, + ]; + if (multiAction) { + actions = [ + { + name: <>Clone, + description: 'Clone this user', + icon: 'copy', + type: 'icon', + onClick: cloneUser, + 'data-test-subj': 'action-clone', + }, + { + name: (user: User) => (user.id ? 'Delete' : 'Remove'), + description: ({ firstName, lastName }) => + `Delete ${firstName} ${lastName}`, + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: deleteUser, + isPrimary: true, + 'data-test-subj': ({ id }) => `action-delete-${id}`, + }, + { + name: 'Edit', + isPrimary: true, + available: ({ online }) => !online, + enabled: ({ online }) => !!online, + description: 'Edit this user', + icon: 'pencil', + type: 'icon', + onClick: () => {}, + 'data-test-subj': 'action-edit', + }, + { + name: 'Share', + isPrimary: true, + description: 'Share this user', + icon: 'share', + type: 'icon', + onClick: () => {}, + 'data-test-subj': 'action-share', + }, + ...actions, + ]; + } + return actions; } + }, [customAction, multiAction]); - return { - pageOfItems, - totalItemCount: users.length, - }; - }; + const columnsWithActions = [ + ...columns, + { + name: 'Actions', + actions, + }, + ]; - const { pageOfItems, totalItemCount } = findUsers(users, pageIndex, pageSize); + /** + * Selection + */ + const [selectedItems, setSelectedItems] = useState([]); - const pagination = { - pageIndex, - pageSize, - totalItemCount, - pageSizeOptions: [10, 0], - showPerPageOptions, - }; + const onSelectionChange = (selectedItems: User[]) => { + setSelectedItems(selectedItems); + }; - const resultsCount = - pageSize === 0 ? ( - All - ) : ( - <> - - {pageSize * pageIndex + 1}-{pageSize * pageIndex + pageSize} - {' '} - of {totalItemCount} - - ); + const selection: EuiTableSelectionType = { + selectable: (user: User) => user.online, + selectableMessage: (selectable: boolean, user: User) => + !selectable + ? `${user.firstName} ${user.lastName} is currently offline` + : `Select ${user.firstName} ${user.lastName}`, + onSelectionChange, + }; + + const deleteSelectedUsers = () => { + deleteUsersByIds(...selectedItems.map((user: User) => user.id)); + setSelectedItems([]); + }; + + const deleteButton = + selectedItems.length > 0 ? ( + + Delete {selectedItems.length} Users + + ) : null; return ( <> - - Hide per page options with{' '} - pagination.showPerPageOptions = false - - } - onChange={togglePerPageOptions} - /> - - - Showing {resultsCount} Users - - - + ({ minHeight: euiTheme?.size?.xxl })} + > + + setMultiAction(!multiAction)} + /> + + + setCustomAction(!customAction)} + /> + + + {deleteButton} + + + + ); }; - ``` -## Adding sorting to a table +## Expanding rows -The following example shows how to configure column sorting via the `sorting` property and flagging the sortable columns as `sortable: true`. To enable the default sorting ability for **every** column, pass `enableAllColumns: true` to the `sorting` prop. If you don't want the user to have control over the sort you can pass `readOnly: true` to the `sorting` prop or per column. +You can expand rows by passing in a `itemIdToExpandedRowMap` prop which will contain the content you want rendered inside the expanded row. When building out your table manually (not using EuiBasicTable), you will also need to add the prop `isExpandedRow` to the row that will be revealed. ```tsx interactive -import React, { useState } from 'react'; +import React, { useState, ReactNode } from 'react'; import { formatDate, Comparators, EuiBasicTable, EuiBasicTableColumn, + EuiTableSelectionType, EuiTableSortingType, Criteria, + EuiButtonIcon, EuiHealth, - EuiIcon, - EuiLink, - EuiToolTip, - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, - EuiSpacer, - EuiCode, + EuiDescriptionList, + EuiScreenReaderOnly, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -397,8 +494,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; - dateOfBirth: Date; online: boolean; location: { city: string; @@ -408,13 +503,11 @@ type User = { const users: User[] = []; -for (let i = 0; i < 20; i++) { +for (let i = 0; i < 5; i++) { users.push({ id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), - dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { city: faker.location.city(), @@ -449,219 +542,121 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: ( - - <> - Github{' '} - - - - ), - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: ( - - <> - Date of Birth{' '} - - - - ), - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - }, { field: 'location', - name: ( - - <> - Nationality{' '} - - - - ), + name: 'Location', + truncateText: true, + textOnly: true, render: (location: User['location']) => { return `${location.city}, ${location.country}`; }, - truncateText: true, - textOnly: true, }, { field: 'online', - name: ( - - <> - Online{' '} - - - - ), + name: 'Online', + dataType: 'boolean', render: (online: User['online']) => { const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; return {label}; }, + sortable: true, + }, + { + name: 'Actions', + actions: [ + { + name: 'Clone', + description: 'Clone this person', + type: 'icon', + icon: 'copy', + onClick: () => '', + }, + ], }, ]; export default () => { - const [enableAll, setEnableAll] = useState(false); - const [readonly, setReadonly] = useState(false); - - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('firstName'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - - const onTableChange = ({ page, sort }: Criteria) => { - if (page) { - const { index: pageIndex, size: pageSize } = page; - setPageIndex(pageIndex); - setPageSize(pageSize); - } - if (sort) { - const { field: sortField, direction: sortDirection } = sort; - setSortField(sortField); - setSortDirection(sortDirection); - } - }; + /** + * Expanding rows + */ + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); - // Manually handle sorting and pagination of data - const findUsers = ( - users: User[], - pageIndex: number, - pageSize: number, - sortField: keyof User, - sortDirection: 'asc' | 'desc' - ) => { - let items; + const toggleDetails = (user: User) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (sortField) { - items = users - .slice(0) - .sort( - Comparators.property(sortField, Comparators.default(sortDirection)) - ); + if (itemIdToExpandedRowMapValues[user.id]) { + delete itemIdToExpandedRowMapValues[user.id]; } else { - items = users; + const { online, location } = user; + + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + const listItems = [ + { + title: 'Location', + description: `${location.city}, ${location.country}`, + }, + { + title: 'Online', + description: {label}, + }, + ]; + itemIdToExpandedRowMapValues[user.id] = ( + + ); } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; - let pageOfItems; - - if (!pageIndex && !pageSize) { - pageOfItems = items; - } else { - const startIndex = pageIndex * pageSize; - pageOfItems = items.slice( - startIndex, - Math.min(startIndex + pageSize, users.length) - ); - } - - return { - pageOfItems, - totalItemCount: users.length, - }; - }; - - const { pageOfItems, totalItemCount } = findUsers( - users, - pageIndex, - pageSize, - sortField, - sortDirection - ); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [3, 5, 8], - }; + const columnsWithExpandingRowToggle: Array> = [ + ...columns, + { + align: 'right', + width: '40px', + isExpander: true, + name: ( + + Expand row + + ), + mobileOptions: { header: false }, + render: (user: User) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, + return ( + toggleDetails(user)} + aria-label={ + itemIdToExpandedRowMapValues[user.id] ? 'Collapse' : 'Expand' + } + iconType={ + itemIdToExpandedRowMapValues[user.id] ? 'arrowDown' : 'arrowRight' + } + /> + ); + }, }, - enableAllColumns: enableAll, - readOnly: readonly, - }; + ]; return ( - <> - - - enableAllColumns} - checked={enableAll} - onChange={() => setEnableAll((enabled) => !enabled)} - /> - - - readOnly} - checked={readonly} - onChange={() => setReadonly((readonly) => !readonly)} - /> - - - - - + ); }; - ``` -## Adding selection to a table - -The following example shows how to configure selection via the `selection` property. For uncontrolled usage, where selection changes are determined entirely by the user, you can set items to be selected initially by passing an array of items to `selection.initialSelected`. You can also use `selected.onSelectionChange` to track or respond to the items that users select. - -To completely control table selection, use `selection.selected` instead (which requires passing `selected.onSelectionChange`). This can be useful if you want to handle table selections based on user interaction with another part of the UI. - -import BasicTableSelection from './table_selection'; - - - ## Adding a footer to a table -The following example shows how to add a footer to your table by adding `footer` to your column definitions. If one or more of your columns contains a `footer` definition, the footer area will be visible. By default, columns with no footer specified (undefined) will render an empty cell to preserve the table layout. Check out the _Build a custom table_ section below for more examples of how you can work with table footers in EUI. +The following example shows how to add a footer to your table by adding `footer` to your column definitions. If one or more of your columns contains a `footer` definition, the footer area will be visible. By default, columns with no footer specified (undefined) will render an empty cell to preserve the table layout. Check out the [custom tables](../custom) page for more examples of how you can work with table footers in EUI. ```tsx interactive import React, { useState } from 'react'; @@ -673,7 +668,6 @@ import { EuiTableSelectionType, EuiTableSortingType, Criteria, - EuiLink, EuiHealth, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -682,8 +676,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; - dateOfBirth: Date; online: boolean; location: { city: string; @@ -693,12 +685,11 @@ type User = { const users: User[] = []; -for (let i = 0; i < 20; i++) { +for (let i = 0; i < 5; i++) { users.push({ id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { @@ -713,7 +704,6 @@ const columns: Array> = [ field: 'firstName', name: 'First Name', footer: Page totals:, - sortable: true, truncateText: true, mobileOptions: { render: (user: User) => ( @@ -735,24 +725,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - footer: ({ items }: { items: User[] }) => <>{items.length} users, - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - sortable: true, - }, { field: 'location', name: 'Location', @@ -780,137 +752,40 @@ const columns: Array> = [ const label = online ? 'Online' : 'Offline'; return {label}; }, - sortable: true, }, ]; export default () => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('firstName'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - const [, setSelectedItems] = useState([]); - - const onTableChange = ({ page, sort }: Criteria) => { - if (page) { - const { index: pageIndex, size: pageSize } = page; - setPageIndex(pageIndex); - setPageSize(pageSize); - } - if (sort) { - const { field: sortField, direction: sortDirection } = sort; - setSortField(sortField); - setSortDirection(sortDirection); - } - }; - - const onSelectionChange = (selectedItems: User[]) => { - setSelectedItems(selectedItems); - }; - - // Manually handle sorting and pagination of data - const findUsers = ( - users: User[], - pageIndex: number, - pageSize: number, - sortField: keyof User, - sortDirection: 'asc' | 'desc' - ) => { - let items; - - if (sortField) { - items = users - .slice(0) - .sort( - Comparators.property(sortField, Comparators.default(sortDirection)) - ); - } else { - items = users; - } - - let pageOfItems; - - if (!pageIndex && !pageSize) { - pageOfItems = items; - } else { - const startIndex = pageIndex * pageSize; - pageOfItems = items.slice( - startIndex, - Math.min(startIndex + pageSize, users.length) - ); - } - - return { - pageOfItems, - totalItemCount: users.length, - }; - }; - - const { pageOfItems, totalItemCount } = findUsers( - users, - pageIndex, - pageSize, - sortField, - sortDirection - ); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [3, 5, 8], - }; - - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - const selection: EuiTableSelectionType = { - selectable: (user: User) => user.online, - selectableMessage: (selectable: boolean, user: User) => - !selectable - ? `${user.firstName} ${user.lastName} is currently offline` - : `Select ${user.firstName} ${user.lastName}`, - onSelectionChange, - }; - return ( ); }; - ``` -## Expanding rows +## Table layout -You can expand rows by passing in a `itemIdToExpandedRowMap` prop which will contain the content you want rendered inside the expanded row. When building out your table manually (not using EuiBasicTable), you will also need to add the prop `isExpandedRow` to the row that will be revealed. +**EuiBasicTable** has a fixed layout by default. You can change it to `auto` using the `tableLayout` prop. Note that setting `tableLayout` to `auto` prevents the `truncateText` prop from working properly. If you want to set different columns widths while still being able to use `truncateText`, set the width of each column using the `width` prop. + +You can also set the vertical alignment (`valign`) at the column level which will affect the cell contents for that entire column excluding the header and footer. ```tsx interactive -import React, { useState, ReactNode } from 'react'; +import React, { useState } from 'react'; import { formatDate, - Comparators, EuiBasicTable, - EuiBasicTableColumn, - EuiTableSelectionType, - EuiTableSortingType, - Criteria, - EuiButtonIcon, - EuiHealth, - EuiDescriptionList, - EuiScreenReaderOnly, + EuiTableFieldDataColumnType, + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiCallOut, + EuiLink, + EuiSpacer, + EuiFlexGroup, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -920,35 +795,29 @@ type User = { lastName: string; github: string; dateOfBirth: Date; - online: boolean; - location: { - city: string; - country: string; - }; + jobTitle: string; + address: string; }; const users: User[] = []; -for (let i = 0; i < 20; i++) { +for (let i = 0; i < 10; i++) { users.push({ id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), github: faker.internet.userName(), - dateOfBirth: faker.date.past(), - online: faker.datatype.boolean(), - location: { - city: faker.location.city(), - country: faker.location.country(), - }, + jobTitle: faker.person.jobTitle(), + address: `${faker.location.streetAddress()} ${faker.location.city()} ${faker.location.state( + { abbreviated: true } + )} ${faker.location.zipCode()}`, }); } -const columns: Array> = [ +const columns: Array> = [ { field: 'firstName', name: 'First Name', - sortable: true, truncateText: true, mobileOptions: { render: (user: User) => ( @@ -971,219 +840,169 @@ const columns: Array> = [ }, }, { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), + field: 'github', + name: 'Github', + render: (username: User['github']) => ( + + {username} + + ), }, { - name: 'Actions', - actions: [ - { - name: 'Clone', - description: 'Clone this person', - type: 'icon', - icon: 'copy', - onClick: () => '', - }, - ], + field: 'jobTitle', + name: 'Job title', + truncateText: true, + }, + { + field: 'address', + name: 'Address', + truncateText: { lines: 2 }, }, ]; -export default () => { - /** - * Expanding rows - */ - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< - Record - >({}); - - const toggleDetails = (user: User) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - - if (itemIdToExpandedRowMapValues[user.id]) { - delete itemIdToExpandedRowMapValues[user.id]; - } else { - const { online, location } = user; - - const color = online ? 'success' : 'danger'; - const label = online ? 'Online' : 'Offline'; - const listItems = [ - { - title: 'Location', - description: `${location.city}, ${location.country}`, - }, - { - title: 'Online', - description: {label}, - }, - ]; - itemIdToExpandedRowMapValues[user.id] = ( - - ); - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; +const tableLayoutButtons: EuiButtonGroupOptionProps[] = [ + { + id: 'tableLayoutFixed', + label: 'Fixed', + value: 'fixed', + }, + { + id: 'tableLayoutAuto', + label: 'Auto', + value: 'auto', + }, + { + id: 'tableLayoutCustom', + label: 'Custom', + value: 'custom', + }, +]; - const columnsWithExpandingRowToggle: Array> = [ - ...columns, - { - align: 'right', - width: '40px', - isExpander: true, - name: ( - - Expand row - - ), - mobileOptions: { header: false }, - render: (user: User) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; +const vAlignButtons: EuiButtonGroupOptionProps[] = [ + { + id: 'columnVAlignTop', + label: 'Top', + value: 'top', + }, + { + id: 'columnVAlignMiddle', + label: 'Middle', + value: 'middle', + }, + { + id: 'columnVAlignBottom', + label: 'Bottom', + value: 'bottom', + }, +]; - return ( - toggleDetails(user)} - aria-label={ - itemIdToExpandedRowMapValues[user.id] ? 'Collapse' : 'Expand' - } - iconType={ - itemIdToExpandedRowMapValues[user.id] ? 'arrowDown' : 'arrowRight' - } - /> - ); - }, - }, - ]; +const alignButtons: EuiButtonGroupOptionProps[] = [ + { + id: 'columnAlignLeft', + label: 'Left', + value: 'left', + }, + { + id: 'columnAlignCenter', + label: 'Center', + value: 'center', + }, + { + id: 'columnAlignRight', + label: 'Right', + value: 'right', + }, +]; - /** - * Selection - */ - const [, setSelectedItems] = useState([]); +export default () => { + const [tableLayout, setTableLayout] = useState('tableLayoutFixed'); + const [vAlign, setVAlign] = useState('columnVAlignMiddle'); + const [align, setAlign] = useState('columnAlignLeft'); - const onSelectionChange = (selectedItems: User[]) => { - setSelectedItems(selectedItems); + const onTableLayoutChange = (id: string, value: string) => { + setTableLayout(id); + columns[4].width = value === 'custom' ? '100px' : undefined; + columns[5].width = value === 'custom' ? '20%' : undefined; }; - /** - * Pagination & sorting - */ - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('firstName'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - - const onTableChange = ({ page, sort }: Criteria) => { - if (page) { - const { index: pageIndex, size: pageSize } = page; - setPageIndex(pageIndex); - setPageSize(pageSize); - } - if (sort) { - const { field: sortField, direction: sortDirection } = sort; - setSortField(sortField); - setSortDirection(sortDirection); - } + const onVAlignChange = (id: string, value: 'top' | 'middle' | 'bottom') => { + setVAlign(id); + columns.forEach((column) => (column.valign = value)); }; - const selection: EuiTableSelectionType = { - selectable: (user: User) => user.online, - selectableMessage: (selectable: boolean, user: User) => - !selectable - ? `${user.firstName} ${user.lastName} is currently offline` - : `Select ${user.firstName} ${user.lastName}`, - onSelectionChange, + const onAlignChange = (id: string, value: 'left' | 'center' | 'right') => { + setAlign(id); + columns.forEach((column) => (column.align = value)); }; - // Manually handle sorting and pagination of data - const findUsers = ( - users: User[], - pageIndex: number, - pageSize: number, - sortField: keyof User, - sortDirection: 'asc' | 'desc' - ) => { - let items; - - if (sortField) { - items = users - .slice(0) - .sort( - Comparators.property(sortField, Comparators.default(sortDirection)) - ); - } else { - items = users; - } - - let pageOfItems; - - if (!pageIndex && !pageSize) { - pageOfItems = items; - } else { - const startIndex = pageIndex * pageSize; - pageOfItems = items.slice( - startIndex, - Math.min(startIndex + pageSize, users.length) - ); - } + let callOutText; - return { - pageOfItems, - totalItemCount: users.length, - }; - }; - - const { pageOfItems, totalItemCount } = findUsers( - users, - pageIndex, - pageSize, - sortField, - sortDirection - ); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [3, 5, 8], - }; - - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; + switch (tableLayout) { + case 'tableLayoutFixed': + callOutText = + 'Job title has truncateText set to true. Address is set to { lines: 2 }'; + break; + case 'tableLayoutAuto': + callOutText = + 'Job title will not wrap or truncate since tableLayout is set to auto. Address will truncate if necessary'; + break; + case 'tableLayoutCustom': + callOutText = + 'Job title has a custom column width of 100px. Address has a custom column width of 20%'; + break; + } return ( - + <> + + + + + + + + + + ); }; - ``` -## Adding actions to table +## Responsive tables -The following example demonstrates "actions" columns. These are special columns where you define per-row, item level actions. The most basic action you might define is a type `button` or `icon` though you can always make your own custom actions as well. +Tables will be mobile-responsive by default, breaking down each row into its own card section and individually displaying each table header above the cell contents. The default breakpoint at which the table will responsively shift into cards is the [`m` window size](../../theming/breakpoints/values), which can be customized with the `responsiveBreakpoint` prop (e.g., `responsiveBreakpoint="s"`). -Actions enforce some strict UI/UX guidelines: +To never render your table responsively (e.g. for tables with very few columns), you may set `responsiveBreakpoint={false}`. Inversely, if you always want your table to render in a mobile-friendly manner, pass `true`. The below example table switches between `true/false` for quick/easy preview between mobile and desktop table UIs at all breakpoints. -* There can only be up to 2 actions visible per row. When more than two actions are defined, the first 2 `isPrimary` actions will stay visible, an ellipses icon button will hold all actions in a single popover. -* Actions change opacity when user hovers over the row with the mouse. When more than 2 actions are supplied, only the ellipses icon button stays visible at all times. -* When one or more table row(s) are selected, all item actions are disabled. Users should be expected to use some bulk action outside the individual table rows instead. +To customize your cell's appearance/rendering in mobile vs. desktop view, use the `mobileOptions` configuration. This object can be passed to each column item in **EuiBasicTable** or to **EuiTableRowCell** directly. See the "Snippet" tab in the below example, or the "Props" tab for a full list of configuration options. ```tsx interactive -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import { formatDate, Comparators, @@ -1192,11 +1011,8 @@ import { EuiTableSelectionType, EuiTableSortingType, Criteria, - DefaultItemAction, - CustomItemAction, EuiLink, EuiHealth, - EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch, @@ -1219,7 +1035,7 @@ type User = { const users: User[] = []; -for (let i = 0; i < 20; i++) { +for (let i = 0; i < 3; i++) { users.push({ id: i + 1, firstName: faker.person.firstName(), @@ -1251,396 +1067,208 @@ const deleteUsersByIds = (...ids: number[]) => { }); }; -const columns: Array> = [ - { - field: 'firstName', - name: 'First Name', - truncateText: true, - sortable: true, - mobileOptions: { - render: (user: User) => ( - <> - {user.firstName} {user.lastName} - +export default () => { + /** + * Mobile column options + */ + const [customHeader, setCustomHeader] = useState(true); + const [isResponsive, setIsResponsive] = useState(true); + + const columns: Array> = [ + { + field: 'firstName', + name: 'First Name', + truncateText: true, + sortable: true, + mobileOptions: { + render: customHeader + ? (user: User) => ( + <> + {user.firstName} {user.lastName} + + ) + : undefined, + header: customHeader ? false : true, + width: customHeader ? '100%' : undefined, + enlarge: customHeader ? true : false, + truncateText: customHeader ? false : true, + }, + }, + { + field: 'lastName', + name: 'Last Name', + truncateText: true, + mobileOptions: { + show: !isResponsive || !customHeader, + }, + }, + { + field: 'github', + name: 'Github', + render: (username: User['github']) => ( + + {username} + ), - header: false, - truncateText: false, - enlarge: true, - width: '100%', }, - }, - { - field: 'lastName', - name: 'Last Name', - truncateText: true, - mobileOptions: { - show: false, + { + field: 'dateOfBirth', + name: 'Date of Birth', + dataType: 'date', + render: (dateOfBirth: User['dateOfBirth']) => + formatDate(dateOfBirth, 'dobLong'), + sortable: true, }, - }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - }, - { - field: 'location', - name: 'Location', - truncateText: true, - textOnly: true, - render: (location: User['location']) => { - return `${location.city}, ${location.country}`; + { + field: 'location', + name: 'Location', + truncateText: true, + textOnly: true, + render: (location: User['location']) => { + return `${location.city}, ${location.country}`; + }, }, - }, - { - field: 'online', - name: 'Online', - dataType: 'boolean', - render: (online: User['online']) => { - const color = online ? 'success' : 'danger'; - const label = online ? 'Online' : 'Offline'; - return {label}; + { + field: 'online', + name: 'Online', + dataType: 'boolean', + render: (online: User['online']) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; + }, + sortable: true, }, - sortable: true, - }, -]; - -export default () => { - /** - * Actions - */ - const [multiAction, setMultiAction] = useState(false); - const [customAction, setCustomAction] = useState(false); - - const deleteUser = (user: User) => { - deleteUsersByIds(user.id); - setSelectedItems([]); - }; - - const cloneUser = (user: User) => { - cloneUserbyId(user.id); - setSelectedItems([]); - }; - - const actions = useMemo(() => { - if (customAction) { - let actions: Array> = [ + { + field: '', + name: 'Mobile only', + mobileOptions: { + only: true, + render: () => 'This column only appears on mobile', + }, + }, + { + name: 'Actions', + actions: [ { - render: (user: User) => { - return ( - deleteUser(user)} color="danger"> - Delete - - ); + name: 'Clone', + description: 'Clone this person', + icon: 'copy', + type: 'icon', + onClick: (user: User) => { + cloneUserbyId(user.id); + setSelectedItems([]); }, }, - ]; - if (multiAction) { - actions = [ - { - ...actions[0], - isPrimary: true, - showOnHover: true, - }, - { - render: (user: User) => { - return ( - cloneUser(user)}> - Clone - - ); - }, - }, - { - render: () => { - return {}}>Edit; - }, - }, - ]; - } - return actions; - } else { - let actions: Array> = [ { - name: 'User profile', - description: ({ firstName, lastName }) => - `Visit ${firstName} ${lastName}'s profile`, - icon: 'editorLink', - color: 'primary', + name: 'Delete', + description: 'Delete this person', + icon: 'trash', type: 'icon', - enabled: ({ online }) => !!online, - href: ({ id }) => `${window.location.href}?id=${id}`, - target: '_self', - 'data-test-subj': 'action-outboundlink', - }, - ]; - if (multiAction) { - actions = [ - { - name: <>Clone, - description: 'Clone this user', - icon: 'copy', - type: 'icon', - onClick: cloneUser, - 'data-test-subj': 'action-clone', - }, - { - name: (user: User) => (user.id ? 'Delete' : 'Remove'), - description: ({ firstName, lastName }) => - `Delete ${firstName} ${lastName}`, - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: deleteUser, - isPrimary: true, - 'data-test-subj': ({ id }) => `action-delete-${id}`, - }, - { - name: 'Edit', - isPrimary: true, - available: ({ online }) => !online, - enabled: ({ online }) => !!online, - description: 'Edit this user', - icon: 'pencil', - type: 'icon', - onClick: () => {}, - 'data-test-subj': 'action-edit', - }, - { - name: 'Share', - isPrimary: true, - description: 'Share this user', - icon: 'share', - type: 'icon', - onClick: () => {}, - 'data-test-subj': 'action-share', + color: 'danger', + onClick: (user: User) => { + deleteUsersByIds(user.id); + setSelectedItems([]); }, - ...actions, - ]; - } - return actions; - } - }, [customAction, multiAction]); - - const columnsWithActions = [ - ...columns, - { - name: 'Actions', - actions, + }, + ], }, ]; - /** - * Selection - */ - const [selectedItems, setSelectedItems] = useState([]); - - const onSelectionChange = (selectedItems: User[]) => { - setSelectedItems(selectedItems); - }; - - const selection: EuiTableSelectionType = { - selectable: (user: User) => user.online, - selectableMessage: (selectable: boolean, user: User) => - !selectable - ? `${user.firstName} ${user.lastName} is currently offline` - : `Select ${user.firstName} ${user.lastName}`, - onSelectionChange, - }; - - const deleteSelectedUsers = () => { - deleteUsersByIds(...selectedItems.map((user: User) => user.id)); - setSelectedItems([]); - }; - - const deleteButton = - selectedItems.length > 0 ? ( - - Delete {selectedItems.length} Users - - ) : null; - - /** - * Pagination & sorting - */ - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('firstName'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - - const onTableChange = ({ page, sort }: Criteria) => { - if (page) { - const { index: pageIndex, size: pageSize } = page; - setPageIndex(pageIndex); - setPageSize(pageSize); - } - if (sort) { - const { field: sortField, direction: sortDirection } = sort; - setSortField(sortField); - setSortDirection(sortDirection); - } - }; - - // Manually handle sorting and pagination of data - const findUsers = ( - users: User[], - pageIndex: number, - pageSize: number, - sortField: keyof User, - sortDirection: 'asc' | 'desc' - ) => { - let items; - - if (sortField) { - items = users - .slice(0) - .sort( - Comparators.property(sortField, Comparators.default(sortDirection)) - ); - } else { - items = users; - } - - let pageOfItems; - - if (!pageIndex && !pageSize) { - pageOfItems = items; - } else { - const startIndex = pageIndex * pageSize; - pageOfItems = items.slice( - startIndex, - Math.min(startIndex + pageSize, users.length) - ); - } - - return { - pageOfItems, - totalItemCount: users.length, - }; - }; - - const { pageOfItems, totalItemCount } = findUsers( - users, - pageIndex, - pageSize, - sortField, - sortDirection - ); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [3, 5, 8], - }; - - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - return ( <> - ({ minHeight: euiTheme?.size?.xxl })} - > + setMultiAction(!multiAction)} + label="Responsive" + checked={isResponsive} + onChange={() => setIsResponsive(!isResponsive)} /> setCustomAction(!customAction)} + label="Custom header" + disabled={!isResponsive} + checked={isResponsive && customHeader} + onChange={() => setCustomHeader(!customHeader)} /> - - {deleteButton} ); }; - ``` -## Table layout +## Manual pagination and sorting -**EuiBasicTable** has a fixed layout by default. You can change it to `auto` using the `tableLayout` prop. Note that setting `tableLayout` to `auto` prevents the `truncateText` prop from working properly. If you want to set different columns widths while still being able to use `truncateText`, set the width of each column using the `width` prop. +**EuiBasicTable**'s `pagination` and `sorting` properties _only_ affect the UI displayed on the table (e.g. rendering sorting arrows or pagination numbers). They do not actually handle showing paginated or sorting your `items` data. -You can also set the vertical alignment (`valign`) at the column level which will affect the cell contents for that entire column excluding the header and footer. +This is primarily useful for large amounts of API-based data, where storing or sorting all rows in-memory would pose significant performance issues. Your API backend should then asynchronously handle sorting/filtering/caching your data. + +:::tip +For non-asynchronous and smaller datasets use-cases (in the hundreds or less), we recommend using [**EuiInMemoryTable**](../in-memory), which automatically handles pagination, sorting, and searching in-memory. +::: + +### Pagination + +The following example shows how to configure pagination via the `pagination` property. ```tsx interactive import React, { useState } from 'react'; import { formatDate, EuiBasicTable, - EuiTableFieldDataColumnType, - EuiButtonGroup, - EuiButtonGroupOptionProps, - EuiCallOut, - EuiLink, + EuiBasicTableColumn, + Criteria, + EuiCode, + EuiHealth, EuiSpacer, - EuiFlexGroup, + EuiSwitch, + EuiHorizontalRule, + EuiText, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; type User = { - id: number; + id: string; firstName: string | null | undefined; lastName: string; - github: string; dateOfBirth: Date; - jobTitle: string; - address: string; + online: boolean; + location: { + city: string; + country: string; + }; }; const users: User[] = []; -for (let i = 0; i < 10; i++) { +for (let i = 0; i < 20; i++) { users.push({ - id: i + 1, + id: faker.string.uuid(), firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), - jobTitle: faker.person.jobTitle(), - address: `${faker.location.streetAddress()} ${faker.location.city()} ${faker.location.state( - { abbreviated: true } - )} ${faker.location.zipCode()}`, + online: faker.datatype.boolean(), + location: { + city: faker.location.city(), + country: faker.location.country(), + }, }); } -const columns: Array> = [ +const columns: Array> = [ { field: 'firstName', name: 'First Name', @@ -1665,15 +1293,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, { field: 'dateOfBirth', name: 'Date of Birth', @@ -1682,158 +1301,116 @@ const columns: Array> = [ formatDate(dateOfBirth, 'dobLong'), }, { - field: 'jobTitle', - name: 'Job title', + field: 'location', + name: 'Location', truncateText: true, + textOnly: true, + render: (location: User['location']) => { + return `${location.city}, ${location.country}`; + }, }, { - field: 'address', - name: 'Address', - truncateText: { lines: 2 }, + field: 'online', + name: 'Online', + dataType: 'boolean', + render: (online: User['online']) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; + }, }, ]; -const tableLayoutButtons: EuiButtonGroupOptionProps[] = [ - { - id: 'tableLayoutFixed', - label: 'Fixed', - value: 'fixed', - }, - { - id: 'tableLayoutAuto', - label: 'Auto', - value: 'auto', - }, - { - id: 'tableLayoutCustom', - label: 'Custom', - value: 'custom', - }, -]; +export default () => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [showPerPageOptions, setShowPerPageOptions] = useState(true); -const vAlignButtons: EuiButtonGroupOptionProps[] = [ - { - id: 'columnVAlignTop', - label: 'Top', - value: 'top', - }, - { - id: 'columnVAlignMiddle', - label: 'Middle', - value: 'middle', - }, - { - id: 'columnVAlignBottom', - label: 'Bottom', - value: 'bottom', - }, -]; + const onTableChange = ({ page }: Criteria) => { + if (page) { + const { index: pageIndex, size: pageSize } = page; + setPageIndex(pageIndex); + setPageSize(pageSize); + } + }; -const alignButtons: EuiButtonGroupOptionProps[] = [ - { - id: 'columnAlignLeft', - label: 'Left', - value: 'left', - }, - { - id: 'columnAlignCenter', - label: 'Center', - value: 'center', - }, - { - id: 'columnAlignRight', - label: 'Right', - value: 'right', - }, -]; + const togglePerPageOptions = () => setShowPerPageOptions(!showPerPageOptions); -export default () => { - const [tableLayout, setTableLayout] = useState('tableLayoutFixed'); - const [vAlign, setVAlign] = useState('columnVAlignMiddle'); - const [align, setAlign] = useState('columnAlignLeft'); + // Manually handle pagination of data + const findUsers = (users: User[], pageIndex: number, pageSize: number) => { + let pageOfItems; - const onTableLayoutChange = (id: string, value: string) => { - setTableLayout(id); - columns[4].width = value === 'custom' ? '100px' : undefined; - columns[5].width = value === 'custom' ? '20%' : undefined; - }; + if (!pageIndex && !pageSize) { + pageOfItems = users; + } else { + const startIndex = pageIndex * pageSize; + pageOfItems = users.slice( + startIndex, + Math.min(startIndex + pageSize, users.length) + ); + } - const onVAlignChange = (id: string, value: 'top' | 'middle' | 'bottom') => { - setVAlign(id); - columns.forEach((column) => (column.valign = value)); + return { + pageOfItems, + totalItemCount: users.length, + }; }; - const onAlignChange = (id: string, value: 'left' | 'center' | 'right') => { - setAlign(id); - columns.forEach((column) => (column.align = value)); - }; + const { pageOfItems, totalItemCount } = findUsers(users, pageIndex, pageSize); - let callOutText; + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 0], + showPerPageOptions, + }; - switch (tableLayout) { - case 'tableLayoutFixed': - callOutText = - 'Job title has truncateText set to true. Address is set to { lines: 2 }'; - break; - case 'tableLayoutAuto': - callOutText = - 'Job title will not wrap or truncate since tableLayout is set to auto. Address will truncate if necessary'; - break; - case 'tableLayoutCustom': - callOutText = - 'Job title has a custom column width of 100px. Address has a custom column width of 20%'; - break; - } + const resultsCount = + pageSize === 0 ? ( + All + ) : ( + <> + + {pageSize * pageIndex + 1}-{pageSize * pageIndex + pageSize} + {' '} + of {totalItemCount} + + ); return ( <> - - - - - - - + Hide per page options with{' '} + pagination.showPerPageOptions = false + + } + onChange={togglePerPageOptions} /> - + + + Showing {resultsCount} Users + + + ); }; - ``` -## Responsive tables - -Tables will be mobile-responsive by default, breaking down each row into its own card section and individually displaying each table header above the cell contents. The default breakpoint at which the table will responsively shift into cards is the [`m` window size](/docs/theming/breakpoints/values), which can be customized with the `responsiveBreakpoint` prop (e.g., `responsiveBreakpoint="s"`). - -To never render your table responsively (e.g. for tables with very few columns), you may set `responsiveBreakpoint={false}`. Inversely, if you always want your table to render in a mobile-friendly manner, pass `true`. The below example table switches between `true/false` for quick/easy preview between mobile and desktop table UIs at all breakpoints. +### Sorting -To customize your cell's appearance/rendering in mobile vs. desktop view, use the `mobileOptions` configuration. This object can be passed to each column item in **EuiBasicTable** or to **EuiTableRowCell** directly. See the "Snippet" tab in the below example, or the "Props" tab for a full list of configuration options. +The following example shows how to configure column sorting via the `sorting` property and flagging the sortable columns as `sortable: true`. To enable the default sorting ability for **every** column, pass `enableAllColumns: true` to the `sorting` prop. If you don't want the user to have control over the sort you can pass `readOnly: true` to the `sorting` prop or per column. ```tsx interactive import React, { useState } from 'react'; @@ -1842,15 +1419,16 @@ import { Comparators, EuiBasicTable, EuiBasicTableColumn, - EuiTableSelectionType, EuiTableSortingType, Criteria, - EuiLink, EuiHealth, + EuiIcon, + EuiToolTip, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiSpacer, + EuiCode, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -1858,7 +1436,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; dateOfBirth: Date; online: boolean; location: { @@ -1874,7 +1451,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { @@ -1884,154 +1460,100 @@ for (let i = 0; i < 20; i++) { }); } -const cloneUserbyId = (id: number) => { - const index = users.findIndex((user) => user.id === id); - if (index >= 0) { - const user = users[index]; - users.splice(index, 0, { ...user, id: users.length }); - } -}; - -const deleteUsersByIds = (...ids: number[]) => { - ids.forEach((id) => { - const index = users.findIndex((user) => user.id === id); - if (index >= 0) { - users.splice(index, 1); - } - }); -}; - -export default () => { - /** - * Mobile column options - */ - const [customHeader, setCustomHeader] = useState(true); - const [isResponsive, setIsResponsive] = useState(true); - - const columns: Array> = [ - { - field: 'firstName', - name: 'First Name', - truncateText: true, - sortable: true, - mobileOptions: { - render: customHeader - ? (user: User) => ( - <> - {user.firstName} {user.lastName} - - ) - : undefined, - header: customHeader ? false : true, - width: customHeader ? '100%' : undefined, - enlarge: customHeader ? true : false, - truncateText: customHeader ? false : true, - }, - }, - { - field: 'lastName', - name: 'Last Name', - truncateText: true, - mobileOptions: { - show: !isResponsive || !customHeader, - }, - }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - +const columns: Array> = [ + { + field: 'firstName', + name: 'First Name', + sortable: true, + truncateText: true, + mobileOptions: { + render: (user: User) => ( + <> + {user.firstName} {user.lastName} + ), + header: false, + truncateText: false, + enlarge: true, + width: '100%', }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - sortable: true, - }, - { - field: 'location', - name: 'Location', - truncateText: true, - textOnly: true, - render: (location: User['location']) => { - return `${location.city}, ${location.country}`; - }, - }, - { - field: 'online', - name: 'Online', - dataType: 'boolean', - render: (online: User['online']) => { - const color = online ? 'success' : 'danger'; - const label = online ? 'Online' : 'Offline'; - return {label}; - }, - sortable: true, + }, + { + field: 'lastName', + name: 'Last Name', + truncateText: true, + mobileOptions: { + show: false, }, - { - field: '', - name: 'Mobile only', - mobileOptions: { - only: true, - render: () => 'This column only appears on mobile', - }, + }, + { + field: 'dateOfBirth', + name: ( + + <> + Date of Birth{' '} + + + + ), + render: (dateOfBirth: User['dateOfBirth']) => + formatDate(dateOfBirth, 'dobLong'), + }, + { + field: 'location', + name: ( + + <> + Nationality{' '} + + + + ), + render: (location: User['location']) => { + return `${location.city}, ${location.country}`; }, - { - name: 'Actions', - actions: [ - { - name: 'Clone', - description: 'Clone this person', - icon: 'copy', - type: 'icon', - onClick: (user: User) => { - cloneUserbyId(user.id); - setSelectedItems([]); - }, - }, - { - name: 'Delete', - description: 'Delete this person', - icon: 'trash', - type: 'icon', - color: 'danger', - onClick: (user: User) => { - deleteUsersByIds(user.id); - setSelectedItems([]); - }, - }, - ], + truncateText: true, + textOnly: true, + }, + { + field: 'online', + name: ( + + <> + Online{' '} + + + + ), + render: (online: User['online']) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; }, - ]; - - /** - * Selection - */ - const [, setSelectedItems] = useState([]); - - const onSelectionChange = (selectedItems: User[]) => { - setSelectedItems(selectedItems); - }; + }, +]; - const selection: EuiTableSelectionType = { - selectable: (user: User) => user.online, - selectableMessage: (selectable: boolean, user: User) => - !selectable - ? `${user.firstName} ${user.lastName} is currently offline` - : `Select ${user.firstName} ${user.lastName}`, - onSelectionChange, - }; +export default () => { + const [enableAll, setEnableAll] = useState(false); + const [readonly, setReadonly] = useState(false); - /** - * Pagination & sorting - */ const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(3); + const [pageSize, setPageSize] = useState(5); const [sortField, setSortField] = useState('firstName'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); @@ -2106,907 +1628,44 @@ export default () => { field: sortField, direction: sortDirection, }, + enableAllColumns: enableAll, + readOnly: readonly, }; return ( <> - + setIsResponsive(!isResponsive)} + label={enableAllColumns} + checked={enableAll} + onChange={() => setEnableAll((enabled) => !enabled)} /> setCustomHeader(!customHeader)} + label={readOnly} + checked={readonly} + onChange={() => setReadonly((readonly) => !readonly)} /> - - - + ); }; - -``` - -## Build a custom table from individual components - -As an alternative to **EuiBasicTable** you can instead construct a table from individual **low level, basic components** like **EuiTableHeader** and **EuiTableRowCell**. Below is one of many ways you might set this up on your own. Important to note are how you need to set individual props like the `truncateText` prop to cells to enforce a single-line behavior and truncate their contents, or set the `textOnly` prop to `false` if you need the contents to be a direct descendent of the cell. - -### Responsive extras - -You must supply a `mobileOptions.header` prop equivalent to the column header on each **EuiTableRowCell** so that the mobile version will use that to populate the per cell headers. - -Also, custom table implementations **will not** auto-populate any header level functions like selection and filtering. In order to add mobile support for these functions, you will need to implement the **EuiTableHeaderMobile** component as a wrapper around these and use **EuiTableSortMobile** and **EuiTableSortMobileItem** components to supply mobile sorting. See demo below. - -```tsx interactive -import React, { Component, ReactNode } from 'react'; -import { - EuiBadge, - EuiHealth, - EuiButton, - EuiButtonIcon, - EuiCheckbox, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiPopover, - EuiSpacer, - EuiTable, - EuiTableBody, - EuiTableFooter, - EuiTableFooterCell, - EuiTableHeader, - EuiTableHeaderCell, - EuiTableHeaderCellCheckbox, - EuiTablePagination, - EuiTableRow, - EuiTableRowCell, - EuiTableRowCellCheckbox, - EuiTableSortMobile, - EuiTableHeaderMobile, - EuiScreenReaderOnly, - EuiTableFieldDataColumnType, - EuiTableSortMobileProps, - LEFT_ALIGNMENT, - RIGHT_ALIGNMENT, - HorizontalAlignment, - Pager, - SortableProperties, -} from '@elastic/eui'; - -interface DataTitle { - value: ReactNode; - truncateText?: boolean; - isLink?: boolean; -} - -interface DataItem { - id: number; - title: ReactNode | DataTitle; - type: string; - dateCreated: string; - magnitude: number; - health: ReactNode; -} - -interface Column { - id: string; - label?: string; - isVisuallyHiddenLabel?: boolean; - isSortable?: boolean; - isCheckbox?: boolean; - isActionsPopover?: boolean; - textOnly?: boolean; - alignment?: HorizontalAlignment; - width?: string; - footer?: ReactNode | Function; - render?: Function; - cellProvider?: Function; - mobileOptions?: EuiTableFieldDataColumnType['mobileOptions']; -} - -interface State { - itemIdToSelectedMap: Record; - itemIdToOpenActionsPopoverMap: Record; - sortedColumn: keyof DataItem; - itemsPerPage: number; - firstItemIndex: number; - lastItemIndex: number; -} - -interface Pagination { - pageIndex: number; - pageSize: number; - totalItemCount: number; -} - -export default class extends Component<{}, State> { - items: DataItem[] = [ - { - id: 0, - title: - 'A very long line which will wrap on narrower screens and NOT become truncated and replaced by an ellipsis', - type: 'user', - dateCreated: 'Tue Dec 28 2016', - magnitude: 1, - health: Healthy, - }, - { - id: 1, - title: { - value: - 'A very long line which will not wrap on narrower screens and instead will become truncated and replaced by an ellipsis', - truncateText: true, - }, - type: 'user', - dateCreated: 'Tue Dec 01 2016', - magnitude: 1, - health: Healthy, - }, - { - id: 2, - title: ( - <> - A very long line in an ELEMENT which will wrap on narrower screens and - NOT become truncated and replaced by an ellipsis - - ), - type: 'user', - dateCreated: 'Tue Dec 01 2016', - magnitude: 10, - health: Warning, - }, - { - id: 3, - title: { - value: ( - <> - A very long line in an ELEMENT which will not wrap on narrower - screens and instead will become truncated and replaced by an - ellipsis - - ), - truncateText: true, - }, - type: 'user', - dateCreated: 'Tue Dec 16 2016', - magnitude: 100, - health: Healthy, - }, - { - id: 4, - title: { - value: 'Dog', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 13 2016', - magnitude: 1000, - health: Warning, - }, - { - id: 5, - title: { - value: 'Dragon', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Healthy, - }, - { - id: 6, - title: { - value: 'Bear', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Danger, - }, - { - id: 7, - title: { - value: 'Dinosaur', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Warning, - }, - { - id: 8, - title: { - value: 'Spider', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Warning, - }, - { - id: 9, - title: { - value: 'Bugbear', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Healthy, - }, - { - id: 10, - title: { - value: 'Bear', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Danger, - }, - { - id: 11, - title: { - value: 'Dinosaur', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Warning, - }, - { - id: 12, - title: { - value: 'Spider', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Healthy, - }, - { - id: 13, - title: { - value: 'Bugbear', - isLink: true, - }, - type: 'user', - dateCreated: 'Tue Dec 11 2016', - magnitude: 10000, - health: Danger, - }, - ]; - - columns: Column[] = [ - { - id: 'checkbox', - isCheckbox: true, - textOnly: false, - width: '32px', - }, - { - id: 'type', - label: 'Type', - isVisuallyHiddenLabel: true, - alignment: LEFT_ALIGNMENT, - width: '24px', - cellProvider: (cell: string) => , - mobileOptions: { - show: false, - }, - }, - { - id: 'title', - label: 'Title', - footer: Title, - alignment: LEFT_ALIGNMENT, - isSortable: false, - mobileOptions: { - show: false, - }, - }, - { - id: 'title_type', - label: 'Title', - mobileOptions: { - only: true, - header: false, - enlarge: true, - width: '100%', - }, - render: (title: DataItem['title'], item: DataItem) => ( - <> - {' '} - {title as ReactNode} - - ), - }, - { - id: 'health', - label: 'Health', - footer: '', - alignment: LEFT_ALIGNMENT, - }, - { - id: 'dateCreated', - label: 'Date created', - footer: 'Date created', - alignment: LEFT_ALIGNMENT, - isSortable: true, - }, - { - id: 'magnitude', - label: 'Orders of magnitude', - footer: ({ - items, - pagination, - }: { - items: DataItem[]; - pagination: Pagination; - }) => { - const { pageIndex, pageSize } = pagination; - const startIndex = pageIndex * pageSize; - const pageOfItems = items.slice( - startIndex, - Math.min(startIndex + pageSize, items.length) - ); - return ( - - Total: {pageOfItems.reduce((acc, cur) => acc + cur.magnitude, 0)} - - ); - }, - alignment: RIGHT_ALIGNMENT, - isSortable: true, - }, - { - id: 'actions', - label: 'Actions', - isVisuallyHiddenLabel: true, - alignment: RIGHT_ALIGNMENT, - isActionsPopover: true, - width: '32px', - }, - ]; - - sortableProperties: SortableProperties; - pager: Pager; - - constructor(props: {}) { - super(props); - - const defaultItemsPerPage = 10; - this.pager = new Pager(this.items.length, defaultItemsPerPage); - - this.state = { - itemIdToSelectedMap: {}, - itemIdToOpenActionsPopoverMap: {}, - sortedColumn: 'magnitude', - itemsPerPage: defaultItemsPerPage, - firstItemIndex: this.pager.getFirstItemIndex(), - lastItemIndex: this.pager.getLastItemIndex(), - }; - - this.sortableProperties = new SortableProperties( - [ - { - name: 'dateCreated', - getValue: (item) => item.dateCreated.toLowerCase(), - isAscending: true, - }, - { - name: 'magnitude', - getValue: (item) => String(item.magnitude).toLowerCase(), - isAscending: true, - }, - ], - this.state.sortedColumn - ); - } - - onChangeItemsPerPage = (itemsPerPage: number) => { - this.pager.setItemsPerPage(itemsPerPage); - this.setState({ - itemsPerPage, - firstItemIndex: this.pager.getFirstItemIndex(), - lastItemIndex: this.pager.getLastItemIndex(), - }); - }; - - onChangePage = (pageIndex: number) => { - this.pager.goToPageIndex(pageIndex); - this.setState({ - firstItemIndex: this.pager.getFirstItemIndex(), - lastItemIndex: this.pager.getLastItemIndex(), - }); - }; - - onSort = (prop: string) => { - this.sortableProperties.sortOn(prop); - - this.setState({ - sortedColumn: prop as keyof DataItem, - }); - }; - - toggleItem = (itemId: number) => { - this.setState((previousState) => { - const newItemIdToSelectedMap = { - ...previousState.itemIdToSelectedMap, - [itemId]: !previousState.itemIdToSelectedMap[itemId], - }; - - return { - itemIdToSelectedMap: newItemIdToSelectedMap, - }; - }); - }; - - toggleAll = () => { - const allSelected = this.areAllItemsSelected(); - const newItemIdToSelectedMap: State['itemIdToSelectedMap'] = {}; - this.items.forEach( - (item) => (newItemIdToSelectedMap[item.id] = !allSelected) - ); - - this.setState({ - itemIdToSelectedMap: newItemIdToSelectedMap, - }); - }; - - isItemSelected = (itemId: number) => { - return this.state.itemIdToSelectedMap[itemId]; - }; - - areAllItemsSelected = () => { - const indexOfUnselectedItem = this.items.findIndex( - (item) => !this.isItemSelected(item.id) - ); - return indexOfUnselectedItem === -1; - }; - - areAnyRowsSelected = () => { - return ( - Object.keys(this.state.itemIdToSelectedMap).findIndex((id) => { - return this.state.itemIdToSelectedMap[id]; - }) !== -1 - ); - }; - - togglePopover = (itemId: number) => { - this.setState((previousState) => { - const newItemIdToOpenActionsPopoverMap = { - ...previousState.itemIdToOpenActionsPopoverMap, - [itemId]: !previousState.itemIdToOpenActionsPopoverMap[itemId], - }; - - return { - itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, - }; - }); - }; - - closePopover = (itemId: number) => { - // only update the state if this item's popover is open - if (this.isPopoverOpen(itemId)) { - this.setState((previousState) => { - const newItemIdToOpenActionsPopoverMap = { - ...previousState.itemIdToOpenActionsPopoverMap, - [itemId]: false, - }; - - return { - itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, - }; - }); - } - }; - - isPopoverOpen = (itemId: number) => { - return this.state.itemIdToOpenActionsPopoverMap[itemId]; - }; - - renderSelectAll = (mobile?: boolean) => { - return ( - - ); - }; - - private getTableMobileSortItems() { - const items: EuiTableSortMobileProps['items'] = []; - - this.columns.forEach((column) => { - if (column.isCheckbox || !column.isSortable) { - return; - } - items.push({ - name: column.label, - key: column.id, - onSort: this.onSort.bind(this, column.id), - isSorted: this.state.sortedColumn === column.id, - isSortAscending: this.sortableProperties.isAscendingByName(column.id), - }); - }); - return items; - } - - renderHeaderCells() { - const headers: ReactNode[] = []; - - this.columns.forEach((column, columnIndex) => { - if (column.isCheckbox) { - headers.push( - - {this.renderSelectAll()} - - ); - } else if (column.isVisuallyHiddenLabel) { - headers.push( - - - {column.label} - - - ); - } else { - headers.push( - - {column.label} - - ); - } - }); - return headers.length ? headers : null; - } - - renderRows() { - const renderRow = (item: DataItem) => { - const cells = this.columns.map((column) => { - const cell = item[column.id as keyof DataItem]; - - let child; - - if (column.isCheckbox) { - return ( - - - - ); - } - - if (column.isActionsPopover) { - return ( - - this.togglePopover(item.id)} - /> - } - isOpen={this.isPopoverOpen(item.id)} - closePopover={() => this.closePopover(item.id)} - panelPaddingSize="none" - anchorPosition="leftCenter" - > - { - this.closePopover(item.id); - }} - > - Edit - , - { - this.closePopover(item.id); - }} - > - Share - , - { - this.closePopover(item.id); - }} - > - Delete - , - ]} - /> - - - ); - } - - if (column.id === 'title' || column.id === 'title_type') { - let title = item.title; - - if ((item.title as DataTitle)?.value) { - const titleObj = item.title as DataTitle; - const titleText = titleObj.value; - title = titleObj.isLink ? ( - {titleText} - ) : ( - titleText - ); - } - - if (column.render) { - child = column.render(title, item); - } else { - child = title; - } - } else if (column.cellProvider) { - child = column.cellProvider(cell); - } else { - child = cell; - } - - return ( - - {child} - - ); - }); - - return ( - - {cells} - - ); - }; - - const rows = []; - - for ( - let itemIndex = this.state.firstItemIndex; - itemIndex <= this.state.lastItemIndex; - itemIndex++ - ) { - const item = this.items[itemIndex]; - rows.push(renderRow(item)); - } - - return rows; - } - - renderFooterCells() { - const footers: ReactNode[] = []; - - const items = this.items; - const pagination = { - pageIndex: this.pager.getCurrentPageIndex(), - pageSize: this.state.itemsPerPage, - totalItemCount: this.pager.getTotalPages(), - }; - - this.columns.forEach((column) => { - const footer = this.getColumnFooter(column, { items, pagination }); - if (column.mobileOptions && column.mobileOptions.only) { - return; // exclude columns that only exist for mobile headers - } - - if (footer) { - footers.push( - - {footer} - - ); - } else { - footers.push( - - {undefined} - - ); - } - }); - return footers; - } - - getColumnFooter = ( - column: Column, - { - items, - pagination, - }: { - items: DataItem[]; - pagination: Pagination; - } - ) => { - if (column.footer === null) { - return null; - } - - if (column.footer) { - if (typeof column.footer === 'function') { - return column.footer({ items, pagination }); - } - return column.footer; - } - - return undefined; - }; - - render() { - let optionalActionButtons; - const exampleId = 'example-id'; - - if (!!this.areAnyRowsSelected()) { - optionalActionButtons = ( - - Delete selected - - ); - } - - return ( - <> - - {optionalActionButtons} - - - - - - - - - - - {this.renderSelectAll(true)} - - - - - - - - {this.renderHeaderCells()} - - {this.renderRows()} - - {this.renderFooterCells()} - - - - - - - ); - } -} - ``` ## Props -import basicTableDocgen from '@elastic/eui-docgen/dist/components/basic_table'; -import tableDocgen from '@elastic/eui-docgen/dist/components/table'; - - - - - - - - - - - - - - - - +import docgen from '@elastic/eui-docgen/dist/components/basic_table'; + + diff --git a/packages/website/docs/components/tabular_content/tables/custom_tables.mdx b/packages/website/docs/components/tabular_content/tables/custom_tables.mdx new file mode 100644 index 00000000000..095bc29915f --- /dev/null +++ b/packages/website/docs/components/tabular_content/tables/custom_tables.mdx @@ -0,0 +1,878 @@ +--- +slug: /tabular-content/tables/custom +id: tabular_content_tables_custom +sidebar_position: 3 +--- + +# Custom tables + +If you need more custom behavior than either [**EuiBasicTable**](../basic) or [**EuiInMemoryTable**](../in-memory) allow, you can opt to completely construct your own table from EUI's low-level table building block components, like **EuiTableHeader** and **EuiTableRowCell**. + +There are several important caveats to keep in mind while doing so: + +:::note Selection and filtering +Custom table implementations must completely handle their own selection and filtering. +::: + +:::note Mobile headers +You must supply a `mobileOptions.header` prop equivalent to the column header on each **EuiTableRowCell** so that the mobile version will use that to populate the per cell headers. + +Also, in order to add mobile support for selection and filtering toolbars, you will need to implement the **EuiTableHeaderMobile** component as a wrapper around these and use **EuiTableSortMobile** and **EuiTableSortMobileItem** components to supply mobile sorting. See demo below. +::: + +:::note Cell text behavior +Set individual props like the `truncateText` prop to cells to enforce a single-line behavior and truncate their contents, or set the `textOnly` prop to `false` if you need the contents to be a direct descendent of the cell. +::: + +Below is one of many ways you might set up a custom table that account for all the above notes: + +```tsx interactive +import React, { Component, ReactNode } from 'react'; +import { + EuiBadge, + EuiHealth, + EuiButton, + EuiButtonIcon, + EuiCheckbox, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiPopover, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableFooter, + EuiTableFooterCell, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableHeaderCellCheckbox, + EuiTablePagination, + EuiTableRow, + EuiTableRowCell, + EuiTableRowCellCheckbox, + EuiTableSortMobile, + EuiTableHeaderMobile, + EuiScreenReaderOnly, + EuiTableFieldDataColumnType, + EuiTableSortMobileProps, + LEFT_ALIGNMENT, + RIGHT_ALIGNMENT, + HorizontalAlignment, + Pager, + SortableProperties, +} from '@elastic/eui'; + +interface DataTitle { + value: ReactNode; + truncateText?: boolean; + isLink?: boolean; +} + +interface DataItem { + id: number; + title: ReactNode | DataTitle; + type: string; + dateCreated: string; + magnitude: number; + health: ReactNode; +} + +interface Column { + id: string; + label?: string; + isVisuallyHiddenLabel?: boolean; + isSortable?: boolean; + isCheckbox?: boolean; + isActionsPopover?: boolean; + textOnly?: boolean; + alignment?: HorizontalAlignment; + width?: string; + footer?: ReactNode | Function; + render?: Function; + cellProvider?: Function; + mobileOptions?: EuiTableFieldDataColumnType['mobileOptions']; +} + +interface State { + itemIdToSelectedMap: Record; + itemIdToOpenActionsPopoverMap: Record; + sortedColumn: keyof DataItem; + itemsPerPage: number; + firstItemIndex: number; + lastItemIndex: number; +} + +interface Pagination { + pageIndex: number; + pageSize: number; + totalItemCount: number; +} + +export default class extends Component<{}, State> { + items: DataItem[] = [ + { + id: 0, + title: + 'A very long line which will wrap on narrower screens and NOT become truncated and replaced by an ellipsis', + type: 'user', + dateCreated: 'Tue Dec 28 2016', + magnitude: 1, + health: Healthy, + }, + { + id: 1, + title: { + value: + 'A very long line which will not wrap on narrower screens and instead will become truncated and replaced by an ellipsis', + truncateText: true, + }, + type: 'user', + dateCreated: 'Tue Dec 01 2016', + magnitude: 1, + health: Healthy, + }, + { + id: 2, + title: ( + <> + A very long line in an ELEMENT which will wrap on narrower screens and + NOT become truncated and replaced by an ellipsis + + ), + type: 'user', + dateCreated: 'Tue Dec 01 2016', + magnitude: 10, + health: Warning, + }, + { + id: 3, + title: { + value: ( + <> + A very long line in an ELEMENT which will not wrap on narrower + screens and instead will become truncated and replaced by an + ellipsis + + ), + truncateText: true, + }, + type: 'user', + dateCreated: 'Tue Dec 16 2016', + magnitude: 100, + health: Healthy, + }, + { + id: 4, + title: { + value: 'Dog', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 13 2016', + magnitude: 1000, + health: Warning, + }, + { + id: 5, + title: { + value: 'Dragon', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Healthy, + }, + { + id: 6, + title: { + value: 'Bear', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Danger, + }, + { + id: 7, + title: { + value: 'Dinosaur', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Warning, + }, + { + id: 8, + title: { + value: 'Spider', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Warning, + }, + { + id: 9, + title: { + value: 'Bugbear', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Healthy, + }, + { + id: 10, + title: { + value: 'Bear', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Danger, + }, + { + id: 11, + title: { + value: 'Dinosaur', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Warning, + }, + { + id: 12, + title: { + value: 'Spider', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Healthy, + }, + { + id: 13, + title: { + value: 'Bugbear', + isLink: true, + }, + type: 'user', + dateCreated: 'Tue Dec 11 2016', + magnitude: 10000, + health: Danger, + }, + ]; + + columns: Column[] = [ + { + id: 'checkbox', + isCheckbox: true, + textOnly: false, + width: '32px', + }, + { + id: 'type', + label: 'Type', + isVisuallyHiddenLabel: true, + alignment: LEFT_ALIGNMENT, + width: '24px', + cellProvider: (cell: string) => , + mobileOptions: { + show: false, + }, + }, + { + id: 'title', + label: 'Title', + footer: Title, + alignment: LEFT_ALIGNMENT, + isSortable: false, + mobileOptions: { + show: false, + }, + }, + { + id: 'title_type', + label: 'Title', + mobileOptions: { + only: true, + header: false, + enlarge: true, + width: '100%', + }, + render: (title: DataItem['title'], item: DataItem) => ( + <> + {' '} + {title as ReactNode} + + ), + }, + { + id: 'health', + label: 'Health', + footer: '', + alignment: LEFT_ALIGNMENT, + }, + { + id: 'dateCreated', + label: 'Date created', + footer: 'Date created', + alignment: LEFT_ALIGNMENT, + isSortable: true, + }, + { + id: 'magnitude', + label: 'Orders of magnitude', + footer: ({ + items, + pagination, + }: { + items: DataItem[]; + pagination: Pagination; + }) => { + const { pageIndex, pageSize } = pagination; + const startIndex = pageIndex * pageSize; + const pageOfItems = items.slice( + startIndex, + Math.min(startIndex + pageSize, items.length) + ); + return ( + + Total: {pageOfItems.reduce((acc, cur) => acc + cur.magnitude, 0)} + + ); + }, + alignment: RIGHT_ALIGNMENT, + isSortable: true, + }, + { + id: 'actions', + label: 'Actions', + isVisuallyHiddenLabel: true, + alignment: RIGHT_ALIGNMENT, + isActionsPopover: true, + width: '32px', + }, + ]; + + sortableProperties: SortableProperties; + pager: Pager; + + constructor(props: {}) { + super(props); + + const defaultItemsPerPage = 10; + this.pager = new Pager(this.items.length, defaultItemsPerPage); + + this.state = { + itemIdToSelectedMap: {}, + itemIdToOpenActionsPopoverMap: {}, + sortedColumn: 'magnitude', + itemsPerPage: defaultItemsPerPage, + firstItemIndex: this.pager.getFirstItemIndex(), + lastItemIndex: this.pager.getLastItemIndex(), + }; + + this.sortableProperties = new SortableProperties( + [ + { + name: 'dateCreated', + getValue: (item) => item.dateCreated.toLowerCase(), + isAscending: true, + }, + { + name: 'magnitude', + getValue: (item) => String(item.magnitude).toLowerCase(), + isAscending: true, + }, + ], + this.state.sortedColumn + ); + } + + onChangeItemsPerPage = (itemsPerPage: number) => { + this.pager.setItemsPerPage(itemsPerPage); + this.setState({ + itemsPerPage, + firstItemIndex: this.pager.getFirstItemIndex(), + lastItemIndex: this.pager.getLastItemIndex(), + }); + }; + + onChangePage = (pageIndex: number) => { + this.pager.goToPageIndex(pageIndex); + this.setState({ + firstItemIndex: this.pager.getFirstItemIndex(), + lastItemIndex: this.pager.getLastItemIndex(), + }); + }; + + onSort = (prop: string) => { + this.sortableProperties.sortOn(prop); + + this.setState({ + sortedColumn: prop as keyof DataItem, + }); + }; + + toggleItem = (itemId: number) => { + this.setState((previousState) => { + const newItemIdToSelectedMap = { + ...previousState.itemIdToSelectedMap, + [itemId]: !previousState.itemIdToSelectedMap[itemId], + }; + + return { + itemIdToSelectedMap: newItemIdToSelectedMap, + }; + }); + }; + + toggleAll = () => { + const allSelected = this.areAllItemsSelected(); + const newItemIdToSelectedMap: State['itemIdToSelectedMap'] = {}; + this.items.forEach( + (item) => (newItemIdToSelectedMap[item.id] = !allSelected) + ); + + this.setState({ + itemIdToSelectedMap: newItemIdToSelectedMap, + }); + }; + + isItemSelected = (itemId: number) => { + return this.state.itemIdToSelectedMap[itemId]; + }; + + areAllItemsSelected = () => { + const indexOfUnselectedItem = this.items.findIndex( + (item) => !this.isItemSelected(item.id) + ); + return indexOfUnselectedItem === -1; + }; + + areAnyRowsSelected = () => { + return ( + Object.keys(this.state.itemIdToSelectedMap).findIndex((id) => { + return this.state.itemIdToSelectedMap[id]; + }) !== -1 + ); + }; + + togglePopover = (itemId: number) => { + this.setState((previousState) => { + const newItemIdToOpenActionsPopoverMap = { + ...previousState.itemIdToOpenActionsPopoverMap, + [itemId]: !previousState.itemIdToOpenActionsPopoverMap[itemId], + }; + + return { + itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, + }; + }); + }; + + closePopover = (itemId: number) => { + // only update the state if this item's popover is open + if (this.isPopoverOpen(itemId)) { + this.setState((previousState) => { + const newItemIdToOpenActionsPopoverMap = { + ...previousState.itemIdToOpenActionsPopoverMap, + [itemId]: false, + }; + + return { + itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, + }; + }); + } + }; + + isPopoverOpen = (itemId: number) => { + return this.state.itemIdToOpenActionsPopoverMap[itemId]; + }; + + renderSelectAll = (mobile?: boolean) => { + return ( + + ); + }; + + private getTableMobileSortItems() { + const items: EuiTableSortMobileProps['items'] = []; + + this.columns.forEach((column) => { + if (column.isCheckbox || !column.isSortable) { + return; + } + items.push({ + name: column.label, + key: column.id, + onSort: this.onSort.bind(this, column.id), + isSorted: this.state.sortedColumn === column.id, + isSortAscending: this.sortableProperties.isAscendingByName(column.id), + }); + }); + return items; + } + + renderHeaderCells() { + const headers: ReactNode[] = []; + + this.columns.forEach((column, columnIndex) => { + if (column.isCheckbox) { + headers.push( + + {this.renderSelectAll()} + + ); + } else if (column.isVisuallyHiddenLabel) { + headers.push( + + + {column.label} + + + ); + } else { + headers.push( + + {column.label} + + ); + } + }); + return headers.length ? headers : null; + } + + renderRows() { + const renderRow = (item: DataItem) => { + const cells = this.columns.map((column) => { + const cell = item[column.id as keyof DataItem]; + + let child; + + if (column.isCheckbox) { + return ( + + + + ); + } + + if (column.isActionsPopover) { + return ( + + this.togglePopover(item.id)} + /> + } + isOpen={this.isPopoverOpen(item.id)} + closePopover={() => this.closePopover(item.id)} + panelPaddingSize="none" + anchorPosition="leftCenter" + > + { + this.closePopover(item.id); + }} + > + Edit + , + { + this.closePopover(item.id); + }} + > + Share + , + { + this.closePopover(item.id); + }} + > + Delete + , + ]} + /> + + + ); + } + + if (column.id === 'title' || column.id === 'title_type') { + let title = item.title; + + if ((item.title as DataTitle)?.value) { + const titleObj = item.title as DataTitle; + const titleText = titleObj.value; + title = titleObj.isLink ? ( + {titleText} + ) : ( + titleText + ); + } + + if (column.render) { + child = column.render(title, item); + } else { + child = title; + } + } else if (column.cellProvider) { + child = column.cellProvider(cell); + } else { + child = cell; + } + + return ( + + {child} + + ); + }); + + return ( + + {cells} + + ); + }; + + const rows = []; + + for ( + let itemIndex = this.state.firstItemIndex; + itemIndex <= this.state.lastItemIndex; + itemIndex++ + ) { + const item = this.items[itemIndex]; + rows.push(renderRow(item)); + } + + return rows; + } + + renderFooterCells() { + const footers: ReactNode[] = []; + + const items = this.items; + const pagination = { + pageIndex: this.pager.getCurrentPageIndex(), + pageSize: this.state.itemsPerPage, + totalItemCount: this.pager.getTotalPages(), + }; + + this.columns.forEach((column) => { + const footer = this.getColumnFooter(column, { items, pagination }); + if (column.mobileOptions && column.mobileOptions.only) { + return; // exclude columns that only exist for mobile headers + } + + if (footer) { + footers.push( + + {footer} + + ); + } else { + footers.push( + + {undefined} + + ); + } + }); + return footers; + } + + getColumnFooter = ( + column: Column, + { + items, + pagination, + }: { + items: DataItem[]; + pagination: Pagination; + } + ) => { + if (column.footer === null) { + return null; + } + + if (column.footer) { + if (typeof column.footer === 'function') { + return column.footer({ items, pagination }); + } + return column.footer; + } + + return undefined; + }; + + render() { + let optionalActionButtons; + const exampleId = 'example-id'; + + if (!!this.areAnyRowsSelected()) { + optionalActionButtons = ( + + Delete selected + + ); + } + + return ( + <> + + {optionalActionButtons} + + + + + + + + + + + {this.renderSelectAll(true)} + + + + + + + + {this.renderHeaderCells()} + + {this.renderRows()} + + {this.renderFooterCells()} + + + + + + + ); + } +} +``` + +## Props + +import docgen from '@elastic/eui-docgen/dist/components/table'; + + + + + + + + + + + + + + + diff --git a/packages/website/docs/components/tabular_content/in_memory_tables.mdx b/packages/website/docs/components/tabular_content/tables/in_memory_tables.mdx similarity index 91% rename from packages/website/docs/components/tabular_content/in_memory_tables.mdx rename to packages/website/docs/components/tabular_content/tables/in_memory_tables.mdx index 7cff9b8c7dd..a0e9dd9b1bd 100644 --- a/packages/website/docs/components/tabular_content/in_memory_tables.mdx +++ b/packages/website/docs/components/tabular_content/tables/in_memory_tables.mdx @@ -1,11 +1,12 @@ --- -slug: /tabular-content/in-memory-tables -id: tabular_content_in_memory_tables +slug: /tabular-content/tables/in-memory +id: tabular_content_tables_in_memory +sidebar_position: 2 --- # In-memory tables -The **EuiInMemoryTable** is a higher level component wrapper around **EuiBasicTable** aimed at displaying tables data when all the data is in memory. It takes the full set of data (all possible items) and based on its configuration, will display it handling all configured functionality (pagination and sorting) for you. +**EuiInMemoryTable** is a higher level component wrapper around [**EuiBasicTable**](../basic) aimed at automatically handling certain functionality (selection, search, sorting, and pagination) in-memory for you, within certain preset configurations. It takes the full set of data that must include all possible items. :::warning Column names must be referentially stable @@ -21,7 +22,6 @@ import { formatDate, EuiInMemoryTable, EuiBasicTableColumn, - EuiLink, EuiHealth, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -30,7 +30,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; dateOfBirth: Date; online: boolean; location: { @@ -46,7 +45,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { @@ -82,15 +80,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, { field: 'dateOfBirth', name: 'Date of Birth', @@ -159,7 +148,7 @@ import InMemoryTableSelection from './table_selection'; ## In-memory table with search -The example shows how to configure **EuiInMemoryTable** to display a search bar by passing the search prop. You can read more about the search bar's properties and its syntax [**here**](/docs/forms/search-bar) . +This example shows how to configure **EuiInMemoryTable** to display a search bar by passing the `search` prop. For more detailed information about the syntax and configuration accepted by this prop, see [**EuiSearchBar**](../../forms/search-bar). ```tsx interactive import React, { useState } from 'react'; @@ -168,7 +157,6 @@ import { EuiInMemoryTable, EuiBasicTableColumn, EuiSearchBarProps, - EuiLink, EuiHealth, EuiSpacer, EuiSwitch, @@ -182,7 +170,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; dateOfBirth: Date; online: boolean; location: string; @@ -196,7 +183,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: faker.location.country(), @@ -235,15 +221,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, { field: 'dateOfBirth', name: 'Date of Birth', @@ -315,8 +292,9 @@ export default () => { /> setFilters(!filters)} + disabled={textSearchFormat} /> { ); }; - ``` ## In-memory table with search callback @@ -369,7 +346,6 @@ import { EuiInMemoryTable, EuiBasicTableColumn, EuiSearchBarProps, - EuiLink, EuiHealth, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -378,7 +354,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; dateOfBirth: Date; online: boolean; location: { @@ -394,7 +369,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { @@ -430,15 +404,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, { field: 'dateOfBirth', name: 'Date of Birth', @@ -519,7 +484,6 @@ export default () => { /> ); }; - ``` ## In-memory table with search and external state @@ -533,7 +497,6 @@ import { EuiInMemoryTable, EuiBasicTableColumn, EuiSearchBarProps, - EuiLink, EuiHealth, EuiFlexGroup, EuiFlexItem, @@ -573,8 +536,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; - dateOfBirth: Date; online: boolean; location: string; locationData: (typeof countries)[number]; @@ -589,8 +550,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), - dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: randomCountry.code, locationData: randomCountry, @@ -623,23 +582,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, - { - field: 'dateOfBirth', - name: 'Date of Birth', - dataType: 'date', - render: (dateOfBirth: User['dateOfBirth']) => - formatDate(dateOfBirth, 'dobLong'), - sortable: true, - }, { field: 'location', name: 'Location', @@ -794,7 +736,6 @@ export default () => { ); }; - ``` ## In-memory table with custom sort values @@ -859,7 +800,6 @@ export default () => { /> ); }; - ``` ## In-memory table with controlled pagination @@ -876,7 +816,6 @@ import { EuiInMemoryTableProps, EuiBasicTableColumn, CriteriaWithPagination, - EuiLink, EuiHealth, } from '@elastic/eui'; import { faker } from '@faker-js/faker'; @@ -885,7 +824,6 @@ type User = { id: number; firstName: string | null | undefined; lastName: string; - github: string; dateOfBirth: Date; online: boolean; location: { @@ -901,7 +839,6 @@ for (let i = 0; i < 20; i++) { id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), location: { @@ -937,15 +874,6 @@ const columns: Array> = [ show: false, }, }, - { - field: 'github', - name: 'Github', - render: (username: User['github']) => ( - - {username} - - ), - }, { field: 'dateOfBirth', name: 'Date of Birth', @@ -1022,7 +950,6 @@ export default () => { /> ); }; - ``` ## Props diff --git a/packages/website/docs/components/tabular_content/tables/overview.mdx b/packages/website/docs/components/tabular_content/tables/overview.mdx new file mode 100644 index 00000000000..85ae291b6c1 --- /dev/null +++ b/packages/website/docs/components/tabular_content/tables/overview.mdx @@ -0,0 +1,16 @@ +--- +slug: /tabular-content/tables +id: tabular_content_tables +--- + +# Tables + +Tables can get complicated very fast. EUI provides both opinionated and non-opinionated ways to build tables. + +- **Opinionated high-level components:** + - These high-level components removes the need to worry about constructing individual building blocks together. You simply arrange your data in the format it asks for. + - [**EuiBasicTable**](./basic) handles mobile row selection, row actions, row expansion, and mobile UX out of the box with relatively simple-to-use APIs. It is best used with asynchronous data, or static datasets that do not need pagination/sorting. + - [**EuiInMemoryTable**](./in-memory) has all the features that EuiBasicTable has, and additionally handles pagination, sorting, and searching the passed data out-of-the-box with relatively minimal APIs. It is best used with smaller synchronous datasets. +- **Non-opinionated building blocks:** + - If your table requires completely custom behavior, you can use individual building block components like [EuiTable, EuiTableRow, EuiTableRowCell, and more](./custom) to do what you need. + - Please note that if you go this route, you must handle your own data management as well as table accessibility and mobile UX. diff --git a/packages/website/docs/components/tabular_content/table_selection.tsx b/packages/website/docs/components/tabular_content/tables/table_selection.tsx similarity index 80% rename from packages/website/docs/components/tabular_content/table_selection.tsx rename to packages/website/docs/components/tabular_content/tables/table_selection.tsx index ff9d5106530..8293cec5b47 100644 --- a/packages/website/docs/components/tabular_content/table_selection.tsx +++ b/packages/website/docs/components/tabular_content/tables/table_selection.tsx @@ -5,7 +5,11 @@ import { EuiSwitch } from '@elastic/eui'; // @ts-expect-error Docusaurus theme is missing types for this component import { Demo } from '@elastic/eui-docusaurus-theme/lib/components/demo'; -const userDataSetup = (varName: string = 'users', isControlled: boolean) => ` +const userDataSetup = ( + varName: string = 'users', + count: number = 20, + isControlled: boolean +) => ` type User = { id: number; firstName: string | null | undefined; @@ -19,12 +23,12 @@ type User = { const ${varName}: User[] = []; -for (let i = 0; i < 20; i++) { +for (let i = 0; i < ${count}; i++) { ${varName}.push({ id: i + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), - online: faker.datatype.boolean(), + online: i === 0 ? true : faker.datatype.boolean(), location: { city: faker.location.city(), country: faker.location.country(), @@ -137,7 +141,7 @@ import { EuiButton, } from '@elastic/eui'; -${userDataSetup('users', isControlled)} +${userDataSetup('users', 5, isControlled)} export default () => { /** @@ -154,87 +158,6 @@ export default () => { }); setSelectedItems([]); } - - /** - * Pagination & sorting - */ - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('firstName'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - - const onTableChange = ({ page, sort }: Criteria) => { - if (page) { - const { index: pageIndex, size: pageSize } = page; - setPageIndex(pageIndex); - setPageSize(pageSize); - } - if (sort) { - const { field: sortField, direction: sortDirection } = sort; - setSortField(sortField); - setSortDirection(sortDirection); - } - }; - - // Manually handle sorting and pagination of data - const findUsers = ( - users: User[], - pageIndex: number, - pageSize: number, - sortField: keyof User, - sortDirection: 'asc' | 'desc' - ) => { - let items; - - if (sortField) { - items = users - .slice(0) - .sort( - Comparators.property(sortField, Comparators.default(sortDirection)) - ); - } else { - items = users; - } - - let pageOfItems; - - if (!pageIndex && !pageSize) { - pageOfItems = items; - } else { - const startIndex = pageIndex * pageSize; - pageOfItems = items.slice( - startIndex, - Math.min(startIndex + pageSize, users.length) - ); - } - - return { - pageOfItems, - totalItemCount: users.length, - }; - }; - - const { pageOfItems, totalItemCount } = findUsers( - users, - pageIndex, - pageSize, - sortField, - sortDirection - ); - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [3, 5, 8], - }; - - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; return ( <> @@ -262,13 +185,10 @@ export default () => { tableCaption="Demo for an EuiBasicTable with ${ isControlled ? 'controlled' : 'uncontrolled' } selection" - items={pageOfItems} + items={users} itemId="id" rowHeader="firstName" columns={columns} - pagination={pagination} - sorting={sorting} - onChange={onTableChange} selection={selection} /> @@ -290,7 +210,7 @@ import { EuiButton, } from '@elastic/eui'; -${userDataSetup('userData', isControlled)} +${userDataSetup('userData', 20, isControlled)} export default () => { ${selectionSetup(isControlled)} diff --git a/packages/website/package.json b/packages/website/package.json index a3937745371..57d95effb2b 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -37,6 +37,7 @@ "moment": "^2.30.1", "moment-timezone": "^0.5.46", "prism-react-renderer": "^2.3.0", + "raw-loader": "^4.0.2", "react": "^18.0.0", "react-dom": "^18.0.0" }, diff --git a/packages/website/src/custom_typings/index.d.ts b/packages/website/src/custom_typings/index.d.ts new file mode 100644 index 00000000000..1ee41bdb8e1 --- /dev/null +++ b/packages/website/src/custom_typings/index.d.ts @@ -0,0 +1,4 @@ +declare module '!!raw-loader!*' { + const content: string; + export default content; +} diff --git a/yarn.lock b/yarn.lock index 9a168e028e1..674d5de1b69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4670,6 +4670,7 @@ __metadata: moment: "npm:^2.30.1" moment-timezone: "npm:^0.5.46" prism-react-renderer: "npm:^2.3.0" + raw-loader: "npm:^4.0.2" react: "npm:^18.0.0" react-dom: "npm:^18.0.0" typescript: "npm:~5.5.4" @@ -28885,6 +28886,18 @@ __metadata: languageName: node linkType: hard +"raw-loader@npm:^4.0.2": + version: 4.0.2 + resolution: "raw-loader@npm:4.0.2" + dependencies: + loader-utils: "npm:^2.0.0" + schema-utils: "npm:^3.0.0" + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + checksum: 10c0/981ebe65e1cee7230300d21ba6dcd8bd23ea81ef4ad2b167c0f62d93deba347f27921d330be848634baab3831cf9f38900af6082d6416c2e937fe612fa6a74ff + languageName: node + linkType: hard + "rc@npm:1.2.8, rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8"