Skip to content

Commit

Permalink
[#5517] Add onItemsRendered ref to store currently visible/virtuali…
Browse files Browse the repository at this point in the history
…zed refs

- see https://react-window.vercel.app/#/api/FixedSizeGrid for `onItemsRendered` API
- this struck me as the quickest/easiest way to determine which virtualized cells are in view
- I opted to save this as a ref instead of as state as I didn't see the need to cause a rerender

- `focusFirstVisibleInteractiveCell` was split out to its own fn as it will shortly be used elsewhere to fix another focus bug in the grid
  • Loading branch information
cee-chen committed Jan 11, 2022
1 parent 7f031ac commit 2f44ab9
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/components/datagrid/body/data_grid_body.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('EuiDataGridBody', () => {
resetAfterRowIndex: jest.fn(),
} as any,
},
gridItemsRendered: {} as any,
wrapperRef: { current: document.createElement('div') },
};

Expand Down
4 changes: 4 additions & 0 deletions src/components/datagrid/body/data_grid_body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
gridStyles,
gridWidth,
gridRef,
gridItemsRendered,
wrapperRef,
} = props;

Expand Down Expand Up @@ -442,6 +443,9 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
<Grid
{...(virtualizationOptions ? virtualizationOptions : {})}
ref={gridRef}
onItemsRendered={(itemsRendered) => {
gridItemsRendered.current = itemsRendered;
}}
innerElementType={InnerElement}
outerRef={outerGridRef}
innerRef={innerGridRef}
Expand Down
14 changes: 10 additions & 4 deletions src/components/datagrid/data_grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import React, {
useRef,
useState,
} from 'react';
import { VariableSizeGrid as Grid } from 'react-window';
import {
VariableSizeGrid as Grid,
GridOnItemsRenderedProps,
} from 'react-window';
import { useGeneratedHtmlId, keys } from '../../services';
import { EuiFocusTrap } from '../focus_trap';
import { EuiI18n, useEuiI18n } from '../i18n';
Expand Down Expand Up @@ -169,6 +172,7 @@ export const EuiDataGrid: FunctionComponent<EuiDataGridProps> = (props) => {
// Imperative handler passed back by react-window - we're setting this at
// the top datagrid level to make passing it to other children & utils easier
const gridRef = useRef<Grid | null>(null);
const gridItemsRendered = useRef<GridOnItemsRenderedProps | null>(null);

/**
* Display
Expand Down Expand Up @@ -254,9 +258,10 @@ export const EuiDataGrid: FunctionComponent<EuiDataGridProps> = (props) => {
const { headerIsInteractive, handleHeaderMutation } = useHeaderIsInteractive(
contentRef.current
);
const { focusProps: wrappingDivFocusProps, ...focusContext } = useFocus(
headerIsInteractive
);
const { focusProps: wrappingDivFocusProps, ...focusContext } = useFocus({
headerIsInteractive,
gridItemsRendered,
});

/**
* Toolbar & full-screen
Expand Down Expand Up @@ -437,6 +442,7 @@ export const EuiDataGrid: FunctionComponent<EuiDataGridProps> = (props) => {
gridStyles={gridStyles}
gridWidth={gridWidth}
gridRef={gridRef}
gridItemsRendered={gridItemsRendered}
wrapperRef={contentRef}
/>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/components/datagrid/data_grid_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
SetStateAction,
MutableRefObject,
} from 'react';
import { VariableSizeGridProps, VariableSizeGrid as Grid } from 'react-window';
import {
VariableSizeGridProps,
VariableSizeGrid as Grid,
GridOnItemsRenderedProps,
} from 'react-window';
import { EuiListGroupItemProps } from '../list_group';
import { EuiButtonEmpty, EuiButtonIcon } from '../button';
import { ExclusiveUnion, CommonProps, OneOf } from '../common';
Expand Down Expand Up @@ -347,6 +351,7 @@ export interface EuiDataGridBodyProps {
gridStyles: EuiDataGridStyle;
gridWidth: number;
gridRef: MutableRefObject<Grid | null>;
gridItemsRendered: MutableRefObject<GridOnItemsRenderedProps | null>;
wrapperRef: MutableRefObject<HTMLDivElement | null>;
}
export interface EuiDataGridCellValueElementProps {
Expand Down
31 changes: 25 additions & 6 deletions src/components/datagrid/utils/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
useState,
HTMLAttributes,
KeyboardEvent,
MutableRefObject,
} from 'react';
import { GridOnItemsRenderedProps } from 'react-window';
import tabbable from 'tabbable';
import { keys } from '../../../services';
import {
Expand All @@ -37,9 +39,13 @@ type FocusProps = Pick<HTMLAttributes<HTMLDivElement>, 'tabIndex' | 'onFocus'>;
/**
* Main focus context and overarching focus state management
*/
export const useFocus = (
headerIsInteractive: boolean
): DataGridFocusContextShape & { focusProps: FocusProps } => {
export const useFocus = ({
headerIsInteractive,
gridItemsRendered,
}: {
headerIsInteractive: boolean;
gridItemsRendered: MutableRefObject<GridOnItemsRenderedProps | null>;
}): DataGridFocusContextShape & { focusProps: FocusProps } => {
// Maintain a map of focus cell state callbacks
const cellsUpdateFocus = useRef<Map<string, Function>>(new Map());

Expand Down Expand Up @@ -81,6 +87,19 @@ export const useFocus = (
}
}, [cellsUpdateFocus, focusedCell]);

const focusFirstVisibleInteractiveCell = useCallback(() => {
const {
visibleColumnStartIndex,
visibleRowStartIndex,
} = gridItemsRendered.current!;

setFocusedCell(
headerIsInteractive
? [0, -1]
: [visibleColumnStartIndex, visibleRowStartIndex]
);
}, [setFocusedCell, headerIsInteractive, gridItemsRendered]);

const focusProps = useMemo<FocusProps>(
() =>
isFocusedCellInView
Expand All @@ -94,13 +113,13 @@ export const useFocus = (
// if e.target (the source element of the `focus event`
// matches e.currentTarget (always the div with this onFocus listener)
// then the user has focused directly on the data grid wrapper (almost definitely by tabbing)
// so shift focus to the first interactive cell within the grid
// so shift focus to the first visible and interactive cell within the grid
if (e.target === e.currentTarget) {
setFocusedCell(headerIsInteractive ? [0, -1] : [0, 0]);
focusFirstVisibleInteractiveCell();
}
},
},
[isFocusedCellInView, setFocusedCell, headerIsInteractive]
[isFocusedCellInView, focusFirstVisibleInteractiveCell]
);

return {
Expand Down

0 comments on commit 2f44ab9

Please sign in to comment.