diff --git a/CHANGELOG.md b/CHANGELOG.md
index a754a65cbda..18a3f3500f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
- Fixed a `EuiDataGrid` sizing bug which didn't account for a horizontal scrollbar ([#5478](https://github.com/elastic/eui/pull/5478))
- Fixed `EuiModalHeaderTitle` to conditionally wrap title strings in an H1 ([#5494](https://github.com/elastic/eui/pull/5494))
+- Fixed a `EuiDataGrid` issue where a focused cell would lose focus when scrolled out of and back into view ([#5488](https://github.com/elastic/eui/pull/5488))
**Deprecations**
diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js
index db272b1635d..47a606c471c 100644
--- a/src-docs/src/views/datagrid/datagrid_example.js
+++ b/src-docs/src/views/datagrid/datagrid_example.js
@@ -358,8 +358,8 @@ export const DataGridExample = {
in memory level
{' '}
to have the grid automatically handle updating your columns.
- Depending upon the level choosen you may need to manage the
- content order separate from the grid.
+ Depending upon the level chosen, you may need to manage the
+ content order separately from the grid.
diff --git a/src-docs/src/views/datagrid/datagrid_focus_example.js b/src-docs/src/views/datagrid/datagrid_focus_example.js
index d4718725ddd..db960a9e8c5 100644
--- a/src-docs/src/views/datagrid/datagrid_focus_example.js
+++ b/src-docs/src/views/datagrid/datagrid_focus_example.js
@@ -93,7 +93,7 @@ export const DataGridFocusExample = {
color="warning"
title="A caution about turning off cell expansion when the width of the column is unknown"
>
- In general, you should turn isExpandible to false
+ In general, you should turn isExpandable to false
only when you know the exact width and number of items that a cell
will include. Control columns that contain row actions are a good
example of when to use them. In certain scenarios, allowing multiple
diff --git a/src/components/datagrid/body/data_grid_cell.test.tsx b/src/components/datagrid/body/data_grid_cell.test.tsx
index 27763b0dcd0..76551df7129 100644
--- a/src/components/datagrid/body/data_grid_cell.test.tsx
+++ b/src/components/datagrid/body/data_grid_cell.test.tsx
@@ -10,6 +10,7 @@ import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { keys } from '../../../services';
import { mockRowHeightUtils } from '../__mocks__/row_height_utils';
+import { DataGridFocusContext } from '../data_grid_context';
import { EuiDataGridCell } from './data_grid_cell';
@@ -31,31 +32,24 @@ describe('EuiDataGridCell', () => {
rowHeightUtils: mockRowHeightUtils,
};
- const mountEuiDataGridCellWithContext = ({ ...props } = {}) => {
- const focusContext = {
- setFocusedCell: jest.fn(),
- onFocusUpdate: jest.fn(),
- };
- return mount(, {
- context: focusContext,
- });
- };
-
beforeEach(() => jest.clearAllMocks());
it('renders', () => {
- const component = mountEuiDataGridCellWithContext();
+ const component = mount();
expect(component).toMatchSnapshot();
});
it('renders cell buttons', () => {
- const component = mountEuiDataGridCellWithContext({
- isExpandable: false,
- column: {
- id: 'someColumn',
- cellActions: [() => ],
- },
- });
+ const component = mount(
+ ],
+ }}
+ />
+ );
component.setState({ popoverIsOpen: true });
const cellButtons = component.find('EuiDataGridCellButtons');
@@ -80,7 +74,7 @@ describe('EuiDataGridCell', () => {
EuiDataGridCell.prototype,
'shouldComponentUpdate'
);
- component = mountEuiDataGridCellWithContext();
+ component = mount();
});
afterEach(() => {
shouldComponentUpdate.mockRestore();
@@ -164,13 +158,49 @@ describe('EuiDataGridCell', () => {
describe('componentDidUpdate', () => {
it('resets cell props when the cell columnId changes', () => {
const setState = jest.spyOn(EuiDataGridCell.prototype, 'setState');
- const component = mountEuiDataGridCellWithContext();
+ const component = mount();
component.setProps({ columnId: 'newColumnId' });
expect(setState).toHaveBeenCalledWith({ cellProps: {} });
});
});
+ describe('componentDidMount', () => {
+ const focusContext = {
+ focusedCell: undefined,
+ onFocusUpdate: jest.fn(),
+ setFocusedCell: jest.fn(),
+ };
+
+ it('creates an onFocusUpdate subscription', () => {
+ mount(
+
+
+
+ );
+
+ expect(focusContext.onFocusUpdate).toHaveBeenCalled();
+ });
+
+ it('mounts the cell with focus state if the current cell should be focused', () => {
+ const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus');
+ const component = mount(
+
+
+
+ );
+
+ expect((component.instance().state as any).isFocused).toEqual(true);
+ expect(focusSpy).toHaveBeenCalledWith({ preventScroll: true });
+ });
+ });
+
// TODO: Test ResizeObserver logic in Cypress alongside Jest
describe('row height logic & resize observers', () => {
describe('recalculateAutoHeight', () => {
@@ -184,9 +214,12 @@ describe('EuiDataGridCell', () => {
it('sets the row height cache with cell heights on update', () => {
(mockRowHeightUtils.isAutoHeight as jest.Mock).mockReturnValue(true);
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: { defaultHeight: 'auto' },
- });
+ const component = mount(
+
+ );
triggerUpdate(component);
expect(mockRowHeightUtils.setRowHeight).toHaveBeenCalled();
@@ -195,9 +228,12 @@ describe('EuiDataGridCell', () => {
it('does not update the cache if cell height is not auto', () => {
(mockRowHeightUtils.isAutoHeight as jest.Mock).mockReturnValue(false);
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: { defaultHeight: 34 },
- });
+ const component = mount(
+
+ );
triggerUpdate(component);
expect(mockRowHeightUtils.setRowHeight).not.toHaveBeenCalled();
@@ -212,10 +248,13 @@ describe('EuiDataGridCell', () => {
describe('default height', () => {
it('observes the first cell for size changes and calls this.props.setRowHeight on change', () => {
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: { defaultHeight: { lineCount: 3 } },
- setRowHeight,
- });
+ const component = mount(
+
+ );
callMethod(component);
expect(
@@ -227,14 +266,17 @@ describe('EuiDataGridCell', () => {
describe('row height overrides', () => {
it('uses the rowHeightUtils.setRowHeight cache instead of this.props.setRowHeight', () => {
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: {
- defaultHeight: { lineCount: 3 },
- rowHeights: { 10: { lineCount: 10 } },
- },
- rowIndex: 10,
- setRowHeight,
- });
+ const component = mount(
+
+ );
callMethod(component);
expect(
@@ -246,10 +288,13 @@ describe('EuiDataGridCell', () => {
});
it('recalculates when rowHeightsOptions.defaultHeight.lineCount changes', () => {
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: { defaultHeight: { lineCount: 7 } },
- setRowHeight,
- });
+ const component = mount(
+
+ );
component.setProps({
rowHeightsOptions: { defaultHeight: { lineCount: 6 } },
@@ -258,10 +303,13 @@ describe('EuiDataGridCell', () => {
});
it('calculates undefined heights as single rows with a lineCount of 1', () => {
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: { defaultHeight: undefined },
- setRowHeight,
- });
+ const component = mount(
+
+ );
callMethod(component);
expect(
@@ -271,10 +319,13 @@ describe('EuiDataGridCell', () => {
});
it('does nothing if cell height is not lineCount or undefined', () => {
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: { defaultHeight: 34 },
- setRowHeight,
- });
+ const component = mount(
+
+ );
callMethod(component);
expect(setRowHeight).not.toHaveBeenCalled();
@@ -286,7 +337,7 @@ describe('EuiDataGridCell', () => {
describe('interactions', () => {
describe('keyboard events', () => {
it('when cell is expandable', () => {
- const component = mountEuiDataGridCellWithContext();
+ const component = mount();
const preventDefault = jest.fn();
component.simulate('keyDown', { preventDefault, key: keys.ENTER });
@@ -296,9 +347,9 @@ describe('EuiDataGridCell', () => {
});
it('when cell is not expandable', () => {
- const component = mountEuiDataGridCellWithContext({
- isExpandable: false,
- });
+ const component = mount(
+
+ );
const preventDefault = jest.fn();
component.simulate('keyDown', { preventDefault, key: keys.ENTER });
@@ -321,7 +372,7 @@ describe('EuiDataGridCell', () => {
});
it('mouse events', () => {
- const component = mountEuiDataGridCellWithContext();
+ const component = mount();
component.simulate('mouseEnter');
expect(component.state('enableInteractions')).toEqual(true);
component.simulate('mouseLeave');
@@ -329,7 +380,7 @@ describe('EuiDataGridCell', () => {
});
it('focus/blur events', () => {
- const component = mountEuiDataGridCellWithContext();
+ const component = mount();
component.simulate('focus');
component.simulate('blur');
expect(component.state('disableCellTabIndex')).toEqual(false);
@@ -337,12 +388,15 @@ describe('EuiDataGridCell', () => {
});
it('renders certain classes/styles if rowHeightOptions is passed', () => {
- const component = mountEuiDataGridCellWithContext({
- rowHeightsOptions: {
- defaultHeight: 20,
- rowHeights: { 0: 10 },
- },
- });
+ const component = mount(
+
+ );
component.setState({ popoverIsOpen: true });
expect(
diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx
index 9b253a403d4..8a89a48b5b9 100644
--- a/src/components/datagrid/body/data_grid_cell.tsx
+++ b/src/components/datagrid/body/data_grid_cell.tsx
@@ -142,7 +142,7 @@ export class EuiDataGridCell extends Component<
return [];
};
- takeFocus = () => {
+ takeFocus = (preventScroll: boolean) => {
const cell = this.cellRef.current;
if (cell) {
@@ -157,9 +157,9 @@ export class EuiDataGridCell extends Component<
const interactables = this.getInteractables();
if (this.props.isExpandable === false && interactables.length === 1) {
// Only one element can be interacted with
- interactables[0].focus();
+ interactables[0].focus({ preventScroll });
} else {
- cell.focus();
+ cell.focus({ preventScroll });
}
}
}
@@ -225,16 +225,29 @@ export class EuiDataGridCell extends Component<
};
componentDidMount() {
+ const { colIndex, visibleRowIndex } = this.props;
+
this.unsubscribeCell = this.context.onFocusUpdate(
- [this.props.colIndex, this.props.visibleRowIndex],
+ [colIndex, visibleRowIndex],
this.onFocusUpdate
);
+
+ // Account for virtualization - when a cell unmounts when scrolled out of view
+ // and then remounts when scrolled back into view, it should retain focus state
+ if (
+ this.context.focusedCell?.[0] === colIndex &&
+ this.context.focusedCell?.[1] === visibleRowIndex
+ ) {
+ // The second flag sets preventScroll: true as a focus option, which prevents
+ // hijacking the user's scroll behavior when the cell re-mounts on scroll
+ this.onFocusUpdate(true, true);
+ }
}
- onFocusUpdate = (isFocused: boolean) => {
+ onFocusUpdate = (isFocused: boolean, preventScroll = false) => {
this.setState({ isFocused }, () => {
if (isFocused) {
- this.takeFocus();
+ this.takeFocus(preventScroll);
}
});
};
diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx
index af29264c086..438462d93cb 100644
--- a/src/components/datagrid/data_grid.tsx
+++ b/src/components/datagrid/data_grid.tsx
@@ -748,10 +748,11 @@ export const EuiDataGrid: FunctionComponent = (props) => {
);
const datagridFocusContext = useMemo(() => {
return {
+ focusedCell,
setFocusedCell,
onFocusUpdate,
};
- }, [setFocusedCell, onFocusUpdate]);
+ }, [focusedCell, setFocusedCell, onFocusUpdate]);
const gridId = useGeneratedHtmlId();
const ariaLabelledById = useGeneratedHtmlId();
diff --git a/src/components/datagrid/data_grid_context.tsx b/src/components/datagrid/data_grid_context.tsx
index 9a252106287..5234bb5c83a 100644
--- a/src/components/datagrid/data_grid_context.tsx
+++ b/src/components/datagrid/data_grid_context.tsx
@@ -16,6 +16,7 @@ import {
export const DataGridFocusContext = React.createContext<
DataGridFocusContextShape
>({
+ focusedCell: undefined,
setFocusedCell: () => {},
onFocusUpdate: () => () => {},
});
diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts
index 103898dfc26..9043dc07b17 100644
--- a/src/components/datagrid/data_grid_types.ts
+++ b/src/components/datagrid/data_grid_types.ts
@@ -167,6 +167,7 @@ export type EuiDataGridFooterRowProps = CommonProps &
};
export interface DataGridFocusContextShape {
+ focusedCell?: EuiDataGridFocusedCell;
setFocusedCell: (cell: EuiDataGridFocusedCell) => void;
onFocusUpdate: (
cell: EuiDataGridFocusedCell,