Skip to content

Commit

Permalink
[EuiDataGrid] Set up ref that exposes focus/popover internal APIs (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
Constance and chandlerprall authored Jan 11, 2022
1 parent dff7a77 commit 59461c7
Show file tree
Hide file tree
Showing 13 changed files with 725 additions and 366 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 access certain `EuiDataGrid` internal methods via the `ref` prop ([#5499](https://github.com/elastic/eui/pull/5499))

**Breaking changes**

- Removed `data-test-subj="dataGridWrapper"` from `EuiDataGrid` in favor of `data-test-subj="euiDataGridBody"` ([#5506](https://github.com/elastic/eui/pull/5506))
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
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
67 changes: 67 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,67 @@
import React from 'react';

import { GuideSectionTypes } from '../../components';
import {
EuiCode,
EuiCodeBlock,
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 focus a specific cell within the data grid
dataGridRef.current.setFocusedCell({ rowIndex, colIndex });
`;

export const DataGridRefExample = {
title: 'Data grid ref methods',
sections: [
{
source: [
{
type: GuideSectionTypes.JS,
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 EuiDataGrid. These methods are:
</p>
<ul>
<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."
>
Your modal or flyout should restore focus into the grid on close
to prevent keyboard or screen reader users from being stranded.
</EuiCallOut>
</li>
</ul>
<EuiCodeBlock language="jsx">{dataGridRefSnippet}</EuiCodeBlock>
<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 },
},
],
};
196 changes: 196 additions & 0 deletions src-docs/src/views/datagrid/ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React, { useCallback, useMemo, useState, useRef } from 'react';
import { fake } from 'faker';

import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiFormRow,
EuiFieldNumber,
EuiButton,
EuiDataGrid,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiText,
} from '../../../../src/components/';

const raw_data = [];
for (let i = 1; i < 100; i++) {
raw_data.push({
name: fake('{{name.lastName}}, {{name.firstName}}'),
email: fake('{{internet.email}}'),
location: fake('{{address.city}}, {{address.country}}'),
account: fake('{{finance.account}}'),
date: fake('{{date.past}}'),
});
}

export default () => {
const dataGridRef = useRef();

// Modal
const [isModalVisible, setIsModalVisible] = useState(false);
const [lastFocusedCell, setLastFocusedCell] = useState({});

const closeModal = useCallback(() => {
setIsModalVisible(false);
dataGridRef.current.setFocusedCell(lastFocusedCell); // Set the data grid focus back to the cell that opened the modal
}, [lastFocusedCell]);

const showModal = useCallback(({ rowIndex, colIndex }) => {
setIsModalVisible(true);
setLastFocusedCell({ rowIndex, colIndex }); // Store the cell that opened this modal
}, []);

const openModalAction = useCallback(
({ Component, rowIndex, colIndex }) => {
return (
<Component
onClick={() => showModal({ rowIndex, colIndex })}
iconType="faceHappy"
aria-label="Open modal"
>
Open modal
</Component>
);
},
[showModal]
);

// Columns
const columns = useMemo(
() => [
{
id: 'name',
displayAsText: 'Name',
cellActions: [openModalAction],
},
{
id: 'email',
displayAsText: 'Email address',
initialWidth: 130,
cellActions: [openModalAction],
},
{
id: 'location',
displayAsText: 'Location',
cellActions: [openModalAction],
},
{
id: 'account',
displayAsText: 'Account',
cellActions: [openModalAction],
},
{
id: 'date',
displayAsText: 'Date',
cellActions: [openModalAction],
},
],
[openModalAction]
);

// Column visibility
const [visibleColumns, setVisibleColumns] = useState(() =>
columns.map(({ id }) => id)
);

// Pagination
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
const onChangePage = useCallback(
(pageIndex) =>
setPagination((pagination) => ({ ...pagination, pageIndex })),
[]
);

// Manual cell focus
const [rowIndexAction, setRowIndexAction] = useState(0);
const [colIndexAction, setColIndexAction] = useState(0);

return (
<>
<EuiFlexGroup alignItems="flexEnd" gutterSize="s" style={{ width: 500 }}>
<EuiFlexItem>
<EuiFormRow label="Row index">
<EuiFieldNumber
min={0}
max={24}
value={rowIndexAction}
onChange={(e) => setRowIndexAction(Number(e.target.value))}
compressed
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="Column index">
<EuiFieldNumber
min={0}
max={4}
value={colIndexAction}
onChange={(e) => setColIndexAction(Number(e.target.value))}
compressed
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
size="s"
onClick={() =>
dataGridRef.current.setFocusedCell({
rowIndex: rowIndexAction,
colIndex: colIndexAction,
})
}
>
Set cell focus
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />

<EuiDataGrid
aria-label="Data grid demo"
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
rowCount={raw_data.length}
renderCellValue={({ rowIndex, columnId }) =>
raw_data[rowIndex][columnId]
}
pagination={{
...pagination,
pageSizeOptions: [25],
onChangePage: onChangePage,
}}
height={400}
ref={dataGridRef}
/>
{isModalVisible && (
<EuiModal onClose={closeModal} style={{ width: 500 }}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h2>Example modal</h2>
</EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
<EuiText>
<p>
When closed, this modal should re-focus into the cell that
toggled it.
</p>
</EuiText>
</EuiModalBody>

<EuiModalFooter>
<EuiButton onClick={closeModal} fill>
Close
</EuiButton>
</EuiModalFooter>
</EuiModal>
)}
</>
);
};
4 changes: 3 additions & 1 deletion src/components/datagrid/body/data_grid_cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ export class EuiDataGridCell extends Component<
rowManager,
...rest
} = this.props;
const { rowIndex } = rest;
const { rowIndex, colIndex } = rest;

const showCellButtons =
this.state.isFocused ||
Expand Down Expand Up @@ -546,6 +546,7 @@ export class EuiDataGridCell extends Component<
</div>
<EuiDataGridCellButtons
rowIndex={rowIndex}
colIndex={colIndex}
column={column}
popoverIsOpen={this.state.popoverIsOpen}
closePopover={this.closePopover}
Expand Down Expand Up @@ -588,6 +589,7 @@ export class EuiDataGridCell extends Component<
panelRefFn={(ref) => (this.popoverPanelRef.current = ref)}
popoverIsOpen={this.state.popoverIsOpen}
rowIndex={rowIndex}
colIndex={colIndex}
renderCellValue={rest.renderCellValue}
popoverContent={PopoverContent}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/components/datagrid/body/data_grid_cell_buttons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('EuiDataGridCellButtons', () => {
closePopover: jest.fn(),
onExpandClick: jest.fn(),
rowIndex: 0,
colIndex: 0,
};

it('renders an expand button', () => {
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('EuiDataGridCellButtons', () => {
<Component
Component={[Function]}
closePopover={[MockFunction]}
colIndex={0}
columnId="someId"
isExpanded={false}
key="0"
Expand Down
Loading

0 comments on commit 59461c7

Please sign in to comment.