From c06db86ae22f0db1ffb5f8da04172c9aa71c8ecb Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Tue, 11 Jan 2022 10:27:58 -0800 Subject: [PATCH 01/10] [#5517] Add `isFocusedCellInView` state to track visibility - and use this to determine whether or not the entire grid should have a tabindex/be focusable, rather than a simple 'hasFocus' check which doesn't account for virtualization - Add new mockFocusContext for easier testing for various tests that need to set a focus context - EuiDataGridCell - DRY out an `this.isFocusedCell` method - Call `setIsFocusedCellInView` on mount and on unmount (`setFocusedCell` handles setting true/false on new cell focus) - Write new unit tests for componentWillUnmount - Clean up unnecessary uncovered function fallback for this.unsubscribeCell - we're already checking for a falsy value before calling it --- .../datagrid/body/data_grid_cell.test.tsx | 51 +++++++++++++++---- .../datagrid/body/data_grid_cell.tsx | 19 +++++-- .../data_grid_header_cell_wrapper.test.tsx | 11 ++-- src/components/datagrid/data_grid_types.ts | 1 + .../datagrid/utils/__mocks__/focus_context.ts | 14 +++++ src/components/datagrid/utils/focus.ts | 16 ++++-- 6 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 src/components/datagrid/utils/__mocks__/focus_context.ts diff --git a/src/components/datagrid/body/data_grid_cell.test.tsx b/src/components/datagrid/body/data_grid_cell.test.tsx index 5f96eca8111..e74c9efd830 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 '../utils/__mocks__/row_heights'; +import { mockFocusContext } from '../utils/__mocks__/focus_context'; import { DataGridFocusContext } from '../utils/focus'; import { EuiDataGridCell } from './data_grid_cell'; @@ -166,27 +167,21 @@ describe('EuiDataGridCell', () => { }); describe('componentDidMount', () => { - const focusContext = { - focusedCell: undefined, - onFocusUpdate: jest.fn(), - setFocusedCell: jest.fn(), - }; - it('creates an onFocusUpdate subscription', () => { mount( - + ); - expect(focusContext.onFocusUpdate).toHaveBeenCalled(); + expect(mockFocusContext.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 }); + expect(mockFocusContext.setIsFocusedCellInView).toHaveBeenCalledWith( + true + ); + }); + }); + + describe('componentWillUnmount', () => { + it('unsubscribes from the onFocusUpdate subscription', () => { + const unsubscribeCellMock = jest.fn(); + mockFocusContext.onFocusUpdate.mockReturnValueOnce(unsubscribeCellMock); + + const component = mount( + + + + ); + component.unmount(); + + expect(unsubscribeCellMock).toHaveBeenCalled(); + }); + + it('sets isFocusedCellInView to false if the current cell is focused and unmounting due to being scrolled out of view', () => { + const component = mount( + + + + ); + component.unmount(); + + expect(mockFocusContext.setIsFocusedCellInView).toHaveBeenCalledWith( + false + ); }); }); diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index 26ab518a0a4..2f200cf1684 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -124,7 +124,7 @@ export class EuiDataGridCell extends Component< enableInteractions: false, disableCellTabIndex: false, }; - unsubscribeCell?: Function = () => {}; + unsubscribeCell?: Function; focusTimeout: number | undefined; style = null; @@ -234,16 +234,21 @@ export class EuiDataGridCell extends Component< // 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 - ) { + if (this.isFocusedCell()) { // 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); + this.context.setIsFocusedCellInView(true); } } + isFocusedCell = () => { + return ( + this.context.focusedCell?.[0] === this.props.colIndex && + this.context.focusedCell?.[1] === this.props.visibleRowIndex + ); + }; + onFocusUpdate = (isFocused: boolean, preventScroll = false) => { this.setState({ isFocused }, () => { if (isFocused) { @@ -257,6 +262,10 @@ export class EuiDataGridCell extends Component< if (this.unsubscribeCell) { this.unsubscribeCell(); } + + if (this.isFocusedCell()) { + this.context.setIsFocusedCellInView(false); + } } componentDidUpdate(prevProps: EuiDataGridCellProps) { diff --git a/src/components/datagrid/body/header/data_grid_header_cell_wrapper.test.tsx b/src/components/datagrid/body/header/data_grid_header_cell_wrapper.test.tsx index 858bf32c97d..908593766de 100644 --- a/src/components/datagrid/body/header/data_grid_header_cell_wrapper.test.tsx +++ b/src/components/datagrid/body/header/data_grid_header_cell_wrapper.test.tsx @@ -12,6 +12,7 @@ import { mount } from 'enzyme'; import { keys } from '../../../../services'; import { DataGridFocusContext } from '../../utils/focus'; +import { mockFocusContext } from '../../utils/__mocks__/focus_context'; import { EuiDataGridHeaderCellWrapper } from './data_grid_header_cell_wrapper'; @@ -23,16 +24,12 @@ describe('EuiDataGridHeaderCellWrapper', () => { children: