diff --git a/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx b/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx index 54436489535..11a9eea4968 100644 --- a/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx +++ b/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx @@ -1,4 +1,4 @@ -import React, { useState, ReactNode } from 'react'; +import React, { useState, useEffect, ReactNode } from 'react'; import { faker } from '@faker-js/faker'; import { @@ -73,12 +73,18 @@ const RenderCellPopover = (props: EuiDataGridCellPopoverElementProps) => { cellActions, cellContentsElement, DefaultCellPopover, + setCellPopoverProps, } = props; let title: ReactNode = 'Custom popover'; let content: ReactNode = {children}; let footer: ReactNode = cellActions; + // Set custom cell expansion popover props + useEffect(() => { + setCellPopoverProps({ panelClassName: 'customCellPopover' }); + }, [setCellPopoverProps]); + // An example of custom popover content if (schema === 'favoriteFranchise') { title = 'Custom popover with custom content'; diff --git a/src-docs/src/views/datagrid/cells_popovers/datagrid_cell_popover_example.js b/src-docs/src/views/datagrid/cells_popovers/datagrid_cell_popover_example.js index b25ee99741d..e8f319b0bd3 100644 --- a/src-docs/src/views/datagrid/cells_popovers/datagrid_cell_popover_example.js +++ b/src-docs/src/views/datagrid/cells_popovers/datagrid_cell_popover_example.js @@ -112,6 +112,15 @@ export const DataGridCellPopoverExample = { rendering for other cells.

+
  • +

    + setCellPopoverProps - this callback is passed + to allow customizing the cell expansion popover. Accepts any + prop that EuiPopover accepts, except for{' '} + button & closePopover, + which is controlled by the data grid. +

    +
  • Take a look at the below example's demo code to see the above diff --git a/src/components/datagrid/body/data_grid_cell.test.tsx b/src/components/datagrid/body/data_grid_cell.test.tsx index de149228cd6..4e2da6b78bd 100644 --- a/src/components/datagrid/body/data_grid_cell.test.tsx +++ b/src/components/datagrid/body/data_grid_cell.test.tsx @@ -30,6 +30,7 @@ describe('EuiDataGridCell', () => { openCellPopover: jest.fn(), setPopoverAnchor: jest.fn(), setPopoverContent: jest.fn(), + setCellPopoverProps: () => {}, }; const requiredProps = { rowIndex: 0, diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index 866a77e8cbd..e90328415b2 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -478,7 +478,11 @@ export class EuiDataGridCell extends Component< handleCellPopover = () => { if (this.isPopoverOpen()) { - const { setPopoverAnchor, setPopoverContent } = this.props.popoverContext; + const { + setPopoverAnchor, + setPopoverContent, + setCellPopoverProps, + } = this.props.popoverContext; // Set popover anchor const cellAnchorEl = this.popoverAnchorRef.current!; @@ -515,6 +519,7 @@ export class EuiDataGridCell extends Component< } DefaultCellPopover={DefaultCellPopover} + setCellPopoverProps={setCellPopoverProps} > -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiDataGrid, EuiDataGridProps } from '../'; const baseProps: EuiDataGridProps = { @@ -100,4 +100,31 @@ describe('EuiDataGridCellPopover', () => { 'not.exist' ); }); + + it('allows consumers to use setCellPopoverProps, passed from renderCellPopover, to customize popover props', () => { + const RenderCellPopover = ({ + DefaultCellPopover, + setCellPopoverProps, + ...props + }) => { + useEffect(() => { + setCellPopoverProps({ + panelClassName: 'hello', + panelProps: { className: 'world' }, + }); + }, [setCellPopoverProps]); + + return ; + }; + + cy.realMount( + + ); + cy.get( + '[data-gridcell-row-index="0"][data-gridcell-column-index="0"]' + ).realClick(); + cy.get('[data-test-subj="euiDataGridCellExpandButton"]').realClick(); + + cy.get('.euiDataGridRowCell__popover.hello.world').should('exist'); + }); }); 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 da00dc334cd..def6ced9387 100644 --- a/src/components/datagrid/body/data_grid_cell_popover.test.tsx +++ b/src/components/datagrid/body/data_grid_cell_popover.test.tsx @@ -7,10 +7,10 @@ */ import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { renderHook, act } from '@testing-library/react-hooks'; import { shallow } from 'enzyme'; + import { keys } from '../../../services'; -import { testCustomHook } from '../../../test/internal'; import { DataGridCellPopoverContextShape } from '../data_grid_types'; import { useCellPopover, DefaultCellPopover } from './data_grid_cell_popover'; @@ -18,55 +18,57 @@ import { useCellPopover, DefaultCellPopover } from './data_grid_cell_popover'; describe('useCellPopover', () => { describe('openCellPopover', () => { it('sets popoverIsOpen state to true', () => { - const { - return: { cellPopoverContext }, - getUpdatedState, - } = testCustomHook(() => useCellPopover()); - expect(cellPopoverContext.popoverIsOpen).toEqual(false); + const { result } = renderHook(useCellPopover); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(false); act(() => - cellPopoverContext.openCellPopover({ rowIndex: 0, colIndex: 0 }) + result.current.cellPopoverContext.openCellPopover({ + rowIndex: 0, + colIndex: 0, + }) ); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual(true); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(true); }); it('does nothing if called again on a popover that is already open', () => { - const { - return: { cellPopoverContext, cellPopover }, - getUpdatedState, - } = testCustomHook(() => useCellPopover()); - expect(cellPopover).toBeFalsy(); + const { result } = renderHook(useCellPopover); + expect(result.current.cellPopover).toBeFalsy(); act(() => { - cellPopoverContext.openCellPopover({ rowIndex: 0, colIndex: 0 }); - cellPopoverContext.setPopoverAnchor(document.createElement('div')); + result.current.cellPopoverContext.openCellPopover({ + rowIndex: 0, + colIndex: 0, + }); + result.current.cellPopoverContext.setPopoverAnchor( + document.createElement('div') + ); }); - expect(getUpdatedState().cellPopover).not.toBeFalsy(); + expect(result.current.cellPopover).not.toBeFalsy(); act(() => { - getUpdatedState().cellPopoverContext.openCellPopover({ + result.current.cellPopoverContext.openCellPopover({ rowIndex: 0, colIndex: 0, }); }); - expect(getUpdatedState().cellPopover).not.toBeFalsy(); + expect(result.current.cellPopover).not.toBeFalsy(); }); }); describe('closeCellPopover', () => { it('sets popoverIsOpen state to false', () => { - const { - return: { cellPopoverContext }, - getUpdatedState, - } = testCustomHook(() => useCellPopover()); + const { result } = renderHook(useCellPopover); act(() => - cellPopoverContext.openCellPopover({ rowIndex: 0, colIndex: 0 }) + result.current.cellPopoverContext.openCellPopover({ + rowIndex: 0, + colIndex: 0, + }) ); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual(true); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(true); - act(() => cellPopoverContext.closeCellPopover()); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual(false); + act(() => result.current.cellPopoverContext.closeCellPopover()); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(false); }); }); @@ -86,14 +88,11 @@ describe('useCellPopover', () => { }; it('renders', () => { - const { - return: { cellPopoverContext, cellPopover }, - getUpdatedState, - } = testCustomHook(() => useCellPopover()); - expect(cellPopover).toBeFalsy(); // Should be empty on init + const { result } = renderHook(useCellPopover); + expect(result.current.cellPopover).toBeFalsy(); // Should be empty on init - populateCellPopover(cellPopoverContext); - expect(getUpdatedState().cellPopover).toMatchInlineSnapshot(` + populateCellPopover(result.current.cellPopoverContext); + expect(result.current.cellPopover).toMatchInlineSnapshot(` } closePopover={[Function]} @@ -134,25 +133,16 @@ describe('useCellPopover', () => { mockCell.appendChild(mockPopoverAnchor); const renderCellPopover = () => { - const { - return: { cellPopoverContext }, - getUpdatedState, - } = testCustomHook>(() => - useCellPopover() - ); - populateCellPopover(cellPopoverContext); - - const { cellPopover } = getUpdatedState(); - const component = shallow(

    {cellPopover}
    ); + const { result } = renderHook(useCellPopover); + populateCellPopover(result.current.cellPopoverContext); + const component = shallow(
    {result.current.cellPopover}
    ); - return { component, getUpdatedState }; + return { result, component }; }; it('closes the popover and refocuses the cell when the Escape key is pressed', () => { - const { component, getUpdatedState } = renderCellPopover(); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual( - true - ); + const { result, component } = renderCellPopover(); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(true); const event = { key: keys.ESCAPE, @@ -165,17 +155,13 @@ describe('useCellPopover', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(event.stopPropagation).toHaveBeenCalled(); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual( - false - ); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(false); expect(document.activeElement).toEqual(mockCell); }); it('closes the popover when the F2 key is pressed', () => { - const { component, getUpdatedState } = renderCellPopover(); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual( - true - ); + const { result, component } = renderCellPopover(); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(true); const event = { key: keys.F2, @@ -188,17 +174,13 @@ describe('useCellPopover', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(event.stopPropagation).toHaveBeenCalled(); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual( - false - ); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(false); expect(document.activeElement).toEqual(mockCell); }); it('does nothing when other keys are pressed', () => { - const { component, getUpdatedState } = renderCellPopover(); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual( - true - ); + const { result, component } = renderCellPopover(); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(true); const event = { key: keys.ENTER, @@ -212,12 +194,12 @@ describe('useCellPopover', () => { expect(event.stopPropagation).not.toHaveBeenCalled(); expect(rafSpy).not.toHaveBeenCalled(); - expect(getUpdatedState().cellPopoverContext.popoverIsOpen).toEqual( - true - ); + expect(result.current.cellPopoverContext.popoverIsOpen).toEqual(true); }); }); }); + + // setCellPopoverProps is tested in the Cypress .spec file }); describe('popover content renderers', () => { @@ -232,6 +214,7 @@ describe('popover content renderers', () => { cellActions:
    Action
    , cellContentsElement, DefaultCellPopover, + setCellPopoverProps: () => {}, }; test('default cell popover', () => { diff --git a/src/components/datagrid/body/data_grid_cell_popover.tsx b/src/components/datagrid/body/data_grid_cell_popover.tsx index 32a38364aae..0a51b131af2 100644 --- a/src/components/datagrid/body/data_grid_cell_popover.tsx +++ b/src/components/datagrid/body/data_grid_cell_popover.tsx @@ -7,9 +7,10 @@ */ import React, { createContext, useState, useCallback, ReactNode } from 'react'; +import classNames from 'classnames'; import { keys } from '../../../services'; -import { EuiWrappingPopover } from '../../popover'; +import { EuiWrappingPopover, EuiPopoverProps } from '../../popover'; import { DataGridCellPopoverContextShape, EuiDataGridCellPopoverElementProps, @@ -24,6 +25,7 @@ export const DataGridCellPopoverContext = createContext< closeCellPopover: () => {}, setPopoverAnchor: () => {}, setPopoverContent: () => {}, + setCellPopoverProps: () => {}, }); export const useCellPopover = (): { @@ -39,6 +41,10 @@ export const useCellPopover = (): { // Popover anchor & content are passed by individual `EuiDataGridCell`s const [popoverAnchor, setPopoverAnchor] = useState(null); const [popoverContent, setPopoverContent] = useState(); + // Allow customization of most (not all) popover props by consumers + const [cellPopoverProps, setCellPopoverProps] = useState< + Partial + >({}); const closeCellPopover = useCallback(() => setPopoverIsOpen(false), []); const openCellPopover = useCallback( @@ -68,21 +74,26 @@ export const useCellPopover = (): { cellLocation, setPopoverAnchor, setPopoverContent, + setCellPopoverProps, }; // Note that this popover is rendered once at the top grid level, rather than one popover per cell const cellPopover = popoverIsOpen && popoverAnchor && ( { if (event.key === keys.F2 || event.key === keys.ESCAPE) { event.preventDefault(); @@ -92,6 +103,8 @@ export const useCellPopover = (): { requestAnimationFrame(() => popoverAnchor.parentElement!.focus()); } }} + button={popoverAnchor} + closePopover={closeCellPopover} > {popoverContent} diff --git a/src/components/datagrid/body/footer/data_grid_footer_row.test.tsx b/src/components/datagrid/body/footer/data_grid_footer_row.test.tsx index 9373b798ab2..8017f317ca9 100644 --- a/src/components/datagrid/body/footer/data_grid_footer_row.test.tsx +++ b/src/components/datagrid/body/footer/data_grid_footer_row.test.tsx @@ -49,6 +49,7 @@ describe('EuiDataGridFooterRow', () => { "closeCellPopover": [Function], "openCellPopover": [Function], "popoverIsOpen": false, + "setCellPopoverProps": [Function], "setPopoverAnchor": [Function], "setPopoverContent": [Function], } @@ -75,6 +76,7 @@ describe('EuiDataGridFooterRow', () => { "closeCellPopover": [Function], "openCellPopover": [Function], "popoverIsOpen": false, + "setCellPopoverProps": [Function], "setPopoverAnchor": [Function], "setPopoverContent": [Function], } @@ -127,6 +129,7 @@ describe('EuiDataGridFooterRow', () => { "closeCellPopover": [Function], "openCellPopover": [Function], "popoverIsOpen": false, + "setCellPopoverProps": [Function], "setPopoverAnchor": [Function], "setPopoverContent": [Function], } @@ -178,6 +181,7 @@ describe('EuiDataGridFooterRow', () => { "closeCellPopover": [Function], "openCellPopover": [Function], "popoverIsOpen": false, + "setCellPopoverProps": [Function], "setPopoverAnchor": [Function], "setPopoverContent": [Function], } diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index c03d3a4bb10..0ae7fb714f7 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -28,6 +28,7 @@ import { ExclusiveUnion, CommonProps, OneOf } from '../common'; import { RowHeightUtils } from './utils/row_heights'; import { IconType } from '../icon'; import { EuiTokenProps } from '../token'; +import { EuiPopoverProps } from '../popover'; // since react-window doesn't export a type with the imperative api only we can // use this to omit the react-specific class component methods @@ -206,6 +207,7 @@ export interface DataGridCellPopoverContextShape { closeCellPopover(): void; setPopoverAnchor(anchor: HTMLElement): void; setPopoverContent(content: ReactNode): void; + setCellPopoverProps: EuiDataGridCellPopoverElementProps['setCellPopoverProps']; } export type CommonGridProps = CommonProps & @@ -501,6 +503,13 @@ export interface EuiDataGridCellPopoverElementProps * If so, that component is provided here as a passed React function component for your usage. */ DefaultCellPopover: JSXElementConstructor; + /** + * Allows passing props to the wrapping cell expansion popover and panel. + * Accepts any props that `EuiPopover` accepts, except for `button` and `closePopover`. + */ + setCellPopoverProps: ( + props: Omit + ) => void; } export interface EuiDataGridCellProps { diff --git a/upcoming_changelogs/6632.md b/upcoming_changelogs/6632.md new file mode 100644 index 00000000000..38281b6abeb --- /dev/null +++ b/upcoming_changelogs/6632.md @@ -0,0 +1 @@ +- Added new `setCellPopoverProps` parameter callback to `EuiDataGrid`'s `renderCellPopover` prop