Skip to content

Commit

Permalink
[EuiDataGrid] Allow consuming applications to control various interna…
Browse files Browse the repository at this point in the history
…l state via `ref` (#5590)

* [EuiDataGrid] Set up `ref` that exposes focus/popover internal APIs (#5499)

* Set up types

* Set up forwardRef

* Add setFocusedCell API to returned grid ref obj

* Add colIndex prop to cell actions

- so that cell actions that trigger modals or flyouts can re-focus into the correct cell using the new ref API

* Add documentation + example + props

* Add changelog

* [PR feedback] Types

Co-authored-by: Chandler Prall <chandler.prall@gmail.com>

* [PR feedback] Clean up unit test

* [Rebase] Tweak useImperativeHandle location

- Moving it below fullscreen logic, as we're oging to expose setIsFullScreen as an API shortly

Co-authored-by: Chandler Prall <chandler.prall@gmail.com>

* [EuiDataGrid] Add `setIsFullScreen` to ref API (#5531)

* Expose `setIsFullScreen` to ref API

* Update documentation/examples

* [EuiDataGrid] Add `openCellPopover` and `closeCellPopover` to ref APIs (#5550)

* [setup] Update testCustomHook to expose fn that allows accessing most recent state/value

- without this callback, the initial returned hook values will be stale/not properly return most recent values
- see next commit for example usage within useCellPopover

* Set up cell popover context

- set up initial open/location state, + open/close popover APIs returned to consumers

- improve auto props documentation - remove EuiDataGridCellLocation in favor of specifying rowIndex and colIndex (it's less DRY but it's easier for devs to not have to look up EuiDataGridCellLocation from our docs)

* Pass down popoverContext to cells as a prop

- I'm not using context here because we're already using this.context for focus, and unfortunately class components can only initialize one context at time using `static contextType` (see https://reactjs.org/docs/context.html#classcontexttype)

* Remove internal cell popoverIsOpen state

- This should now be handled by the overarching context state, and the cell should simply react to it or update it (similar to how focusContext works)

+ add new var for hasCellButtons

+ add unit tests for isFocusedCell alongside isPopoverOpen (since both methods perform similar functions)

* Update cell popovers to set the popover anchor & content

- content is TODO, will likely be easier to compare when cleaning it up/moving it all at once

* Refactor EuiDataGridCellPopover

- It should no longer exist as a reusable component that belongs to every single cell, but instead as a single popover that exists at the top grid level and moves from cell to cell

- I cleaned and split up the JSX for the popover (e.g. moving popover actions to data_grid_cell_buttons, where it feels like it belongs more) and think it's significantly more DRY now - note the entire `anchorContent` branch removed from EuiDataGridCell that is no longer necessary

- Note that due to this change, we now have to mock EuiWrappingPopover in EuiDataGrid tests, as we see failures otherwise

* [bugfix] Handle cells with open popover being scrolled out of view

- this is the same behavior as in prod

- causes weird DOM issues if we don't close the cell popover automatically

* [bugfix] Workaround for popover DOM stuttering issues

* [enhancement] Account for openCellPopover being called on cells out of view

+ write bonus Cypress test for useScroll's focus effect now that we have access to the imperative ref

* Update documentation example

+ remove code snippet - it was starting to get redundant with the API bullet points, and is already available as tab if needed

+ fix control button widths

* [PR feedback] Be more specific about useImperativeHandle dependencies

+ add a few explanatory comments

* [PR feedback] Rename openCellLocation to cellLocation

- to make it sound less like a verb/method

* [PR feedback] Ignore edge case of `openCellPopover` being called on an `isExpandable: false` cell

* [EuiDataGrid] Handle exposed ref APIs potentially pointing to invalid, off-page, or out of view cells (#5572)

* Enable sorting + targeting row indices outside of the current page

- to test handling the exposed APIs when dealing with sorted/paginated data

* Switch data grid example to Typescript

- to test type issues during consumer usage

+ @ts-ignore faker complaints

* Fix cell expansion buttons on paginated pages not working correctly

* Attempt to more clearly document `rowIndex`s that are actually `visibleRowIndex`s

* [setup] Move imperative handler setup to its own util file

- this will let us set up ref-specific helpers & add more comment context without bloating the main file

* Add catch/check for cell locations that do not exist in the current grid

* Add getVisibleRowIndex helper

- Converts the `rowIndex` from the consumer to a `visibleRowIndex` that our internal state can use

- Account for sorted grid by finding the inversed index of the `sortedRowMap`
- To make this easier, I converted soredRowMap to an array (since it's already only uses numbers for both keys and values), since arrays have a handy .findIndex method

- Handles automatically paginating the grid if the targeted cell is on a different page

* Replace grid ref Jest tests with more complete Cypress tests

* Update documentation with new behavior

* [PR feedback] Rename fns to indicate multiple concerns

- having a side effect in a getter feels bad, so change that to a `find`
- rename use hook to indicate sorting and pagination concerns

* Improve changelog

* Data grid ref methods docs review

* Fix colIndex to be available in renderCellValue as well as cell actions

- primarily for use within trailing/leading cells and custom actions

- see 1609e45

* Fix main datagrid example to restore cell focus on modal/flyout close

* [a11y] Fix missing header cell text in main datagrid example

Co-authored-by: Chandler Prall <chandler.prall@gmail.com>
Co-authored-by: Elizabet Oliveira <elizabet.oliveira@elastic.co>
  • Loading branch information
3 people authored Feb 2, 2022
1 parent 331e6f6 commit d794a70
Show file tree
Hide file tree
Showing 30 changed files with 2,249 additions and 735 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## [`main`](https://github.com/elastic/eui/tree/main)

- Added the ability to control internal `EuiDataGrid` fullscreen, cell focus, and cell popover state via the `ref` prop ([#5590](https://github.com/elastic/eui/pull/5590))

**Bug fixes**

- Fixed `EuiInMemoryTable`'s `onTableChange` callback not returning the correct `sort.field` value on pagination ([#5588](https://github.com/elastic/eui/pull/5588))
Expand Down
2 changes: 2 additions & 0 deletions src-docs/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { DataGridControlColumnsExample } from './views/datagrid/datagrid_control
import { DataGridFooterRowExample } from './views/datagrid/datagrid_footer_row_example';
import { DataGridVirtualizationExample } from './views/datagrid/datagrid_virtualization_example';
import { DataGridRowHeightOptionsExample } from './views/datagrid/datagrid_height_options_example';
import { DataGridRefExample } from './views/datagrid/datagrid_ref_example';

import { DatePickerExample } from './views/date_picker/date_picker_example';

Expand Down Expand Up @@ -489,6 +490,7 @@ const navigation = [
DataGridFooterRowExample,
DataGridVirtualizationExample,
DataGridRowHeightOptionsExample,
DataGridRefExample,
TableExample,
TableInMemoryExample,
].map((example) => createExample(example)),
Expand Down
24 changes: 19 additions & 5 deletions src-docs/src/views/datagrid/datagrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, {
createContext,
useContext,
useRef,
createRef,
} from 'react';
import { Link } from 'react-router-dom';
import { fake } from 'faker';
Expand All @@ -29,11 +30,13 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiPopover,
EuiScreenReaderOnly,
EuiText,
EuiTitle,
} from '../../../../src/components/';
const DataContext = createContext();

const gridRef = createRef();
const DataContext = createContext();
const raw_data = [];

for (let i = 1; i < 100; i++) {
Expand Down Expand Up @@ -216,13 +219,20 @@ const trailingControlColumns = [
{
id: 'actions',
width: 40,
headerCellRender: () => null,
rowCellRender: function RowCellRender() {
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>Controls</span>
</EuiScreenReaderOnly>
),
rowCellRender: function RowCellRender({ rowIndex, colIndex }) {
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
const closePopover = () => setIsPopoverVisible(false);

const [isModalVisible, setIsModalVisible] = useState(false);
const closeModal = () => setIsModalVisible(false);
const closeModal = () => {
setIsModalVisible(false);
gridRef.current.setFocusedCell({ rowIndex, colIndex });
};
const showModal = () => {
closePopover();
setIsModalVisible(true);
Expand Down Expand Up @@ -263,7 +273,10 @@ const trailingControlColumns = [
}

const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const closeFlyout = () => setIsFlyoutVisible(false);
const closeFlyout = () => {
setIsFlyoutVisible(false);
gridRef.current.setFocusedCell({ rowIndex, colIndex });
};
const showFlyout = () => {
closePopover();
setIsFlyoutVisible(true);
Expand Down Expand Up @@ -415,6 +428,7 @@ export default () => {
onChangePage: onChangePage,
}}
onColumnResize={onColumnResize.current}
ref={gridRef}
/>
</DataContext.Provider>
);
Expand Down
17 changes: 17 additions & 0 deletions src-docs/src/views/datagrid/datagrid_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
EuiDataGridRowHeightsOptions,
EuiDataGridCellValueElementProps,
EuiDataGridSchemaDetector,
EuiDataGridRefProps,
} from '!!prop-loader!../../../../src/components/datagrid/data_grid_types';

const gridSnippet = `
Expand Down Expand Up @@ -164,6 +165,8 @@ const gridSnippet = `
);
},
}}
// Optional. For advanced control of internal data grid popover/focus state, passes back an object of API methods
ref={dataGridRef}
/>
`;

Expand Down Expand Up @@ -323,6 +326,19 @@ const gridConcepts = [
</span>
),
},
{
title: 'ref',
description: (
<span>
Passes back an object of internal <strong>EuiDataGridRefProps</strong>{' '}
methods for advanced control of data grid popover/focus state. See{' '}
<Link to="/tabular-content/data-grid-ref-methods">
Data grid ref methods
</Link>{' '}
for more details and examples.
</span>
),
},
];

export const DataGridExample = {
Expand Down Expand Up @@ -414,6 +430,7 @@ export const DataGridExample = {
EuiDataGridToolBarAdditionalControlsLeftOptions,
EuiDataGridPopoverContentProps,
EuiDataGridRowHeightsOptions,
EuiDataGridRefProps,
},
demo: (
<Fragment>
Expand Down
117 changes: 117 additions & 0 deletions src-docs/src/views/datagrid/datagrid_ref_example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from 'react';

import { GuideSectionTypes } from '../../components';
import { EuiCode, EuiSpacer, EuiCallOut } from '../../../../src/components';

import { EuiDataGridRefProps } from '!!prop-loader!../../../../src/components/datagrid/data_grid_types';
import DataGridRef from './ref';
const dataGridRefSource = require('!!raw-loader!./ref');
const dataGridRefSnippet = `const dataGridRef = useRef();
<EuiDataGrid ref={dataGridRef} {...props} />
// Mnaually toggle the data grid's full screen state
dataGridRef.current.setIsFullScreen(true);
// Mnaually focus a specific cell within the data grid
dataGridRef.current.setFocusedCell({ rowIndex, colIndex });
// Manually opens the popover of a specified cell within the data grid
dataGridRef.current.openCellPopover({ rowIndex, colIndex });
// Close any open cell popover
dataGridRef.current.closeCellPopover();
`;

export const DataGridRefExample = {
title: 'Data grid ref methods',
sections: [
{
source: [
{
type: GuideSectionTypes.TSX,
code: dataGridRefSource,
},
],
text: (
<>
<p>
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 <EuiCode>ref</EuiCode> prop of <strong>EuiDataGrid</strong>.
These methods are:
</p>
<ul>
<li>
<p>
<EuiCode>setIsFullScreen(isFullScreen)</EuiCode> - controls the
full screen state of the data grid. Accepts a true/false boolean
flag.
</p>
</li>
<li>
<EuiCode>setFocusedCell({'{ rowIndex, colIndex }'})</EuiCode> -
focuses the specified cell in the grid.
<EuiSpacer size="s" />
<EuiCallOut
iconType="accessibility"
title="Using this method is an accessibility requirement if your data
grid toggles a modal or flyout."
color="warning"
>
Your modal or flyout should restore focus into the grid on close
to prevent keyboard or screen reader users from being stranded.
</EuiCallOut>
<EuiSpacer size="m" />
</li>
<li>
<EuiCode>openCellPopover({'{ rowIndex, colIndex }'})</EuiCode> -
opens the specified cell&apos;s popover contents.
<EuiSpacer size="m" />
</li>
<li>
<p>
<EuiCode>closeCellPopover()</EuiCode> - closes any currently
open cell popover.
</p>
</li>
</ul>

<EuiSpacer size="s" />

<EuiCallOut title="Handling cell location">
When using <EuiCode>setFocusedCell</EuiCode> or{' '}
<EuiCode>openCellPopover</EuiCode>, keep in mind:
<ul>
<li>
<EuiCode>colIndex</EuiCode> is affected by the user reordering
or hiding columns.
</li>
<li>
If the passed cell indices are outside the data grid&apos;s
total row count or visible column count, an error will be
thrown.
</li>
<li>
If the data grid is paginated or sorted, the grid will handle
automatically finding specified row index&apos;s correct
location for you.
</li>
</ul>
</EuiCallOut>

<EuiSpacer />

<p>
The below example shows how to use the internal APIs for a data grid
that opens a modal via cell actions.
</p>
</>
),
components: { DataGridRef },
demo: <DataGridRef />,
snippet: dataGridRefSnippet,
props: { EuiDataGridRefProps },
},
],
};
Loading

0 comments on commit d794a70

Please sign in to comment.