diff --git a/CHANGELOG.md b/CHANGELOG.md index f09fddc01be..f538b9c4108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ ## [`main`](https://github.com/elastic/eui/tree/main) -No public interface changes since `48.1.1`. +- Added new `renderCellPopover` prop to `EuiDataGrid` ([#5640](https://github.com/elastic/eui/pull/5640)) +- Added cell `schema` info to `EuiDataGrid`'s `renderCellValue` props ([#5640](https://github.com/elastic/eui/pull/5640)) + +**Breaking changes** + +- Removed `popoverContents` props from `EuiDataGrid` (use new `renderCellPopover` instead) ([#5640](https://github.com/elastic/eui/pull/5640)) ## [`48.1.1`](https://github.com/elastic/eui/tree/v48.1.1) diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 5e39463694b..4fea864ea23 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -83,6 +83,7 @@ import { CopyExample } from './views/copy/copy_example'; import { DataGridExample } from './views/datagrid/datagrid_example'; import { DataGridMemoryExample } from './views/datagrid/datagrid_memory_example'; import { DataGridSchemaExample } from './views/datagrid/datagrid_schema_example'; +import { DataGridCellPopoverExample } from './views/datagrid/datagrid_cell_popover_example'; import { DataGridFocusExample } from './views/datagrid/datagrid_focus_example'; import { DataGridStylingExample } from './views/datagrid/datagrid_styling_example'; import { DataGridControlColumnsExample } from './views/datagrid/datagrid_controlcolumns_example'; @@ -486,6 +487,7 @@ const navigation = [ DataGridExample, DataGridMemoryExample, DataGridSchemaExample, + DataGridCellPopoverExample, DataGridFocusExample, DataGridStylingExample, DataGridControlColumnsExample, diff --git a/src-docs/src/views/datagrid/cell_popover_is_details.tsx b/src-docs/src/views/datagrid/cell_popover_is_details.tsx new file mode 100644 index 00000000000..a8af66dfeb5 --- /dev/null +++ b/src-docs/src/views/datagrid/cell_popover_is_details.tsx @@ -0,0 +1,96 @@ +import React, { useState, ReactNode } from 'react'; +// @ts-ignore - faker does not have type declarations +import { fake } from 'faker'; + +import { + EuiDataGrid, + EuiDataGridCellValueElementProps, + EuiDataGridColumnCellAction, + EuiDataGridColumn, + EuiTitle, +} from '../../../../src/components'; + +const cellActions: EuiDataGridColumnCellAction[] = [ + ({ Component }) => ( + + Filter in + + ), + ({ Component }) => ( + + Filter out + + ), +]; + +const columns: EuiDataGridColumn[] = [ + { + id: 'default', + cellActions, + }, + { + id: 'datetime', + cellActions, + }, + { + id: 'json', + schema: 'json', + cellActions, + }, + { + id: 'custom', + schema: 'favoriteFranchise', + cellActions, + }, +]; + +const data: Array<{ [key: string]: ReactNode }> = []; +for (let i = 1; i < 5; i++) { + data.push({ + default: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + datetime: fake('{{date.past}}'), + json: JSON.stringify([ + { + numeric: fake('{{finance.account}}'), + currency: fake('${{finance.amount}}'), + date: fake('{{date.past}}'), + }, + ]), + custom: i % 2 === 0 ? 'Star Wars' : 'Star Trek', + }); +} + +const RenderCellValue = ({ + rowIndex, + columnId, + schema, + isDetails, +}: EuiDataGridCellValueElementProps) => { + let value = data[rowIndex][columnId]; + + if (schema === 'favoriteFranchise' && isDetails) { + value = ( + +

{value} is the best!

+
+ ); + } + + return value; +}; + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + return ( + + ); +}; diff --git a/src-docs/src/views/datagrid/cell_popover_is_expandable.tsx b/src-docs/src/views/datagrid/cell_popover_is_expandable.tsx new file mode 100644 index 00000000000..aeb7e31c3f2 --- /dev/null +++ b/src-docs/src/views/datagrid/cell_popover_is_expandable.tsx @@ -0,0 +1,68 @@ +import React, { useState, ReactNode } from 'react'; +// @ts-ignore - faker does not have type declarations +import { fake } from 'faker'; + +import { + EuiDataGrid, + EuiDataGridColumnCellAction, + EuiDataGridColumn, +} from '../../../../src/components'; + +const cellActions: EuiDataGridColumnCellAction[] = [ + ({ Component }) => ( + + Filter in + + ), + ({ Component }) => ( + + Filter out + + ), +]; + +const columns: EuiDataGridColumn[] = [ + { + id: 'firstName', + cellActions, + }, + { + id: 'lastName', + isExpandable: false, + cellActions, + }, + { + id: 'suffix', + isExpandable: false, + }, + { + id: 'boolean', + isExpandable: false, + }, +]; + +const data: Array<{ [key: string]: ReactNode }> = []; +for (let i = 1; i < 5; i++) { + data.push({ + firstName: fake('{{name.firstName}}'), + lastName: fake('{{name.lastName}}'), + suffix: fake('{{name.suffix}}'), + boolean: fake('{{random.boolean}}'), + }); +} + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + return ( + data[rowIndex][columnId]} + /> + ); +}; diff --git a/src-docs/src/views/datagrid/cell_popover_rendercellpopover.tsx b/src-docs/src/views/datagrid/cell_popover_rendercellpopover.tsx new file mode 100644 index 00000000000..78b386a95f9 --- /dev/null +++ b/src-docs/src/views/datagrid/cell_popover_rendercellpopover.tsx @@ -0,0 +1,176 @@ +import React, { useState, ReactNode } from 'react'; +// @ts-ignore - faker does not have type declarations +import { fake } from 'faker'; + +import { + EuiDataGrid, + EuiDataGridCellPopoverElementProps, + EuiDataGridColumnCellAction, + EuiDataGridColumn, + EuiPopoverTitle, + EuiPopoverFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiCopy, + EuiText, + EuiImage, +} from '../../../../src/components'; + +const cellActions: EuiDataGridColumnCellAction[] = [ + ({ Component }) => ( + + Filter in + + ), + ({ Component }) => ( + + Filter out + + ), +]; + +const columns: EuiDataGridColumn[] = [ + { + id: 'default', + cellActions, + }, + { + id: 'datetime', + cellActions, + }, + { + id: 'json', + cellActions, + }, + { + id: 'custom', + schema: 'favoriteFranchise', + cellActions, + }, +]; + +const data: Array<{ [key: string]: ReactNode }> = []; +for (let i = 1; i < 5; i++) { + data.push({ + default: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + datetime: fake('{{date.past}}'), + json: JSON.stringify([ + { + numeric: fake('{{finance.account}}'), + currency: fake('${{finance.amount}}'), + date: fake('{{date.past}}'), + }, + ]), + custom: i % 2 === 0 ? 'Star Wars' : 'Star Trek', + }); +} + +const RenderCellPopover = (props: EuiDataGridCellPopoverElementProps) => { + const { + columnId, + schema, + children, + cellActions, + cellContentsElement, + DefaultCellPopover, + } = props; + + let title: ReactNode = 'Custom popover'; + let content: ReactNode = {children}; + let footer: ReactNode = cellActions; + + // An example of custom popover content + if (schema === 'favoriteFranchise') { + title = 'Custom popover with custom content'; + const franchise = cellContentsElement.innerText; + const caption = `${franchise} is the best!`; + content = ( + <> + {franchise === 'Star Wars' ? ( + + ) : ( + + )} + + ); + } + + // An example of a custom cell actions footer, and of using + // `cellContentsElement` to directly access a cell's raw text + if (columnId === 'datetime') { + title = 'Custom popover with custom actions'; + footer = ( + + + + {/* When not using the default cellActions, be sure to replace them + with your own action buttons to ensure a consistent user experience */} + Filter in + Filter out + + + + {(copy) => ( + + Click to copy + + )} + + + + + ); + } + + // An example of conditionally falling back back to the default cell popover render. + // Note that JSON schemas have automatic EuiCodeBlock and isCopyable formatting + // which can be nice to keep intact. For cells that have non-JSON content but + // JSON popovers, you can also manually pass a `json` schema to force this formatting. + if (columnId === 'json') { + return ; + } + + return ( + <> + {title} + {content} + {footer} + + ); +}; + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + return ( + data[rowIndex][columnId]} + renderCellPopover={RenderCellPopover} + /> + ); +}; diff --git a/src-docs/src/views/datagrid/datagrid_cell_popover_example.js b/src-docs/src/views/datagrid/datagrid_cell_popover_example.js new file mode 100644 index 00000000000..4825c869604 --- /dev/null +++ b/src-docs/src/views/datagrid/datagrid_cell_popover_example.js @@ -0,0 +1,158 @@ +import React from 'react'; + +import { GuideSectionTypes } from '../../components'; +import { EuiCode, EuiCallOut, EuiSpacer } from '../../../../src/components'; + +import IsDetailsPopover from './cell_popover_is_details'; +const IsDetailsPopoverSource = require('!!raw-loader!./cell_popover_is_details'); + +import RenderCellPopover from './cell_popover_rendercellpopover'; +const renderCellPopoverSource = require('!!raw-loader!./cell_popover_rendercellpopover'); + +import IsExpandablePopover from './cell_popover_is_expandable'; +const isExpandablePopoverSource = require('!!raw-loader!./cell_popover_is_expandable'); + +import { + EuiDataGridCellPopoverElementProps, + EuiDataGridCellValueElementProps, + EuiDataGridColumn, +} from '!!prop-loader!../../../../src/components/datagrid/data_grid_types'; + +export const DataGridCellPopoverExample = { + title: 'Data grid cell popovers', + sections: [ + { + title: 'Conditionally customizing cell popover content', + text: ( + <> +

+ Cell popover content values can be conditionally customized using + the isDetails flag passed to{' '} + renderCellValue. If you need basic customization + of cell popover values based on, e.g. schema or column, this is the + most straightforward approach. +

+

+ By default, all cell popover contents are rendered with an{' '} + EuiText wrapper, and cell actions are rendered + within an EuiPopoverFooter as{' '} + EuiEmptyButtons. Columns with a{' '} + json schema will additionally have an automatic + formatter that indents and displays the popover content within an{' '} + EuiCodeBlock. +

+ + ), + demo: , + components: { IsDetailsPopover }, + source: [ + { + type: GuideSectionTypes.TSX, + code: IsDetailsPopoverSource, + }, + ], + props: { + EuiDataGridCellValueElementProps, + }, + }, + { + title: 'Completely customizing cell popover rendering', + text: ( + <> +

+ If you want complete control over the rendering of the entire cell + popover, use the renderCellPopover prop to pass a + component. This allows you to do things like set your own wrappers + and replace the default cell actions rendering with your own. +

+

+ To make falling back to atoms of the default cell popover easier, + several props are passed to your custom{' '} + renderCellPopover function: +

+
    +
  • +

    + children - the direct JSX output of the + cell's returned renderCellValue. It can + be used (e.g.) if you want a custom wrapper or cell actions, but + default popover content. +

    +
  • +
  • +

    + cellContentsElement - a direct reference to + the cell's HTML content node, which allows accessing the + cell's innerText for cases where raw + non-JSX text is useful (e.g. copying). +

    +
  • +
  • + cellActions - the direct JSX output of the + default popover footer and buttons. Use this prop if you want + custom popover content but default cell actions. + + If deliberately leaving out the default{' '} + cellActions, you must{' '} + re-implement your cell actions in the popover in some form. + Leaving out cell actions available in the cell but not in the + popover introduces UX inconsistencies and will confuse the end + user. + + +
  • +
  • +

    + DefaultCellPopover - the default popover + component. Use this component if you only want custom popover + content for certain schemas or columns and default popover + rendering for other cells. +

    +
  • +
+

+ Take a look at the below example's demo code to see the above + props in action. +

+ + ), + components: { RenderCellPopover }, + demo: , + source: [ + { + type: GuideSectionTypes.TSX, + code: renderCellPopoverSource, + }, + ], + props: { + EuiDataGridCellPopoverElementProps, + }, + }, + { + title: 'Disabling cell expansion popovers', + text: ( +

+ Popovers can sometimes be unnecessary for short form content. In the + example below we've turned them off by setting{' '} + isExpandable=false on specific{' '} + columns. +

+ ), + demo: , + components: { IsExpandablePopover }, + source: [ + { + type: GuideSectionTypes.TSX, + code: isExpandablePopoverSource, + }, + ], + props: { + EuiDataGridColumn, + }, + }, + ], +}; diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index 3ecd3c20e5b..f0279f67dfa 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -27,19 +27,17 @@ import { EuiDataGridToolBarAdditionalControlsLeftOptions, EuiDataGridColumnVisibility, EuiDataGridColumnActions, - EuiDataGridPopoverContentProps, EuiDataGridControlColumn, EuiDataGridToolBarVisibilityColumnSelectorOptions, EuiDataGridRowHeightsOptions, EuiDataGridCellValueElementProps, + EuiDataGridCellPopoverElementProps, EuiDataGridSchemaDetector, EuiDataGridRefProps, } from '!!prop-loader!../../../../src/components/datagrid/data_grid_types'; const gridSnippet = ` alert('test') }]} + { id: 'A', initialWidth: 150, isResizable: false, actions: false }, + { id: 'B', isExpandable: false, actions: { showMoveLeft: false, showMoveRight: false } }, + { id: 'C', schema: 'franchise', cellActions: [{ label: 'test', iconType: 'heart', callback: ()=> alert('test') }]} ]} - // Optional. This allows you to initially hide columns. Users can still turn them on. + // Required. Determines column visibility state. Allows you to initially hide columns, although users can still turn them on. columnVisibility={{ visibleColumns: ['A', 'C'], setVisibleColumns: () => {}, }} + // Optional leadingControlColumns={[ { id: 'selection', @@ -64,6 +63,7 @@ const gridSnippet = ` rowCellRender: () =>
, }, ]} + // Optional trailingControlColumns={[ { id: 'actions', @@ -72,11 +72,27 @@ const gridSnippet = ` rowCellRender: MyGridActionsComponent, }, ]} - // Optional. Customize the content inside the cell. The current example outputs the row and column position. - // Often used in combination with useEffect() to dynamically change the render. + // Required. Renders the content of each cell. The current example outputs the row and column position. + // Treated as a React component allowing hooks, context, and other React concepts to be used. renderCellValue={({ rowIndex, columnId }) => - \`\${rowIndex}, \${columnId}\` + \`\${rowIndex}, \${columnId}\` } + // Optional. Customizes the content of each cell expansion popover. + // Treated as a React component allowing hooks, context, and other React concepts to be used. + renderCellPopover={({ children, cellActions }) => ( + <> + I'm a custom popover! + {children} + {cellActions} + + )} + // Optional. Will try to autodectect schemas and do sorting and pagination in memory. + inMemory={{ level: 'sorting' }} + // Optional, but required when inMemory is set. Provides the sort and gives a callback for when it changes in the grid. + sorting={{ + columns: [{ id: 'C', direction: 'asc' }], + onSort: () => {}, + }} // Optional. Add pagination. pagination={{ pageIndex: 1, @@ -85,11 +101,6 @@ const gridSnippet = ` onChangePage: () => {}, onChangeItemsPerPage: () => {}, }} - // Optional, but required when inMemory is set. Provides the sort and gives a callback for when it changes in the grid. - sorting={{ - columns: [{ id: 'C', direction: 'asc' }], - onSort: () => {}, - }} // Optional. Allows you to configure what features the toolbar shows. // The prop also accepts a boolean if you want to toggle the entire toolbar on/off. toolbarVisibility={{ @@ -115,7 +126,7 @@ const gridSnippet = ` rowHeightsOptions={{ defaultHeight: 34, rowHeights: { - 0: auto + 0: 'auto', }, lineHeight: '1em', }} @@ -149,22 +160,6 @@ const gridSnippet = ` color: '#000000', }, ]} - // Optional. Mapped against the schema, provide custom layout and/or content for the popover. - popoverContents={{ - numeric: ({ children, cellContentsElement }) => { - // \`children\` is the datagrid's \`renderCellValue\` as a ReactElement and should be used when you are only wrapping the contents - // \`cellContentsElement\` is the cell's existing DOM element and can be used to extract the text value for processing, as below - - // want to process the already-rendered cell value - const stringContents = cellContentsElement.textContent; - - // extract the groups-of-three digits that are right-aligned - return stringContents.replace(/((\\d{3})+)$/, match => - // then replace each group of xyz digits with ,xyz - match.replace(/(\\d{3})/g, ',$1') - ); - }, - }} // Optional. For advanced control of internal data grid popover/focus state, passes back an object of API methods ref={dataGridRef} /> @@ -221,16 +216,6 @@ const gridConcepts = [ ), }, - { - title: 'popoverContents', - description: ( - - An object mapping EuiDataGridColumn schemas to a custom - popover render. This dictates the content of the popovers when you click - into each cell. - - ), - }, { title: 'rowCount', description: @@ -292,8 +277,22 @@ const gridConcepts = [ A function called to render a cell's value. Behind the scenes it is treated as a React component allowing hooks, context, and other React - concepts to be used. The function receives a{' '} - EuiDataGridCellValueElement as its only argument. + concepts to be used. The function receives{' '} + EuiDataGridCellValueElementProps as its only argument. + + ), + }, + { + title: 'renderCellPopover', + description: ( + + An optional function called to render a cell's popover. Behind the + scenes it is treated as a React component, receiving{' '} + EuiDataGridCellPopoverElementProps as its props. See{' '} + + Data grid cell popovers + {' '} + for more details and examples. ), }, @@ -422,13 +421,13 @@ export const DataGridExample = { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridCellValueElementProps, + EuiDataGridCellPopoverElementProps, EuiDataGridSchemaDetector, EuiDataGridStyle, EuiDataGridToolBarVisibilityOptions, EuiDataGridToolBarVisibilityColumnSelectorOptions, EuiDataGridToolBarAdditionalControlsOptions, EuiDataGridToolBarAdditionalControlsLeftOptions, - EuiDataGridPopoverContentProps, EuiDataGridRowHeightsOptions, EuiDataGridRefProps, }, diff --git a/src-docs/src/views/datagrid/datagrid_schema_example.js b/src-docs/src/views/datagrid/datagrid_schema_example.js index 249b3552f09..865191c5e79 100644 --- a/src-docs/src/views/datagrid/datagrid_schema_example.js +++ b/src-docs/src/views/datagrid/datagrid_schema_example.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { GuideSectionTypes } from '../../components'; import { EuiDataGrid, EuiCode } from '../../../../src/components'; @@ -9,18 +9,13 @@ const dataGridSchemaSource = require('!!raw-loader!./schema'); import { EuiDataGridColumn, EuiDataGridColumnActions, - EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridInMemory, - EuiDataGridStyle, - EuiDataGridToolBarVisibilityOptions, - EuiDataGridColumnVisibility, EuiDataGridSchemaDetector, - EuiDataGridCellValueElementProps, } from '!!prop-loader!../../../../src/components/datagrid/data_grid_types'; export const DataGridSchemaExample = { - title: 'Data grid schemas and popovers', + title: 'Data grid schemas', sections: [ { source: [ @@ -30,7 +25,7 @@ export const DataGridSchemaExample = { }, ], text: ( - + <>

