diff --git a/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js b/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js index c5e7bff6f9f..718d2106093 100644 --- a/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js +++ b/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js @@ -15,6 +15,9 @@ import { import DataGridColumnCellActions from './column_cell_actions'; const dataGridColumnCellActionsSource = require('!!raw-loader!./column_cell_actions'); +import VisibleCellActions from './visible_cell_actions'; +const visibleCellActionsSource = require('!!raw-loader!./visible_cell_actions'); + import { DataGridCellPopoverExample } from './datagrid_cell_popover_example'; import DataGridFocus from './focus'; @@ -63,20 +66,26 @@ export const DataGridCellsExample = { text: (

- On top of making a cell expandable, you can add more custom actions - by setting cellActions. This contains functions - called to render additional buttons in the cell and in the popover - when expanded. Behind the scenes those are treated as a React + In addition to making a cell expandable, you can add more custom + actions by setting columns.cellActions. These + actions will render as icon buttons in the cell on hover/focus, and + render as full buttons in the cell expansion popover. Note that once + any cellActions are passed, the cell becomes + automatically expandable - this ensures keyboard and screen reader + users have access to all cell actions. +

+

+ columns.cellActions accepts an array of render + functions. Behind the scenes, the functions are treated as a React components allowing hooks, context, and other React concepts to be - used. The functions receives an argument of type - EuiDataGridColumnCellActionProps. The icons of these - actions are displayed on mouse over, and also appear in the popover - when the cell is expanded. Note that once you've defined the{' '} - cellAction property, the cell's - automatically expandable. + used. Because different button types are used between the cell and + the cell popover, we pass your render function a{' '} + Component prop which you must render in order for + your cell actions to switch correctly between button icons and + buttons.

- Below, the email and city columns provide 1{' '} + In the below example, the email and city columns provide 1{' '} cellAction each, while the country column provides 2 cellActions.
@@ -95,6 +104,42 @@ export const DataGridCellsExample = { }, demo: , }, + { + title: 'Visible cell actions and cell popovers', + text: ( + <> +

+ By default, only the first 2 cell actions of a cell will be + displayed to the left of the expand action button, and remaining + actions will be displayed in the footer of the cell expansion + popover. +

+

+ This number is configurable by setting{' '} + columns.visibleCellActions, should you need to + display more cell actions immediately. However, we advise caution + when increasing this limit - the default is set to ensure cell + action buttons do not crowd out content. +

+

+ The below example shows an increasing number of{' '} + cellActions in each column. The third column + shows visibleCellActions set to 3, and the fourth + column shows excess actions overflowing into the popover. +

+ + ), + demo: , + source: [ + { + type: GuideSectionTypes.TSX, + code: visibleCellActionsSource, + }, + ], + props: { + EuiDataGridColumn, + }, + }, ...DataGridCellPopoverExample.sections, diff --git a/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx b/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx new file mode 100644 index 00000000000..82af460ac21 --- /dev/null +++ b/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx @@ -0,0 +1,97 @@ +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 cellActions1: EuiDataGridColumnCellAction[] = [ + ({ Component }) => ( + {}} iconType="timeline"> + Add to timeline + + ), +]; +const cellActions2: EuiDataGridColumnCellAction[] = [ + ({ Component }) => ( + + Filter in + + ), + ({ Component }) => ( + + Filter out + + ), +]; +const cellActions3 = [...cellActions2, ...cellActions1]; +const cellActions5: EuiDataGridColumnCellAction[] = [ + ...cellActions3, + ({ Component }) => ( + {}} iconType="starEmpty"> + Custom action 1 + + ), + ({ Component }) => ( + {}} iconType="starEmpty"> + Custom action 2 + + ), +]; + +const columns: EuiDataGridColumn[] = [ + { + id: 'default', + cellActions: cellActions1, + }, + { + id: 'datetime', + cellActions: cellActions2, + }, + { + id: 'json', + schema: 'json', + cellActions: cellActions3, + visibleCellActions: 3, + }, + { + id: 'custom', + schema: 'favoriteFranchise', + cellActions: cellActions5, + }, +]; + +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', + }); +} + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + return ( + data[rowIndex][columnId]} + /> + ); +}; 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 f44c7d44ea8..e8f2afac7be 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 @@ -1,5 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`EuiDataGridCell componentDidUpdate handles the cell popover by forwarding the cell's DOM node and contents to the parent popover context 1`] = ` +Array [ +
+
+ + +
+
, +
+
+
+
+
+
+
+
, +] +`; + 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 [ -
-
- - -
-
, -
-
-
-
-
-
, -] -`; diff --git a/src/components/datagrid/body/data_grid_cell_actions.test.tsx b/src/components/datagrid/body/data_grid_cell_actions.test.tsx index c1e77e8a3a4..ef8e7e47f9b 100644 --- a/src/components/datagrid/body/data_grid_cell_actions.test.tsx +++ b/src/components/datagrid/body/data_grid_cell_actions.test.tsx @@ -110,6 +110,37 @@ describe('EuiDataGridCellActions', () => { `); }); + + describe('visible cell actions limit', () => { + it('by default, does not render more than the first two primary cell actions', () => { + const component = shallow( + + ); + + expect(component.find('MockAction')).toHaveLength(2); + }); + + it('allows configuring the default number of visible cell actions', () => { + const component = shallow( + + ); + + expect(component.find('MockAction')).toHaveLength(3); + }); + }); }); describe('EuiDataGridCellPopoverActions', () => { @@ -123,25 +154,29 @@ describe('EuiDataGridCellPopoverActions', () => { ); expect(component).toMatchInlineSnapshot(` - - - + + - - - - + +
+ +
+
+ + +
`); const action = component.find('MockAction') as any; @@ -154,6 +189,100 @@ describe('EuiDataGridCellPopoverActions', () => { `); }); + it('renders primary actions in their own footer, and all remaining secondary actions in a column footer', () => { + const component = shallow( + + ); + + expect(component).toMatchInlineSnapshot(` + + + + +
+ +
+
+ +
+ +
+
+
+
+ + + +
+ +
+
+
+
+
+ `); + }); + + it('uses visibleCellActions to configure the number of primary vs. secondary actions', () => { + const component = shallow( + + ); + + expect( + component.find('EuiPopoverFooter').first().find('MockAction') + ).toHaveLength(3); + expect( + component.find('EuiPopoverFooter').last().find('MockAction') + ).toHaveLength(1); + }); + it('does not render anything if the column has no cell actions', () => { const component = shallow( { /> ); - expect(component.isEmptyRender()).toBe(true); + expect(component).toMatchInlineSnapshot(''); }); }); diff --git a/src/components/datagrid/body/data_grid_cell_actions.tsx b/src/components/datagrid/body/data_grid_cell_actions.tsx index cf2649601c4..c4b8770ebaa 100644 --- a/src/components/datagrid/body/data_grid_cell_actions.tsx +++ b/src/components/datagrid/body/data_grid_cell_actions.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { JSXElementConstructor, useMemo } from 'react'; +import React, { JSXElementConstructor, useMemo, useCallback } from 'react'; import { EuiDataGridColumn, EuiDataGridColumnCellAction, @@ -59,6 +59,8 @@ export const EuiDataGridCellActions = ({ ); const additionalButtons = useMemo(() => { + if (!column || !Array.isArray(column?.cellActions)) return []; + const ButtonComponent = (props: EuiButtonIconProps) => ( ); - return column && Array.isArray(column.cellActions) - ? column.cellActions.map( - (Action: EuiDataGridColumnCellAction, idx: number) => { - // React is more permissible than the TS types indicate - const ActionButtonElement = Action as JSXElementConstructor< - EuiDataGridColumnCellActionProps - >; - return ( - - ); - } - ) - : []; + + const [visibleCellActions] = getVisibleCellActions( + column?.cellActions, + column?.visibleCellActions + ); + return visibleCellActions.map( + (Action: EuiDataGridColumnCellAction, idx: number) => { + // React is more permissible than the TS types indicate + const ActionButtonElement = Action as JSXElementConstructor< + EuiDataGridColumnCellActionProps + >; + return ( + + ); + } + ); }, [column, colIndex, rowIndex, closePopover]); return ( @@ -106,32 +111,70 @@ export const EuiDataGridCellPopoverActions = ({ colIndex: number; rowIndex: number; }) => { - if (!column?.cellActions?.length) return null; + const [primaryActions, secondaryActions] = getVisibleCellActions( + column?.cellActions, + column?.visibleCellActions + ); + + const renderActions = useCallback( + (Action: EuiDataGridColumnCellAction, idx: number) => { + const ActionButtonElement = Action as JSXElementConstructor< + EuiDataGridColumnCellActionProps + >; + return ( + +
+ ( + + )} + isExpanded={true} + /> +
+
+ ); + }, + [column, colIndex, rowIndex] + ); return ( - - - {column.cellActions.map( - (Action: EuiDataGridColumnCellAction, idx: number) => { - const ActionButtonElement = Action as JSXElementConstructor< - EuiDataGridColumnCellActionProps - >; - return ( - - ( - - )} - isExpanded={true} - /> - - ); - } - )} - - + <> + {primaryActions.length > 0 && ( + + + {primaryActions.map(renderActions)} + + + )} + {secondaryActions.length > 0 && ( + + + {secondaryActions.map(renderActions)} + + + )} + ); }; + +// Util helper to separate primary actions (columns.visibleCellActions, defaults to 2) +// and secondary actions (all remaning actions) +const getVisibleCellActions = ( + cellActions?: EuiDataGridColumnCellAction[], + visibleCellActions = 2 +): [EuiDataGridColumnCellAction[], EuiDataGridColumnCellAction[]] => { + if (!cellActions) return [[], []]; + if (cellActions.length <= visibleCellActions) return [cellActions, []]; + + const primaryCellActions = cellActions.slice(0, visibleCellActions); + const remainingCellActions = cellActions.slice(visibleCellActions); + + return [primaryCellActions, remainingCellActions]; +}; diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 7b5bd70e7e4..3c0a2e7413d 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -576,6 +576,12 @@ export interface EuiDataGridColumn { * Additional actions displayed as icon on hover / focus, and in the expanded view of the cell containing the value */ cellActions?: EuiDataGridColumnCellAction[]; + /** + * Configures the amount of cell action buttons immediately visible on a cell. + * Any cell actions above this number will only display in the cell expansion popover. + * Defaults to 2. + */ + visibleCellActions?: number; } export type EuiDataGridColumnCellAction = diff --git a/upcoming_changelogs/5675.md b/upcoming_changelogs/5675.md new file mode 100644 index 00000000000..d3013fac2d9 --- /dev/null +++ b/upcoming_changelogs/5675.md @@ -0,0 +1 @@ +- `EuiDataGrid` now allows limiting the number of visible cell actions with a new `columns.visibleCellActions` prop (defaults to 2). All additional actions will be shown in the cell expansion popover.