From a870b2be7a123e5b07bae13b96e57130e97aa10c Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 7 Mar 2022 09:01:04 -0800 Subject: [PATCH 1/7] Add unit tests for useHeaderFocusWorkaround --- .../utils/{focus.test.ts => focus.test.tsx} | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) rename src/components/datagrid/utils/{focus.test.ts => focus.test.tsx} (54%) diff --git a/src/components/datagrid/utils/focus.test.ts b/src/components/datagrid/utils/focus.test.tsx similarity index 54% rename from src/components/datagrid/utils/focus.test.ts rename to src/components/datagrid/utils/focus.test.tsx index 714a101bb29..989373afe1d 100644 --- a/src/components/datagrid/utils/focus.test.ts +++ b/src/components/datagrid/utils/focus.test.tsx @@ -6,7 +6,14 @@ * Side Public License, v 1. */ -import { getParentCellContent } from './focus'; +import React from 'react'; +import { mount } from 'enzyme'; + +import { + DataGridFocusContext, + getParentCellContent, + useHeaderFocusWorkaround, +} from './focus'; describe('getParentCellContent', () => { const doc = document.createDocumentFragment(); @@ -40,3 +47,36 @@ describe('getParentCellContent', () => { expect(getParentCellContent(body)).toBeNull(); }); }); + +describe('useHeaderFocusWorkaround', () => { + const MockComponent = () => { + useHeaderFocusWorkaround(false); + return
; + }; + + it('moves focus down from the header to the first data row if the header becomes uninteractive', () => { + const focusedCell = [2, -1]; + const setFocusedCell = jest.fn(); + mount( + + + + ); + expect(setFocusedCell).toHaveBeenCalledWith([2, 0]); + }); + + it('does nothing if the focus was not on the header when the header became uninteractive', () => { + const focusedCell = [2, 0]; + const setFocusedCell = jest.fn(); + mount( + + + + ); + expect(setFocusedCell).not.toHaveBeenCalled(); + }); +}); From 6f68e45c446741252b3c41a94daf48293584528a Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 7 Mar 2022 09:43:59 -0800 Subject: [PATCH 2/7] Add unit tests for preventTabbing + remove unnecessary role=gridcell check - after inspecting our DOM, that role attr is set on the topmost cell wrapper but `data-datagrid-cellcontent` is set on a div below that, so there's no way the parent cell should be inside the cell content + swap dataset check for hasAttribute - for some reason Jest/JSDOM was failing to register dataset, but hasAttribute works and should be equivalent --- src/components/datagrid/utils/focus.test.tsx | 55 +++++++++++++++++++- src/components/datagrid/utils/focus.ts | 5 +- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/components/datagrid/utils/focus.test.tsx b/src/components/datagrid/utils/focus.test.tsx index 989373afe1d..71fc0c04790 100644 --- a/src/components/datagrid/utils/focus.test.tsx +++ b/src/components/datagrid/utils/focus.test.tsx @@ -11,10 +11,61 @@ import { mount } from 'enzyme'; import { DataGridFocusContext, + preventTabbing, getParentCellContent, useHeaderFocusWorkaround, } from './focus'; +describe('preventTabbing', () => { + const mockCellWithInteractiveChildren = () => { + const cell = document.createElement('div'); + cell.setAttribute('data-datagrid-cellcontent', 'true'); + + const button = cell.appendChild(document.createElement('button')); + const link = cell.appendChild(document.createElement('a')); + link.setAttribute('href', 'courageous'); + + return [cell, button, link]; + }; + + it('on mutation, sets all interactive children of the parent cell to tabindex -1', () => { + const [cell, button, link] = mockCellWithInteractiveChildren(); + preventTabbing([{ target: cell }] as any); + + expect(button.getAttribute('tabIndex')).toEqual('-1'); + expect(button.getAttribute('data-datagrid-interactable')).toEqual('true'); + + expect(link.getAttribute('tabIndex')).toEqual('-1'); + expect(link.getAttribute('data-datagrid-interactable')).toEqual('true'); + }); + + it('stops early if two separate mutation records occur from the same cell', () => { + const [button, link] = mockCellWithInteractiveChildren(); + preventTabbing([{ target: button }, { target: link }] as any); + + // There isn't a super great way of asserting a continue, + // so this is mostly just here for line code coverage + }); + + it('does nothing if the mutation event does not have a valid parent cell', () => { + const notCell = document.createElement('div'); + preventTabbing([{ target: notCell }] as any); + + // There isn't a super great way of asserting this, + // so this is mostly just here for branch code coverage + }); + + it('ignores header cells that manage their own tabindex state (data-euigrid-tab-managed attr)', () => { + const [cell, button] = mockCellWithInteractiveChildren(); + button.setAttribute('data-euigrid-tab-managed', 'true'); + button.setAttribute('tabIndex', '0'); + + preventTabbing([{ target: cell }] as any); + + expect(button.getAttribute('tabIndex')).toEqual('0'); + }); +}); + describe('getParentCellContent', () => { const doc = document.createDocumentFragment(); @@ -36,11 +87,11 @@ describe('getParentCellContent', () => { }); it('locates the cell element when starting with an element inside the cell', () => { - expect(getParentCellContent(span!)).toBe(cell); + expect(getParentCellContent(span)).toBe(cell); }); it('locates the cell element when starting with a text node inside the cell', () => { - expect(getParentCellContent(text!)).toBe(cell); + expect(getParentCellContent(text)).toBe(cell); }); it('does not locate the cell element when starting outside the cell', () => { diff --git a/src/components/datagrid/utils/focus.ts b/src/components/datagrid/utils/focus.ts index 1bcfdc023d9..0dc00c313b8 100644 --- a/src/components/datagrid/utils/focus.ts +++ b/src/components/datagrid/utils/focus.ts @@ -271,10 +271,7 @@ export const preventTabbing = (records: MutationRecord[]) => { const tabbables = tabbable(cell); for (let i = 0; i < tabbables.length; i++) { const element = tabbables[i]; - if ( - element.getAttribute('role') !== 'gridcell' && - !element.dataset['euigrid-tab-managed'] - ) { + if (!element.hasAttribute('data-euigrid-tab-managed')) { element.setAttribute('tabIndex', '-1'); element.setAttribute('data-datagrid-interactable', 'true'); } From 7e81b0fb6e371020a23f828b39d646e533b75582 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 7 Mar 2022 11:48:10 -0800 Subject: [PATCH 3/7] Add unit tests for createKeyDownHandler --- src/components/datagrid/utils/focus.test.tsx | 327 ++++++++++++++++++- 1 file changed, 326 insertions(+), 1 deletion(-) diff --git a/src/components/datagrid/utils/focus.test.tsx b/src/components/datagrid/utils/focus.test.tsx index 71fc0c04790..52ee8b7eaaa 100644 --- a/src/components/datagrid/utils/focus.test.tsx +++ b/src/components/datagrid/utils/focus.test.tsx @@ -8,14 +8,339 @@ import React from 'react'; import { mount } from 'enzyme'; - +import { keys } from '../../../services'; import { DataGridFocusContext, + createKeyDownHandler, preventTabbing, getParentCellContent, useHeaderFocusWorkaround, } from './focus'; +describe('createKeyDownHandler', () => { + const focusContext = { + focusedCell: [0, 0], + setFocusedCell: jest.fn(), + } as any; + const mockArgs = { + gridElement: document.createElement('div'), + visibleColCount: 5, + visibleRowCount: 10, + visibleRowStartIndex: 0, + rowCount: 10, + pagination: undefined, + hasFooter: false, + headerIsInteractive: true, + focusContext, + }; + const mockKeyDown = { + preventDefault: jest.fn(), + key: keys.ARROW_UP, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + // Ensure document.activeElement is inside the grid pass the early return + const mockFocus = mockArgs.gridElement.appendChild( + document.createElement('button') + ); + mockFocus.focus(); + }); + + describe('left arrow key', () => { + it('moves the focusedCell x position left by 1', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [1, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_LEFT }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([0, 1]); + }); + + describe('when focus is on the left-most column', () => { + it('does nothing', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [0, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_LEFT }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + }); + }); + + describe('right arrow key', () => { + it('moves the focusedCell x position right by 1', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [1, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_RIGHT }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([2, 1]); + }); + + describe('when focus is on the right-most column', () => { + it('does nothing', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [4, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_RIGHT }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + }); + }); + + describe('down arrow key', () => { + it('moves the focusedCell y position down by 1', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [1, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_DOWN }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([1, 2]); + }); + + describe('when focus is on the bottom-most row', () => { + it('does nothing', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [1, 9] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_DOWN }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + + it('accounts for the footer row', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + hasFooter: true, + focusContext: { ...focusContext, focusedCell: [1, 10] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_DOWN }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + }); + + describe('when moving focus down from the sticky header', () => { + it('moves focus down to the visibleRowStartIndex, since rowIndex 0 may not be virtualized', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + visibleRowStartIndex: 5, + focusContext: { ...focusContext, focusedCell: [1, -1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_DOWN }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([1, 5]); + }); + }); + }); + + describe('up arrow key', () => { + it('moves the focusedCell y position up by 1', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [1, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_UP }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([1, 0]); + }); + + describe('when focus is on the top-most row', () => { + it('does nothing', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + headerIsInteractive: false, + focusContext: { ...focusContext, focusedCell: [1, 0] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_UP }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + + it('accounts for an interactive header row', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + headerIsInteractive: true, + focusContext: { ...focusContext, focusedCell: [1, -1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.ARROW_UP }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + }); + }); + + describe('page down key', () => { + describe('when grid pagination is set', () => { + const pagination = { + pageIndex: 0, + pageSize: 5, + onChangePage: jest.fn(), + onChangeItemsPerPage: () => {}, + }; + + it('paginates to the next page and sets the focus to the first data row on that page', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + pagination, + focusContext: { ...focusContext, focusedCell: [2, 5] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.PAGE_DOWN }); + + expect(pagination.onChangePage).toHaveBeenCalledWith(1); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([2, 0]); + }); + + it('does not paginate if already on the last page, but still moves focus to the last row', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + pagination: { ...pagination, pageIndex: 1 }, + focusContext: { ...focusContext, focusedCell: [2, 5] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.PAGE_DOWN }); + + expect(pagination.onChangePage).not.toHaveBeenCalled(); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([2, 0]); + }); + }); + + describe('when grid pagination is not set', () => { + it('does nothing', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + pagination: undefined, + }); + keyDownHandler({ ...mockKeyDown, key: keys.PAGE_DOWN }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + }); + }); + + describe('page up key', () => { + describe('when grid pagination is set', () => { + const pagination = { + pageIndex: 1, + pageSize: 5, + onChangePage: jest.fn(), + onChangeItemsPerPage: () => {}, + }; + + it('paginates to the previous page and sets the focus to the last data row on that page', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + pagination, + focusContext: { ...focusContext, focusedCell: [2, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.PAGE_UP }); + + expect(pagination.onChangePage).toHaveBeenCalled(); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([2, 4]); + }); + + it('does not paginate if already on the first page, but still moves focus to the last row', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + pagination: { ...pagination, pageIndex: 0 }, + focusContext: { ...focusContext, focusedCell: [2, 1] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.PAGE_UP }); + + expect(pagination.onChangePage).not.toHaveBeenCalledWith(0); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([2, 4]); + }); + }); + + describe('when grid pagination is not set', () => { + it('does nothing', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + pagination: undefined, + }); + keyDownHandler({ ...mockKeyDown, key: keys.PAGE_UP }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + }); + }); + + describe('home key', () => { + it('moves the focusedCell to the leftmost column in the current row', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [2, 2] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.HOME }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([0, 2]); + }); + + describe('ctrl+home key', () => { + it('moves the focusedCell to the topmost and leftmost cell on the current page', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [2, 2] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.HOME, ctrlKey: true }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([0, 0]); + }); + }); + }); + + describe('end key', () => { + it('moves the focusedCell to the rightmost column in the current row', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [2, 2] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.END }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([4, 2]); + }); + + describe('ctrl+end key', () => { + it('moves the focusedCell to the rightmost and bottommost cell on the current page', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: [2, 2] }, + }); + keyDownHandler({ ...mockKeyDown, key: keys.END, ctrlKey: true }); + expect(focusContext.setFocusedCell).toHaveBeenCalledWith([4, 9]); + }); + }); + }); + + it('does nothing for other non-navigation keys', () => { + const keyDownHandler = createKeyDownHandler(mockArgs); + keyDownHandler({ ...mockKeyDown, key: keys.SPACE }); + expect(focusContext.setFocusedCell).not.toHaveBeenCalled(); + }); + + // mostly here for branch/code coverage + describe('early returns', () => { + test('unintialized focusedCell', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + focusContext: { ...focusContext, focusedCell: undefined }, + }); + keyDownHandler(mockKeyDown); + expect(mockKeyDown.preventDefault).not.toHaveBeenCalled(); + }); + + test('unintialized grids', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + gridElement: null, + }); + keyDownHandler(mockKeyDown); + expect(mockKeyDown.preventDefault).not.toHaveBeenCalled(); + }); + + test('document.activeElement is not inside the grid', () => { + const keyDownHandler = createKeyDownHandler({ + ...mockArgs, + gridElement: document.createElement('div'), + }); + keyDownHandler(mockKeyDown); + expect(mockKeyDown.preventDefault).not.toHaveBeenCalled(); + }); + }); +}); + describe('preventTabbing', () => { const mockCellWithInteractiveChildren = () => { const cell = document.createElement('div'); From e5e4ad8ae07bc8d2dad2a4fab87201f21e2d8fee Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 7 Mar 2022 11:56:23 -0800 Subject: [PATCH 4/7] Add unit tests for notifyCellOfFocusState --- src/components/datagrid/utils/focus.test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/datagrid/utils/focus.test.tsx b/src/components/datagrid/utils/focus.test.tsx index 52ee8b7eaaa..a0cbde9f7d6 100644 --- a/src/components/datagrid/utils/focus.test.tsx +++ b/src/components/datagrid/utils/focus.test.tsx @@ -11,12 +11,31 @@ import { mount } from 'enzyme'; import { keys } from '../../../services'; import { DataGridFocusContext, + notifyCellOfFocusState, createKeyDownHandler, preventTabbing, getParentCellContent, useHeaderFocusWorkaround, } from './focus'; +describe('notifyCellOfFocusState', () => { + const onFocus = jest.fn(); + const cellsUpdateFocus = new Map(); + cellsUpdateFocus.set('0-0', onFocus); + + it("looks through the cellsUpdateFocus map and calls the focused cell's onFocus callback", () => { + notifyCellOfFocusState(cellsUpdateFocus, [0, 0], true); + expect(onFocus).toHaveBeenLastCalledWith(true); + + notifyCellOfFocusState(cellsUpdateFocus, [0, 0], false); + expect(onFocus).toHaveBeenLastCalledWith(false); + }); + + it('does not error if the cell does not exist in the map', () => { + notifyCellOfFocusState(cellsUpdateFocus, [1, 1], true); + }); +}); + describe('createKeyDownHandler', () => { const focusContext = { focusedCell: [0, 0], From 8b0baa342b8dde2110b38ff66d6ef9c96eae2f2d Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 7 Mar 2022 13:54:10 -0800 Subject: [PATCH 5/7] Add unit tests for onFocus + change unmount_enzyme helper to run after each file instead of each test, to allow onFocusUpdate tests to retain shared state between each it() block --- scripts/jest/setup/unmount_enzyme.js | 2 +- src/components/datagrid/utils/focus.test.tsx | 172 +++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) diff --git a/scripts/jest/setup/unmount_enzyme.js b/scripts/jest/setup/unmount_enzyme.js index 3934522214c..c7de11c742f 100644 --- a/scripts/jest/setup/unmount_enzyme.js +++ b/scripts/jest/setup/unmount_enzyme.js @@ -11,7 +11,7 @@ jest.mock('enzyme', () => { }; }); -afterEach(() => { +afterAll(() => { while (mockMountedComponents.length) { const component = mockMountedComponents.pop(); if (component.length === 1) { diff --git a/src/components/datagrid/utils/focus.test.tsx b/src/components/datagrid/utils/focus.test.tsx index a0cbde9f7d6..848a8165f6e 100644 --- a/src/components/datagrid/utils/focus.test.tsx +++ b/src/components/datagrid/utils/focus.test.tsx @@ -7,10 +7,13 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { keys } from '../../../services'; +import { testCustomHook } from '../../../test/test_custom_hook.test_helper'; import { DataGridFocusContext, + useFocus, notifyCellOfFocusState, createKeyDownHandler, preventTabbing, @@ -18,6 +21,175 @@ import { useHeaderFocusWorkaround, } from './focus'; +describe('useFocus', () => { + type ReturnValues = ReturnType; + const mockArgs = { + headerIsInteractive: true, + gridItemsRendered: { current: null }, + }; + + describe('onFocusUpdate', () => { + const onFocus = jest.fn(); + const { + return: { onFocusUpdate, setFocusedCell }, + } = testCustomHook(() => useFocus(mockArgs)); + + let cleanupFn: Function; + + it("adds a cell's onFocus callback to the internal cellsUpdateFocus map,", () => { + cleanupFn = onFocusUpdate([0, 0], onFocus); + // Note: there's no great way to assert this since cellsUpdateFocus is internal, + // so this is a separate test mostly just to document the intention/behavior + }); + + it("calls the cell's onFocus callback with true when the cell becomes focused", () => { + act(() => setFocusedCell([0, 0])); + expect(onFocus).toHaveBeenCalledWith(true); + }); + + it("calls the previous cell's onFocus callback with false when another cell becomes focused", () => { + act(() => setFocusedCell([1, 1])); + expect(onFocus).toHaveBeenCalledWith(false); + }); + + it('removes the cell from the internal cellsUpdateFocus map as a cleanup function', () => { + cleanupFn(); + // Note: there's no great way to assert this since cellsUpdateFocus is internal, + // so this is mostly here to document behavior and for line coverage + }); + }); + + describe('focusedCell / setFocusedCell', () => { + it('gets and sets the focusedCell state', () => { + const { + return: { focusedCell, setFocusedCell }, + getUpdatedState, + } = testCustomHook(() => useFocus(mockArgs)); + expect(focusedCell).toEqual(undefined); + + act(() => setFocusedCell([2, 2])); + expect(getUpdatedState().focusedCell).toEqual([2, 2]); + }); + }); + + describe('focusFirstVisibleInteractiveCell', () => { + describe('when the sticky header is interactive', () => { + it('always focuses the first header cell', () => { + const { + return: { focusFirstVisibleInteractiveCell }, + getUpdatedState, + } = testCustomHook(() => + useFocus({ ...mockArgs, headerIsInteractive: true }) + ); + + act(() => focusFirstVisibleInteractiveCell()); + expect(getUpdatedState().focusedCell).toEqual([0, -1]); + }); + }); + + describe('describe when the header is not interactive', () => { + it('focuses the first visible data cell in the virtualized grid', () => { + const { + return: { focusFirstVisibleInteractiveCell }, + getUpdatedState, + } = testCustomHook(() => + useFocus({ + headerIsInteractive: false, + gridItemsRendered: { + current: { + visibleColumnStartIndex: 1, + visibleRowStartIndex: 10, + } as any, + }, + }) + ); + + act(() => focusFirstVisibleInteractiveCell()); + expect(getUpdatedState().focusedCell).toEqual([1, 10]); + }); + + it("does nothing if the grid isn't yet rendered", () => { + const { + return: { focusFirstVisibleInteractiveCell }, + getUpdatedState, + } = testCustomHook(() => + useFocus({ + headerIsInteractive: false, + gridItemsRendered: { current: null }, + }) + ); + + act(() => focusFirstVisibleInteractiveCell()); + expect(getUpdatedState().focusedCell).toEqual(undefined); + }); + }); + }); + + describe('setIsFocusedCellInView / focusProps', () => { + describe('when no focused child cell is in view', () => { + it('renders the grid with tabindex 0 and an onFocus event', () => { + const { + return: { focusProps }, + } = testCustomHook(() => useFocus(mockArgs)); + + expect(focusProps).toEqual({ + tabIndex: 0, + onFocus: expect.any(Function), + }); + }); + + describe('onFocus event', () => { + const mockGrid = document.createElement('div'); + const someChild = mockGrid.appendChild( + document.createElement('button') + ); + + it('focuses into the first visible cell of the grid when the grid is directly tabbed to', () => { + const { + return: { + focusProps: { onFocus }, + }, + getUpdatedState, + } = testCustomHook(() => useFocus(mockArgs)); + + act(() => + onFocus!({ target: mockGrid, currentTarget: mockGrid } as any) + ); + expect(getUpdatedState().focusedCell).toEqual([0, -1]); + }); + + it('does nothing if the focus event was not on the grid itself', () => { + const { + return: { + focusProps: { onFocus }, + }, + getUpdatedState, + } = testCustomHook(() => useFocus(mockArgs)); + + act(() => + onFocus!({ target: someChild, currentTarget: mockGrid } as any) + ); + expect(getUpdatedState().focusedCell).toEqual(undefined); + }); + }); + }); + + describe('when a focused cell is in view', () => { + it('renders the grid with tabindex -1 (because the child cell will already have a tabindex 0)', () => { + const { + return: { setIsFocusedCellInView }, + getUpdatedState, + } = testCustomHook(() => useFocus(mockArgs)); + + act(() => setIsFocusedCellInView(true)); + expect(getUpdatedState().focusProps).toEqual({ + tabIndex: -1, + }); + }); + }); + }); +}); + describe('notifyCellOfFocusState', () => { const onFocus = jest.fn(); const cellsUpdateFocus = new Map(); From 7bd108ea82112240ae37be86890073a74e683667 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Thu, 10 Mar 2022 15:04:17 -0800 Subject: [PATCH 6/7] Fix failing tests caused by unmount_enzyme change --- .../body/header/data_grid_header_cell_wrapper.test.tsx | 3 +++ src/components/image/image.test.tsx | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 be2d9e31e33..bb81d92d2c3 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 @@ -110,6 +110,9 @@ describe('EuiDataGridHeaderCellWrapper', () => { expect(headerCell.getAttribute('tabIndex')).toEqual('-1'); }; + // Reset focus between between tests + beforeEach(() => (document.activeElement as HTMLElement)?.blur()); + describe('isFocused context', () => { describe('when true', () => { it('focuses the interactive cell children when present', () => { diff --git a/src/components/image/image.test.tsx b/src/components/image/image.test.tsx index 38f1cd3f551..c191006954a 100644 --- a/src/components/image/image.test.tsx +++ b/src/components/image/image.test.tsx @@ -76,7 +76,7 @@ describe('EuiImage', () => { describe('Fullscreen behaviour', () => { let component: ReactWrapper; - beforeEach(() => { + beforeAll(() => { const testProps = { ...requiredProps, 'data-test-subj': 'euiImage', @@ -91,7 +91,9 @@ describe('EuiImage', () => { {...testProps} /> ); + }); + beforeEach(() => { findTestSubject(component, 'activateFullScreenButton').simulate('click'); }); From a6cce1be741919fa117c01b580c339678368c55a Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 14 Mar 2022 12:38:48 -0700 Subject: [PATCH 7/7] Fix comment typo --- .../datagrid/body/header/data_grid_header_cell_wrapper.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bb81d92d2c3..58f95c04e41 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 @@ -110,7 +110,7 @@ describe('EuiDataGridHeaderCellWrapper', () => { expect(headerCell.getAttribute('tabIndex')).toEqual('-1'); }; - // Reset focus between between tests + // Reset focus between tests beforeEach(() => (document.activeElement as HTMLElement)?.blur()); describe('isFocused context', () => {