Schemas are data types you pass to grid columns to affect how the columns and expansion popovers render. Schemas also allow you to @@ -60,36 +55,16 @@ export const DataGridSchemaExample = { {' '} to each matching cell.

-

Defining expansion

-

- Likewise, you can inject custom content into any of the popovers a - cell expands into. Add popoverContents functions - to populate a matching schema's popover using a new component. - You can see an example of this by clicking into one of the cells in - the last column below. -

-

Disabling expansion popovers

-

- Often the popovers are unnecessary for short form content. In the - example below we've turned them off by setting{' '} - isExpandable=false on the boolean - column. -

-
+ ), components: { DataGridSchema }, props: { + EuiDataGridSchemaDetector, EuiDataGrid, EuiDataGridInMemory, EuiDataGridColumn, EuiDataGridColumnActions, - EuiDataGridColumnVisibility, - EuiDataGridPaginationProps, EuiDataGridSorting, - EuiDataGridCellValueElementProps, - EuiDataGridSchemaDetector, - EuiDataGridStyle, - EuiDataGridToolBarVisibilityOptions, }, demo: , }, diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 0139a811f6f..e4a8fbb86e8 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -1,12 +1,7 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { fake } from 'faker'; -import { - EuiDataGrid, - EuiImage, - EuiTitle, - EuiSpacer, -} from '../../../../src/components/'; +import { EuiDataGrid } from '../../../../src/components/'; const columns = [ { @@ -14,7 +9,6 @@ const columns = [ }, { id: 'boolean', - isExpandable: false, }, { id: 'numeric', @@ -38,11 +32,13 @@ const columns = [ const storeData = []; for (let i = 1; i < 5; i++) { - let json; - let franchise; - if (i < 3) { - franchise = 'Star Wars'; - json = JSON.stringify([ + storeData.push({ + default: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + boolean: fake('{{random.boolean}}'), + numeric: fake('{{finance.account}}'), + currency: fake('${{finance.amount}}'), + datetime: fake('{{date.past}}'), + json: JSON.stringify([ { default: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), boolean: fake('{{random.boolean}}'), @@ -51,70 +47,28 @@ for (let i = 1; i < 5; i++) { date: fake('{{date.past}}'), custom: fake('{{date.past}}'), }, - ]); - } else { - franchise = 'Star Trek'; - json = JSON.stringify([ - { - name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), - }, - ]); - } - - storeData.push({ - default: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), - boolean: fake('{{random.boolean}}'), - numeric: fake('{{finance.account}}'), - currency: fake('${{finance.amount}}'), - datetime: fake('{{date.past}}'), - json: json, - custom: franchise, + ]), + custom: i % 2 === 0 ? 'Star Wars' : 'Star Trek', }); } -const Franchise = (props) => { - return ( -
- -

{props.name} is the best!

-
- - {props.name === 'Star Wars' ? ( - - ) : ( - - )} -
+const commaSeparateNumbers = (numberString) => { + // extract the groups-of-three digits that are right-aligned + return numberString.replace(/((\d{3})+)$/, (match) => + // then replace each group of xyz digits with ,xyz + match.replace(/(\d{3})/g, ',$1') ); }; -const DataGridSchema = () => { - const [data, setData] = useState(storeData); +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + const [data, setData] = useState(storeData); const [sortingColumns, setSortingColumns] = useState([ { id: 'custom', direction: 'asc' }, ]); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, - }); - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) - ); const setSorting = (sortingColumns) => { const sortedData = [...data].sort((a, b) => { @@ -134,49 +88,23 @@ const DataGridSchema = () => { setSortingColumns(sortingColumns); }; - const setPageIndex = useCallback( - (pageIndex) => { - setPagination({ ...pagination, pageIndex }); - }, - [pagination, setPagination] - ); - - const setPageSize = useCallback( - (pageSize) => { - setPagination({ ...pagination, pageIndex: 0, pageSize }); - }, - [pagination, setPagination] - ); - - const handleVisibleColumns = (visibleColumns) => - setVisibleColumns(visibleColumns); - return ( { - const value = data[rowIndex][columnId]; + renderCellValue={({ rowIndex, columnId, schema }) => { + let value = data[rowIndex][columnId]; - if (columnId === 'custom' && isDetails) { - return ; + if (schema === 'numeric') { + value = commaSeparateNumbers(value); } return value; }} sorting={{ columns: sortingColumns, onSort: setSorting }} - pagination={{ - ...pagination, - pageSizeOptions: [5, 10, 25], - onChangeItemsPerPage: setPageSize, - onChangePage: setPageIndex, - }} schemaDetectors={[ { type: 'favoriteFranchise', @@ -200,19 +128,6 @@ const DataGridSchema = () => { color: '#800080', }, ]} - popoverContents={{ - numeric: ({ cellContentsElement }) => { - // want to process the already-rendered cell value - const stringContents = cellContentsElement.textContent; - - // extract the groups-of-three digits that are right-aligned - return stringContents.replace(/((\d{3})+)$/, (match) => - // then replace each group of xyz digits with ,xyz - match.replace(/(\d{3})/g, ',$1') - ); - }, - }} /> ); }; -export default DataGridSchema; diff --git a/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap b/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap index 3fb1bf8932d..f44c7d44ea8 100644 --- a/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap +++ b/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap @@ -6,7 +6,6 @@ exports[`EuiDataGridCell renders 1`] = ` columnId="someColumn" interactiveCellId="someId" isExpandable={true} - popoverContent={[Function]} popoverContext={ Object { "cellLocation": Object { @@ -173,7 +172,7 @@ exports[`EuiDataGridCell renders 1`] = ` exports[`EuiDataGridCell componentDidUpdate handles the cell popover by forwarding the cell's DOM node and contents to the parent popover context 1`] = ` Array [
), - popoverContent: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), popoverContext: mockPopoverContext, rowHeightUtils: mockRowHeightUtils, }; @@ -51,7 +48,7 @@ describe('EuiDataGridCell', () => { expect(component).toMatchSnapshot(); }); - it('renders cell buttons', () => { + it('renders cell actions', () => { const component = mount( { it('renderCellValue', () => { component.setProps({ renderCellValue: () =>
test
}); }); + it('renderCellPopover', () => { + component.setProps({ renderCellPopover: () =>
test
}); + }); it('interactiveCellId', () => { component.setProps({ interactiveCellId: 'test' }); }); - it('popoverContent', () => { - component.setProps({ popoverContent: () =>
test
}); - }); it('popoverContext.popoverIsOpen', () => { component.setProps({ popoverContext: { ...mockPopoverContext, popoverIsOpen: true }, diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index 6bab64e6610..ff6d836595f 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -31,11 +31,13 @@ import { EuiDataGridCellState, EuiDataGridCellValueElementProps, EuiDataGridCellValueProps, + EuiDataGridCellPopoverElementProps, } from '../data_grid_types'; import { EuiDataGridCellActions, EuiDataGridCellPopoverActions, } from './data_grid_cell_actions'; +import { DefaultCellPopover } from './data_grid_cell_popover'; import { IS_JEST_ENVIRONMENT } from '../../../test'; const EuiDataGridCellContent: FunctionComponent< @@ -89,6 +91,7 @@ const EuiDataGridCellContent: FunctionComponent< data-test-subj="cell-content" rowIndex={rowIndex} colIndex={colIndex} + schema={column?.schema || rest.columnType} {...rest} />
@@ -292,7 +295,8 @@ export class EuiDataGridCell extends Component< this.props.popoverContext.popoverIsOpen !== prevProps.popoverContext.popoverIsOpen || this.props.popoverContext.cellLocation !== - prevProps.popoverContext.cellLocation + prevProps.popoverContext.cellLocation || + this.props.renderCellPopover !== prevProps.renderCellPopover ) { this.handleCellPopover(); } @@ -315,9 +319,10 @@ export class EuiDataGridCell extends Component< if (nextProps.rowHeightsOptions !== this.props.rowHeightsOptions) return true; if (nextProps.renderCellValue !== this.props.renderCellValue) return true; + if (nextProps.renderCellPopover !== this.props.renderCellPopover) + return true; if (nextProps.interactiveCellId !== this.props.interactiveCellId) return true; - if (nextProps.popoverContent !== this.props.popoverContent) return true; if ( nextProps.popoverContext.popoverIsOpen !== this.props.popoverContext.popoverIsOpen || @@ -442,35 +447,44 @@ export class EuiDataGridCell extends Component< // Set popover contents with cell content const { - popoverContent: PopoverContent, + renderCellPopover, renderCellValue, rowIndex, colIndex, column, columnId, + columnType, } = this.props; + const PopoverElement = (renderCellPopover || + DefaultCellPopover) as JSXElementConstructor< + EuiDataGridCellPopoverElementProps + >; const CellElement = renderCellValue as JSXElementConstructor< EuiDataGridCellValueElementProps >; + const sharedProps = { + rowIndex, + colIndex, + columnId, + schema: column?.schema || columnType, + }; const popoverContent = ( - <> - - - - + } + DefaultCellPopover={DefaultCellPopover} + > + - + ); setPopoverContent(popoverContent); } @@ -480,7 +494,6 @@ export class EuiDataGridCell extends Component< const { width, isExpandable, - popoverContent, popoverContext: { closeCellPopover, openCellPopover }, interactiveCellId, columnType, @@ -594,7 +607,7 @@ export class EuiDataGridCell extends Component< ...rest, setCellProps: this.setCellProps, column, - columnType: columnType, + columnType, isExpandable, isExpanded: popoverIsOpen, isDetails: false, diff --git a/src/components/datagrid/body/data_grid_cell_popover.test.tsx b/src/components/datagrid/body/data_grid_cell_popover.test.tsx index ddd535a3e43..c519f10cab7 100644 --- a/src/components/datagrid/body/data_grid_cell_popover.test.tsx +++ b/src/components/datagrid/body/data_grid_cell_popover.test.tsx @@ -13,7 +13,7 @@ import { keys } from '../../../services'; import { testCustomHook } from '../../../test/test_custom_hook.test_helper'; import { DataGridCellPopoverContextShape } from '../data_grid_types'; -import { useCellPopover } from './data_grid_cell_popover'; +import { useCellPopover, DefaultCellPopover } from './data_grid_cell_popover'; describe('useCellPopover', () => { describe('openCellPopover', () => { @@ -202,3 +202,55 @@ describe('useCellPopover', () => { }); }); }); + +describe('popover content renderers', () => { + const cellContentsElement = document.createElement('div'); + cellContentsElement.innerText = '{ "hello": "world" }'; + const props = { + rowIndex: 0, + colIndex: 0, + columnId: 'someId', + schema: null, + children:
Content
, + cellActions:
Action
, + cellContentsElement, + DefaultCellPopover, + }; + + test('default cell popover', () => { + const component = shallow(); + expect(component).toMatchInlineSnapshot(` + + +
+ Content +
+
+
+ Action +
+
+ `); + }); + + test('JSON schema popover', () => { + const component = shallow(); + const codeBlock = component.find('JsonPopoverContent').dive(); + expect(codeBlock).toMatchInlineSnapshot(` + + { + "hello": "world" + } + + `); + }); +}); diff --git a/src/components/datagrid/body/data_grid_cell_popover.tsx b/src/components/datagrid/body/data_grid_cell_popover.tsx index f1a446d9c0c..73c93f46b8a 100644 --- a/src/components/datagrid/body/data_grid_cell_popover.tsx +++ b/src/components/datagrid/body/data_grid_cell_popover.tsx @@ -10,7 +10,10 @@ import React, { createContext, useState, useCallback, ReactNode } from 'react'; import { keys } from '../../../services'; import { EuiWrappingPopover } from '../../popover'; -import { DataGridCellPopoverContextShape } from '../data_grid_types'; +import { + DataGridCellPopoverContextShape, + EuiDataGridCellPopoverElementProps, +} from '../data_grid_types'; export const DataGridCellPopoverContext = createContext< DataGridCellPopoverContextShape @@ -94,3 +97,51 @@ export const useCellPopover = (): { return { cellPopoverContext, cellPopover }; }; + +/** + * Popover content renderers + */ +import { EuiText } from '../../text'; +import { EuiCodeBlock } from '../../code'; + +export const DefaultCellPopover = ({ + schema, + cellActions, + children, + cellContentsElement, +}: EuiDataGridCellPopoverElementProps) => { + switch (schema) { + case 'json': + return ( + <> + + {cellActions} + + ); + default: + return ( + <> + {children} + {cellActions} + + ); + } +}; + +export const JsonPopoverContent = ({ cellText }: { cellText: string }) => { + let formattedText = cellText; + try { + formattedText = JSON.stringify(JSON.parse(formattedText), null, 2); + } catch (e) {} // eslint-disable-line no-empty + + return ( + + {formattedText} + + ); +}; diff --git a/src/components/datagrid/body/data_grid_footer_row.test.tsx b/src/components/datagrid/body/data_grid_footer_row.test.tsx index 345a775ccb0..33af4964473 100644 --- a/src/components/datagrid/body/data_grid_footer_row.test.tsx +++ b/src/components/datagrid/body/data_grid_footer_row.test.tsx @@ -18,7 +18,6 @@ describe('EuiDataGridFooterRow', () => { trailingControlColumns: [], columns: [{ id: 'someColumn' }, { id: 'someColumnWithoutSchema' }], schema: { someColumn: { columnType: 'string' } }, - popoverContents: {}, columnWidths: { someColumn: 30 }, renderCellValue: () =>
, interactiveCellId: 'someId', @@ -41,7 +40,6 @@ describe('EuiDataGridFooterRow', () => { interactiveCellId="someId" isExpandable={true} key="someColumn-10" - popoverContent={[Function]} popoverContext={ Object { "cellLocation": Object { @@ -68,7 +66,6 @@ describe('EuiDataGridFooterRow', () => { interactiveCellId="someId" isExpandable={true} key="someColumnWithoutSchema-10" - popoverContent={[Function]} popoverContext={ Object { "cellLocation": Object { @@ -119,7 +116,6 @@ describe('EuiDataGridFooterRow', () => { interactiveCellId="someId" isExpandable={true} key="someLeadingColumn-10" - popoverContent={[Function]} popoverContext={ Object { "cellLocation": Object { @@ -176,7 +172,6 @@ describe('EuiDataGridFooterRow', () => { interactiveCellId="someId" isExpandable={true} key="someTrailingColumn-10" - popoverContent={[Function]} popoverContext={ Object { "cellLocation": Object { diff --git a/src/components/datagrid/body/data_grid_footer_row.tsx b/src/components/datagrid/body/data_grid_footer_row.tsx index ecfa63563a2..fea8bdc8ea7 100644 --- a/src/components/datagrid/body/data_grid_footer_row.tsx +++ b/src/components/datagrid/body/data_grid_footer_row.tsx @@ -10,7 +10,6 @@ import classnames from 'classnames'; import React, { forwardRef, memo, useContext } from 'react'; import { EuiDataGridCell } from './data_grid_cell'; import { DataGridCellPopoverContext } from './data_grid_cell_popover'; -import { DefaultColumnFormatter } from './popover_utils'; import { EuiDataGridFooterRowProps } from '../data_grid_types'; const EuiDataGridFooterRow = memo( @@ -21,11 +20,11 @@ const EuiDataGridFooterRow = memo( trailingControlColumns, columns, schema, - popoverContents, columnWidths, defaultColumnWidth, className, renderCellValue, + renderCellPopover, rowIndex, interactiveCellId, 'data-test-subj': _dataTestSubj, @@ -69,7 +68,6 @@ const EuiDataGridFooterRow = memo( key={`${id}-${rowIndex}`} colIndex={i} columnId={id} - popoverContent={DefaultColumnFormatter} width={width} renderCellValue={() => null} className="euiDataGridFooterCell euiDataGridRowCell--controlColumn" @@ -77,10 +75,6 @@ const EuiDataGridFooterRow = memo( ))} {columns.map(({ id }, i) => { const columnType = schema[id] ? schema[id].columnType : null; - const popoverContent = - (columnType && popoverContents[columnType]) || - DefaultColumnFormatter; - const width = columnWidths[id] || defaultColumnWidth; const columnPosition = i + leadingControlColumns.length; @@ -91,9 +85,9 @@ const EuiDataGridFooterRow = memo( colIndex={columnPosition} columnId={id} columnType={columnType} - popoverContent={popoverContent} width={width || undefined} renderCellValue={renderCellValue} + renderCellPopover={renderCellPopover} className="euiDataGridFooterCell" /> ); @@ -107,7 +101,6 @@ const EuiDataGridFooterRow = memo( key={`${id}-${rowIndex}`} colIndex={colIndex} columnId={id} - popoverContent={DefaultColumnFormatter} width={width} renderCellValue={() => null} className="euiDataGridFooterCell euiDataGridRowCell--controlColumn" diff --git a/src/components/datagrid/body/popover_utils.test.tsx b/src/components/datagrid/body/popover_utils.test.tsx deleted file mode 100644 index d27f7879ed4..00000000000 --- a/src/components/datagrid/body/popover_utils.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 from 'react'; -import { shallow } from 'enzyme'; - -import { - DefaultColumnFormatter, - providedPopoverContents, -} from './popover_utils'; - -describe('popover utils', () => { - const cellContentsElement = document.createElement('div'); - cellContentsElement.innerText = '{ "hello": "world" }'; - - test('DefaultColumnFormatter', () => { - const component = shallow( - - Test - - ); - - expect(component).toMatchInlineSnapshot(` - - Test - - `); - }); - - test('providedPopoverContents.json', () => { - const Component = providedPopoverContents.json; - const component = shallow( - Test - ); - - expect(component).toMatchInlineSnapshot(` - - { - "hello": "world" - } - - `); - }); -}); diff --git a/src/components/datagrid/body/popover_utils.tsx b/src/components/datagrid/body/popover_utils.tsx deleted file mode 100644 index 87a97be26a1..00000000000 --- a/src/components/datagrid/body/popover_utils.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 from 'react'; -import { EuiText } from '../../text'; -import { EuiCodeBlock } from '../../code'; -import { - EuiDataGridPopoverContents, - EuiDataGridPopoverContent, -} from '../data_grid_types'; - -export const DefaultColumnFormatter: EuiDataGridPopoverContent = ({ - children, -}) => { - return {children}; -}; - -export const providedPopoverContents: EuiDataGridPopoverContents = { - json: ({ cellContentsElement }) => { - let formattedText = cellContentsElement.innerText; - - // attempt to pretty-print the json - try { - formattedText = JSON.stringify(JSON.parse(formattedText), null, 2); - } catch (e) {} // eslint-disable-line no-empty - - return ( - - {formattedText} - - ); - }, -}; diff --git a/src/components/datagrid/data_grid.spec.tsx b/src/components/datagrid/data_grid.spec.tsx index 0e1814615d2..7163ca3cb59 100644 --- a/src/components/datagrid/data_grid.spec.tsx +++ b/src/components/datagrid/data_grid.spec.tsx @@ -111,14 +111,14 @@ describe('EuiDataGrid', () => { .first(); // make sure the horizontal scrollbar is present - virtualizedContainer.then(([outerContainer]: [HTMLDivElement]) => { + virtualizedContainer.then(([outerContainer]: JQuery) => { expect(outerContainer.offsetHeight).to.be.greaterThan( outerContainer.clientHeight ); }); // make sure the vertical scrollbar is gone - virtualizedContainer.then(([outerContainer]: [HTMLDivElement]) => { + virtualizedContainer.then(([outerContainer]: JQuery) => { expect(outerContainer.offsetWidth).to.equal(outerContainer.clientWidth); }); }); @@ -261,25 +261,33 @@ describe('EuiDataGrid', () => { cy.focused().should('not.exist'); // tab through the control bar - cy.get('body') - .tab() - .should('have.attr', 'data-test-subj', 'dataGridColumnSelectorButton') - .tab() - .should( - 'have.attr', - 'data-test-subj', - 'dataGridDisplaySelectorButton' - ) - .tab() - .should('have.attr', 'data-test-subj', 'dataGridFullScreenButton'); + cy.realPress('Tab'); + cy.focused().should( + 'have.attr', + 'data-test-subj', + 'dataGridColumnSelectorButton' + ); + cy.realPress('Tab'); + cy.focused().should( + 'have.attr', + 'data-test-subj', + 'dataGridDisplaySelectorButton' + ); + cy.realPress('Tab'); + cy.focused().should( + 'have.attr', + 'data-test-subj', + 'dataGridFullScreenButton' + ); // tab into the grid, should focus first cell after a short delay - cy.focused().tab(); + cy.realPress('Tab'); cy.focused() .should('have.attr', 'data-gridcell-column-index', '0') .should('have.attr', 'data-gridcell-row-index', '0'); - cy.focused().tab().should('have.id', 'final-tabbable'); + cy.realPress('Tab'); + cy.focused().should('have.id', 'final-tabbable'); }); it('arrow-keying focuses another cell, unless it has only one interactive element', () => { @@ -379,9 +387,9 @@ describe('EuiDataGrid', () => { // enable interactives & focus trap cy.focused().type('{enter}'); cy.focused().should('have.attr', 'data-test-subj', 'btn-yes'); - cy.focused().tab(); + cy.realPress('Tab'); cy.focused().should('have.attr', 'data-test-subj', 'btn-no'); - cy.focused().tab(); + cy.realPress('Tab'); cy.focused().should('have.attr', 'data-test-subj', 'btn-yes'); cy.focused().type('{esc}'); cy.focused() @@ -400,9 +408,9 @@ describe('EuiDataGrid', () => { cy.focused().parentsUntil( '[data-test-subj="euiDataGridExpansionPopover"]' ); // ensure focus is in the popover - cy.focused().tab(); + cy.realPress('Tab'); cy.focused().should('have.attr', 'data-test-subj', 'btn-no'); - cy.focused().tab(); + cy.realPress('Tab'); cy.focused().should( 'have.attr', 'data-test-subj', @@ -415,6 +423,185 @@ describe('EuiDataGrid', () => { }); }); }); + + describe('cell popovers', () => { + // Props + const cellActions = [ + ({ Component }) => ( + + Filter in + + ), + ({ Component }) => ( + + Filter out + + ), + ]; + const columns = [ + { id: 'default', cellActions }, + { id: 'json', schema: 'json', cellActions }, + ]; + const columnVisibility = { + visibleColumns: ['default', 'json'], + setVisibleColumns: () => {}, + }; + const baseCellPopoverProps = { ...baseProps, columns, columnVisibility }; + + // Utils + const openPopover = (columnId = 'default', rowIndex = 0) => { + cy.realPress('Escape'); // Close any open popovers + cy.get( + `[data-gridcell-column-id="${columnId}"][data-gridcell-row-index="${rowIndex}"]` + ).click(); + cy.focused().type('{enter}'); // Open cell popover + }; + + // Tests + it('renders a default popover with default cell values and a footer with cell actions', () => { + cy.mount(); + openPopover(); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('.euiText').text()).equals('default, 0'); + expect(el.find('.euiPopoverFooter').length).equals(1); + expect(el.find('.euiButtonEmpty').length).equals(2); + }); + }); + + describe('renderCellValue isDetails', () => { + it('renders a default popover with custom content in an EuiText wrapper and default cell actions', () => { + cy.mount( + { + if (isDetails && schema !== 'json') { + return 'custom popover content'; + } + return 'no -popover content'; + }} + /> + ); + openPopover(); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('.euiText').text()).equals('custom popover content'); + expect(el.find('.euiPopoverFooter').length).equals(1); + }); + }); + }); + + describe('renderCellPopover', () => { + it('renders a custom popover with completely custom content', () => { + cy.mount( + ( + <> +
Hello world!
+ + + )} + /> + ); + openPopover(); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('.euiText').length).equals(0); + expect(el.find('.euiPopoverFooter').length).equals(0); + + expect(el.find('[data-test-subj="customPopover"]').text()).equals( + 'Hello world!' + ); + expect(el.find('[data-test-subj="customAction"]').text()).equals( + 'Test' + ); + }); + }); + + it('renders a custom popover with default cell content and no cell actions', () => { + cy.mount( + ( +
{children}
+ )} + /> + ); + openPopover(); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('.euiText').length).equals(0); + expect(el.find('.euiPopoverFooter').length).equals(0); + + expect(el.find('[data-test-subj="customPopover"]').text()).equals( + 'default, 0' + ); + }); + }); + + it('renders a custom popover with custom cell content and default cell actions', () => { + cy.mount( + ( + <> +
Test
+ {cellActions} + + )} + /> + ); + openPopover(); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('.euiText').length).equals(0); + expect(el.find('.euiPopoverFooter').length).equals(1); + expect(el.find('.euiButtonEmpty').length).equals(2); + + expect(el.find('[data-test-subj="customPopover"]').text()).equals( + 'Test' + ); + }); + }); + + it('conditionally renders default cell popovers and custom cell popovers', () => { + cy.mount( + { + const { schema, DefaultCellPopover, cellContentsElement } = props; + + if (schema === 'json') { + return ; + } + if (cellContentsElement.innerText === 'default, 2') { + return ( +
+ Extremely custom popover +
+ ); + } + return
Custom popover
; + }} + /> + ); + openPopover('default'); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('[data-test-subj="customPopover"]').text()).equals( + 'Custom popover' + ); + }); + openPopover('default', 2); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('[data-test-subj="oneOffPopover"]').text()).equals( + 'Extremely custom popover' + ); + }); + openPopover('json'); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]').then((el) => { + expect(el.find('[data-test-subj="customPopover"]').length).equals(0); + expect(el.find('.euiCodeBlock').length).equals(1); + expect(el.find('.euiPopoverFooter').length).equals(1); + }); + }); + }); + }); }); function getGridData() { diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 88feaf92d46..36ad9f3181d 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -48,7 +48,6 @@ import { DataGridCellPopoverContext, useCellPopover, } from './body/data_grid_cell_popover'; -import { providedPopoverContents } from './body/popover_utils'; import { computeVisibleRows } from './utils/row_count'; import { EuiDataGridPaginationRenderer } from './utils/data_grid_pagination'; import { @@ -117,6 +116,7 @@ export const EuiDataGrid = forwardRef( schemaDetectors, rowCount, renderCellValue, + renderCellPopover, renderFooterCellValue, className, gridStyle, @@ -124,7 +124,6 @@ export const EuiDataGrid = forwardRef( pagination, sorting, inMemory, - popoverContents, onColumnResize, minSizeForControls, height, @@ -142,14 +141,6 @@ export const EuiDataGrid = forwardRef( [gridStyle] ); - const mergedPopoverContents = useMemo( - () => ({ - ...providedPopoverContents, - ...popoverContents, - }), - [popoverContents] - ); - const [inMemoryValues, onCellRender] = useInMemoryValues( inMemory, rowCount @@ -464,9 +455,9 @@ export const EuiDataGrid = forwardRef( headerIsInteractive={headerIsInteractive} handleHeaderMutation={handleHeaderMutation} schemaDetectors={allSchemaDetectors} - popoverContents={mergedPopoverContents} pagination={pagination} renderCellValue={renderCellValue} + renderCellPopover={renderCellPopover} renderFooterCellValue={renderFooterCellValue} rowCount={rowCount} visibleRows={visibleRows} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 2f5084f2fb8..53f380f0e84 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -161,10 +161,10 @@ export type EuiDataGridFooterRowProps = CommonProps & trailingControlColumns: EuiDataGridControlColumn[]; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; - popoverContents: EuiDataGridPopoverContents; columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; renderCellValue: EuiDataGridCellProps['renderCellValue']; + renderCellPopover?: EuiDataGridCellProps['renderCellPopover']; interactiveCellId: EuiDataGridCellProps['interactiveCellId']; visibleRowIndex?: number; }; @@ -227,23 +227,32 @@ export type CommonGridProps = CommonProps & * An array of custom #EuiDataGridSchemaDetector objects. You can inject custom schemas to the grid to define the classnames applied */ schemaDetectors?: EuiDataGridSchemaDetector[]; - /** - * An object mapping #EuiDataGridColumn `schema`s to a custom popover formatting component which receives #EuiDataGridPopoverContent props - */ - popoverContents?: EuiDataGridPopoverContents; /** * The total number of rows in the dataset (used by e.g. pagination to know how many pages to list) */ rowCount: number; /** * A function called to render a cell's value. Behind the scenes it is treated as a React component - * allowing hooks, context, and other React concepts to be used. The function receives a #CellValueElement + * allowing hooks, context, and other React concepts to be used. The function receives #EuiDataGridCellValueElementProps * as its only argument. */ renderCellValue: EuiDataGridCellProps['renderCellValue']; /** - * A function called to render a cell's value. Behind the scenes it is treated as a React component - * allowing hooks, context, and other React concepts to be used. The function receives a #CellValueElement + * An optional function that can be used to completely customize the rendering of cell popovers. + * + * If not specified, defaults to an `` wrapper around the rendered cell value and an + * `` around the cell actions. + * + * Behind the scenes it is treated as a React component allowing hooks, context, and other React concepts to be used. + * The function receives #EuiDataGridCellPopoverElementProps as its only argument. + * + */ + renderCellPopover?: EuiDataGridCellProps['renderCellPopover']; + /** + * An optional function called to render a footer cell. If not specified, no footer row is rendered. + * + * Behind the scenes it is treated as a React component + * allowing hooks, context, and other React concepts to be used. The function receives #EuiDataGridCellValueElementProps * as its only argument. */ renderFooterCellValue?: EuiDataGridCellProps['renderCellValue']; @@ -345,6 +354,7 @@ export interface EuiDataGridColumnSortingDraggableProps { */ display: string; } + export interface EuiDataGridBodyProps { leadingControlColumns: EuiDataGridControlColumn[]; trailingControlColumns: EuiDataGridControlColumn[]; @@ -352,10 +362,10 @@ export interface EuiDataGridBodyProps { visibleColCount: number; schema: EuiDataGridSchema; schemaDetectors: EuiDataGridSchemaDetector[]; - popoverContents: EuiDataGridPopoverContents; rowCount: number; visibleRows: EuiDataGridVisibleRows; renderCellValue: EuiDataGridCellProps['renderCellValue']; + renderCellPopover?: EuiDataGridCellProps['renderCellPopover']; renderFooterCellValue?: EuiDataGridCellProps['renderCellValue']; interactiveCellId: EuiDataGridCellProps['interactiveCellId']; pagination?: EuiDataGridPaginationProps; @@ -373,42 +383,78 @@ export interface EuiDataGridBodyProps { gridItemsRendered: MutableRefObject; wrapperRef: MutableRefObject; } -export interface EuiDataGridCellValueElementProps { + +/** + * Props shared between renderCellValue and renderCellPopover + */ +interface SharedRenderCellElementProps { /** - * index of the row being rendered, 0 represents the first row. This index always includes + * Index of the row being rendered, 0 represents the first row. This index always includes * pagination offset, meaning the first rowIndex in a grid is `pagination.pageIndex * pagination.pageSize` * so take care if you need to adjust the rowIndex to fit your data */ rowIndex: number; /** - * index of the column being rendered, 0 represents the first column. This index accounts + * Index of the column being rendered, 0 represents the first column. This index accounts * for columns that have been hidden or reordered by the user, so take care if you need * to adjust the colIndex to fit your data */ colIndex: number; /** - * id of the column being rendered, the value comes from the #EuiDataGridColumn `id` + * ID of the column being rendered, the value comes from the #EuiDataGridColumn `id` */ columnId: string; /** - * callback function to set custom props & attributes on the cell's wrapping `div` element; + * The schema type of the column being rendered + */ + schema: string | undefined | null; +} + +export interface EuiDataGridCellValueElementProps + extends SharedRenderCellElementProps { + /** + * Callback function to set custom props & attributes on the cell's wrapping `div` element; * it's best to wrap calls to `setCellProps` in a `useEffect` hook */ setCellProps: (props: CommonProps & HTMLAttributes) => void; /** - * whether or not the cell is expandable, comes from the #EuiDataGridColumn `isExpandable` which defaults to `true` + * Whether or not the cell is expandable, comes from the #EuiDataGridColumn `isExpandable` which defaults to `true` */ isExpandable: boolean; /** - * whether or not the cell is expanded + * Whether or not the cell is expanded */ isExpanded: boolean; /** - * when rendering the cell, `isDetails` is false; when the cell is expanded, `renderCellValue` is called again to render into the details popover and `isDetails` is true + * When rendering the cell, `isDetails` is false; when the cell is expanded, `renderCellValue` is called again to render into the details popover and `isDetails` is true */ isDetails: boolean; } +export interface EuiDataGridCellPopoverElementProps + extends SharedRenderCellElementProps { + /** + * The default `children` passed to the cell popover comes from the passed `renderCellValue` prop as a ReactElement. + * + * Allows wrapping the rendered content: `({ children }) =>
{children}
` - or leave it out to render completely custom content. + */ + children: ReactNode; + /** + * References the div element the cell contents have been rendered into. Primarily useful for processing the rendered text + */ + cellContentsElement: HTMLDivElement; + /** + * An `EuiPopoverFooter` containing all column `cellActions` (as `EuiEmptyButton`s). + * Use `{cellActions}` to render the default cell action buttons, or leave it out to hide cell actions/render your own. + */ + cellActions: ReactNode; + /** + * For certain columns or schemas, you may want to fall back to the standard EuiDataGrid popover display. + * If so, that component is provided here as a passed React function component for your usage. + */ + DefaultCellPopover: JSXElementConstructor; +} + export interface EuiDataGridCellProps { rowIndex: number; visibleRowIndex: number; @@ -420,11 +466,13 @@ export interface EuiDataGridCellProps { interactiveCellId: string; isExpandable: boolean; className?: string; - popoverContent: EuiDataGridPopoverContent; popoverContext: DataGridCellPopoverContextShape; renderCellValue: | JSXElementConstructor | ((props: EuiDataGridCellValueElementProps) => ReactNode); + renderCellPopover?: + | JSXElementConstructor + | ((props: EuiDataGridCellPopoverElementProps) => ReactNode); setRowHeight?: (height: number) => void; getRowHeight?: (rowIndex: number) => number; style?: React.CSSProperties; @@ -443,11 +491,7 @@ export interface EuiDataGridCellState { export type EuiDataGridCellValueProps = Omit< EuiDataGridCellProps, - | 'width' - | 'interactiveCellId' - | 'popoverContent' - | 'popoverContext' - | 'rowManager' + 'width' | 'interactiveCellId' | 'popoverContext' | 'rowManager' >; export interface EuiDataGridControlColumn { /** @@ -777,23 +821,6 @@ export interface EuiDataGridInMemoryValues { [rowIndex: string]: { [columnId: string]: string }; } -export interface EuiDataGridPopoverContentProps { - /** - * your `cellValueRenderer` as a ReactElement; allows wrapping the rendered content: `({children}) =>
{children}
` - */ - children: ReactNode; - /** - * div element the cell contents have been rendered into; useful for processing the rendered text - */ - cellContentsElement: HTMLDivElement; -} -export type EuiDataGridPopoverContent = ComponentType< - EuiDataGridPopoverContentProps ->; -export interface EuiDataGridPopoverContents { - [key: string]: EuiDataGridPopoverContent; -} - export interface EuiDataGridOnColumnResizeData { columnId: string; width: number; diff --git a/src/components/datagrid/utils/in_memory.tsx b/src/components/datagrid/utils/in_memory.tsx index 3e14c8fa4b1..fdf50cdc40a 100644 --- a/src/components/datagrid/utils/in_memory.tsx +++ b/src/components/datagrid/utils/in_memory.tsx @@ -125,6 +125,7 @@ export const EuiDataGridInMemoryRenderer: FunctionComponent