From 59461c7e068bbd6b61fa85d8479516fbd9070a82 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 11 Jan 2022 13:30:32 -0800 Subject: [PATCH] [EuiDataGrid] Set up `ref` that exposes focus/popover internal APIs (#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 * [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 --- CHANGELOG.md | 2 + src-docs/src/routes.js | 2 + .../src/views/datagrid/datagrid_example.js | 17 + .../views/datagrid/datagrid_ref_example.js | 67 ++ src-docs/src/views/datagrid/ref.js | 196 +++++ .../datagrid/body/data_grid_cell.tsx | 4 +- .../body/data_grid_cell_buttons.test.tsx | 2 + .../datagrid/body/data_grid_cell_buttons.tsx | 5 +- .../body/data_grid_cell_popover.test.tsx | 3 +- .../datagrid/body/data_grid_cell_popover.tsx | 2 + src/components/datagrid/data_grid.test.tsx | 27 +- src/components/datagrid/data_grid.tsx | 746 +++++++++--------- src/components/datagrid/data_grid_types.ts | 18 + 13 files changed, 725 insertions(+), 366 deletions(-) create mode 100644 src-docs/src/views/datagrid/datagrid_ref_example.js create mode 100644 src-docs/src/views/datagrid/ref.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6728405d0..4b13ae8a11c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 11d3ff15f0a..23c7d8366e5 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -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'; @@ -489,6 +490,7 @@ const navigation = [ DataGridFooterRowExample, DataGridVirtualizationExample, DataGridRowHeightOptionsExample, + DataGridRefExample, TableExample, TableInMemoryExample, ].map((example) => createExample(example)), diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index 47a606c471c..3ecd3c20e5b 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -33,6 +33,7 @@ import { EuiDataGridRowHeightsOptions, EuiDataGridCellValueElementProps, EuiDataGridSchemaDetector, + EuiDataGridRefProps, } from '!!prop-loader!../../../../src/components/datagrid/data_grid_types'; const gridSnippet = ` @@ -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} /> `; @@ -323,6 +326,19 @@ const gridConcepts = [ ), }, + { + title: 'ref', + description: ( + + Passes back an object of internal EuiDataGridRefProps{' '} + methods for advanced control of data grid popover/focus state. See{' '} + + Data grid ref methods + {' '} + for more details and examples. + + ), + }, ]; export const DataGridExample = { @@ -414,6 +430,7 @@ export const DataGridExample = { EuiDataGridToolBarAdditionalControlsLeftOptions, EuiDataGridPopoverContentProps, EuiDataGridRowHeightsOptions, + EuiDataGridRefProps, }, demo: ( diff --git a/src-docs/src/views/datagrid/datagrid_ref_example.js b/src-docs/src/views/datagrid/datagrid_ref_example.js new file mode 100644 index 00000000000..9345f4d75c0 --- /dev/null +++ b/src-docs/src/views/datagrid/datagrid_ref_example.js @@ -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(); + + +// 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: ( + <> +

+ 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 ref prop of EuiDataGrid. These methods are: +

+
    +
  • + setFocusedCell({'{ rowIndex, colIndex }'}) - + focuses the specified cell in the grid. + + + Your modal or flyout should restore focus into the grid on close + to prevent keyboard or screen reader users from being stranded. + +
  • +
+ {dataGridRefSnippet} +

+ The below example shows how to use the internal APIs for a data grid + that opens a modal via cell actions. +

+ + ), + components: { DataGridRef }, + demo: , + snippet: dataGridRefSnippet, + props: { EuiDataGridRefProps }, + }, + ], +}; diff --git a/src-docs/src/views/datagrid/ref.js b/src-docs/src/views/datagrid/ref.js new file mode 100644 index 00000000000..e043abcedc9 --- /dev/null +++ b/src-docs/src/views/datagrid/ref.js @@ -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 ( + showModal({ rowIndex, colIndex })} + iconType="faceHappy" + aria-label="Open modal" + > + Open modal + + ); + }, + [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 ( + <> + + + + setRowIndexAction(Number(e.target.value))} + compressed + /> + + + + + setColIndexAction(Number(e.target.value))} + compressed + /> + + + + + dataGridRef.current.setFocusedCell({ + rowIndex: rowIndexAction, + colIndex: colIndexAction, + }) + } + > + Set cell focus + + + + + + + raw_data[rowIndex][columnId] + } + pagination={{ + ...pagination, + pageSizeOptions: [25], + onChangePage: onChangePage, + }} + height={400} + ref={dataGridRef} + /> + {isModalVisible && ( + + + +

Example modal

+
+
+ + + +

+ When closed, this modal should re-focus into the cell that + toggled it. +

+
+
+ + + + Close + + +
+ )} + + ); +}; diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index 26ab518a0a4..60e78a26479 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -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 || @@ -546,6 +546,7 @@ export class EuiDataGridCell extends Component< (this.popoverPanelRef.current = ref)} popoverIsOpen={this.state.popoverIsOpen} rowIndex={rowIndex} + colIndex={colIndex} renderCellValue={rest.renderCellValue} popoverContent={PopoverContent} /> diff --git a/src/components/datagrid/body/data_grid_cell_buttons.test.tsx b/src/components/datagrid/body/data_grid_cell_buttons.test.tsx index 5f6de44b079..ed26a36cb56 100644 --- a/src/components/datagrid/body/data_grid_cell_buttons.test.tsx +++ b/src/components/datagrid/body/data_grid_cell_buttons.test.tsx @@ -17,6 +17,7 @@ describe('EuiDataGridCellButtons', () => { closePopover: jest.fn(), onExpandClick: jest.fn(), rowIndex: 0, + colIndex: 0, }; it('renders an expand button', () => { @@ -66,6 +67,7 @@ describe('EuiDataGridCellButtons', () => { void; onExpandClick: () => void; column?: EuiDataGridColumn; rowIndex: number; + colIndex: number; }) => { const buttonIconClasses = classNames('euiDataGridRowCell__expandButtonIcon', { 'euiDataGridRowCell__expandButtonIcon-isActive': popoverIsOpen, @@ -74,6 +76,7 @@ export const EuiDataGridCellButtons = ({ {[...additionalButtons, expandButton]} diff --git a/src/components/datagrid/body/data_grid_cell_popover.test.tsx b/src/components/datagrid/body/data_grid_cell_popover.test.tsx index 8402eff4c01..0e87a303c8e 100644 --- a/src/components/datagrid/body/data_grid_cell_popover.test.tsx +++ b/src/components/datagrid/body/data_grid_cell_popover.test.tsx @@ -14,8 +14,8 @@ import { EuiDataGridCellPopover } from './data_grid_cell_popover'; describe('EuiDataGridCellPopover', () => { const requiredProps = { - // column, rowIndex: 0, + colIndex: 0, cellContentProps: { rowIndex: 0, columnId: 'someId', @@ -123,6 +123,7 @@ describe('EuiDataGridCellPopover', () => { ( diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 4b2698d7b70..a4200aab0ad 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, createRef } from 'react'; import { mount, ReactWrapper, render } from 'enzyme'; -import { EuiDataGrid, EuiDataGridProps } from './'; +import { EuiDataGrid } from './'; +import { EuiDataGridProps, EuiDataGridRefProps } from './data_grid_types'; import { findTestSubject, requiredProps, @@ -2724,4 +2725,26 @@ describe('EuiDataGrid', () => { expect(takeMountedSnapshot(component)).toMatchSnapshot(); }); }); + + it('returns a ref which exposes internal imperative APIs', () => { + const gridRef = createRef(); + + mount( + {}, + }} + rowCount={1} + renderCellValue={() => 'value'} + ref={gridRef} + /> + ); + + expect(gridRef.current).toEqual({ + setFocusedCell: expect.any(Function), + }); + }); }); diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 46096ad9aaa..f5688219b83 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -8,11 +8,12 @@ import classNames from 'classnames'; import React, { - FunctionComponent, + forwardRef, KeyboardEvent, useMemo, useRef, useState, + useImperativeHandle, } from 'react'; import { VariableSizeGrid as Grid } from 'react-window'; import { useGeneratedHtmlId, keys } from '../../services'; @@ -51,6 +52,7 @@ import { import { EuiDataGridColumn, EuiDataGridProps, + EuiDataGridRefProps, EuiDataGridStyleBorders, EuiDataGridStyleCellPaddings, EuiDataGridStyleFontSizes, @@ -98,372 +100,394 @@ const cellPaddingsToClassMap: { l: 'euiDataGrid--paddingLarge', }; -export const EuiDataGrid: FunctionComponent = (props) => { - const { - leadingControlColumns = [], - trailingControlColumns = [], - columns, - columnVisibility, - schemaDetectors, - rowCount, - renderCellValue, - renderFooterCellValue, - className, - gridStyle, - toolbarVisibility = true, - pagination, - sorting, - inMemory, - popoverContents, - onColumnResize, - minSizeForControls, - height, - width, - rowHeightsOptions: _rowHeightsOptions, - virtualizationOptions, - ...rest - } = props; - - /** - * Merge consumer settings with defaults - */ - const gridStyleWithDefaults = { ...startingStyles, ...gridStyle }; - - const mergedPopoverContents = useMemo( - () => ({ - ...providedPopoverContents, - ...popoverContents, - }), - [popoverContents] - ); - - const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory, rowCount); - - const allSchemaDetectors = useMemo( - () => [...providedSchemaDetectors, ...(schemaDetectors || [])], - [schemaDetectors] - ); - - const mergedSchema = useMergedSchema({ - columns, - inMemory, - inMemoryValues, - schemaDetectors: allSchemaDetectors, - autoDetectSchema: inMemory != null, - }); - - /** - * Grid refs & observers - */ - // Outermost wrapper div - const resizeRef = useRef(null); - const { width: gridWidth } = useResizeObserver(resizeRef.current, 'width'); - - // Wrapper div around EuiDataGridBody - const contentRef = useRef(null); - useMutationObserver(contentRef.current, preventTabbing, { - subtree: true, - childList: true, - }); - - // 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(null); - - /** - * Display - */ - const displayValues: { [key: string]: string } = useMemo(() => { - return columns.reduce( - (acc: { [key: string]: string }, column: EuiDataGridColumn) => ({ - ...acc, - [column.id]: column.displayAsText || column.id, +export const EuiDataGrid = forwardRef( + (props, ref) => { + const { + leadingControlColumns = [], + trailingControlColumns = [], + columns, + columnVisibility, + schemaDetectors, + rowCount, + renderCellValue, + renderFooterCellValue, + className, + gridStyle, + toolbarVisibility = true, + pagination, + sorting, + inMemory, + popoverContents, + onColumnResize, + minSizeForControls, + height, + width, + rowHeightsOptions: _rowHeightsOptions, + virtualizationOptions, + ...rest + } = props; + + /** + * Merge consumer settings with defaults + */ + const gridStyleWithDefaults = { ...startingStyles, ...gridStyle }; + + const mergedPopoverContents = useMemo( + () => ({ + ...providedPopoverContents, + ...popoverContents, }), - {} + [popoverContents] ); - }, [columns]); - - const [ - displaySelector, - gridStyles, - rowHeightsOptions, - ] = useDataGridDisplaySelector( - checkOrDefaultToolBarDisplayOptions( - toolbarVisibility, - 'showDisplaySelector' - ), - gridStyleWithDefaults, - _rowHeightsOptions - ); - - /** - * Column order & visibility - */ - const [ - columnSelector, - orderedVisibleColumns, - setVisibleColumns, - switchColumnPos, - ] = useDataGridColumnSelector( - columns, - columnVisibility, - checkOrDefaultToolBarDisplayOptions( - toolbarVisibility, - 'showColumnSelector' - ), - displayValues - ); - - const visibleColCount = useMemo(() => { - return ( - orderedVisibleColumns.length + - leadingControlColumns.length + - trailingControlColumns.length + + const [inMemoryValues, onCellRender] = useInMemoryValues( + inMemory, + rowCount ); - }, [orderedVisibleColumns, leadingControlColumns, trailingControlColumns]); - - const visibleRows = useMemo( - () => computeVisibleRows({ pagination, rowCount }), - [pagination, rowCount] - ); - const { visibleRowCount } = visibleRows; - - /** - * Sorting - */ - const columnSorting = useDataGridColumnSorting( - orderedVisibleColumns, - sorting, - mergedSchema, - allSchemaDetectors, - displayValues - ); - - const sortingContext = useSorting({ - sorting, - inMemory, - inMemoryValues, - schema: mergedSchema, - schemaDetectors: allSchemaDetectors, - startRow: visibleRows.startRow, - }); - - /** - * Focus - */ - const { headerIsInteractive, handleHeaderMutation } = useHeaderIsInteractive( - contentRef.current - ); - const { focusProps: wrappingDivFocusProps, ...focusContext } = useFocus( - headerIsInteractive - ); - - /** - * Toolbar & full-screen - */ - const showToolbar = !!toolbarVisibility; - const [toolbarRef, setToolbarRef] = useState(null); - const { height: toolbarHeight } = useResizeObserver(toolbarRef, 'height'); - - const [isFullScreen, setIsFullScreen] = useState(false); - const handleGridKeyDown = (event: KeyboardEvent) => { - switch (event.key) { - case keys.ESCAPE: - if (isFullScreen) { - event.preventDefault(); - setIsFullScreen(false); - } - break; - } - }; - - /** - * Classes - */ - const classes = classNames( - 'euiDataGrid', - fontSizesToClassMap[gridStyles.fontSize!], - bordersToClassMap[gridStyles.border!], - headerToClassMap[gridStyles.header!], - footerToClassMap[gridStyles.footer!], - rowHoverToClassMap[gridStyles.rowHover!], - cellPaddingsToClassMap[gridStyles.cellPadding!], - { - 'euiDataGrid--stripes': gridStyles.stripes!, - }, - { - 'euiDataGrid--stickyFooter': gridStyles.footer && gridStyles.stickyFooter, - }, - { - 'euiDataGrid--fullScreen': isFullScreen, - }, - { - 'euiDataGrid--noControls': !toolbarVisibility, - }, - className - ); - - const controlBtnClasses = classNames('euiDataGrid__controlBtn', { - 'euiDataGrid__controlBtn--active': isFullScreen, - }); - - /** - * Accessibility - */ - const gridId = useGeneratedHtmlId(); - const interactiveCellId = useGeneratedHtmlId(); - const ariaLabelledById = useGeneratedHtmlId(); - - const ariaLabel = useEuiI18n( - 'euiDataGrid.ariaLabel', - '{label}; Page {page} of {pageCount}.', - { - label: rest['aria-label'], - page: pagination ? pagination.pageIndex + 1 : 0, - pageCount: pagination ? Math.ceil(rowCount / pagination.pageSize) : 0, + + const allSchemaDetectors = useMemo( + () => [...providedSchemaDetectors, ...(schemaDetectors || [])], + [schemaDetectors] + ); + + const mergedSchema = useMergedSchema({ + columns, + inMemory, + inMemoryValues, + schemaDetectors: allSchemaDetectors, + autoDetectSchema: inMemory != null, + }); + + /** + * Grid refs & observers + */ + // Outermost wrapper div + const resizeRef = useRef(null); + const { width: gridWidth } = useResizeObserver(resizeRef.current, 'width'); + + // Wrapper div around EuiDataGridBody + const contentRef = useRef(null); + useMutationObserver(contentRef.current, preventTabbing, { + subtree: true, + childList: true, + }); + + // 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(null); + + /** + * Display + */ + const displayValues: { [key: string]: string } = useMemo(() => { + return columns.reduce( + (acc: { [key: string]: string }, column: EuiDataGridColumn) => ({ + ...acc, + [column.id]: column.displayAsText || column.id, + }), + {} + ); + }, [columns]); + + const [ + displaySelector, + gridStyles, + rowHeightsOptions, + ] = useDataGridDisplaySelector( + checkOrDefaultToolBarDisplayOptions( + toolbarVisibility, + 'showDisplaySelector' + ), + gridStyleWithDefaults, + _rowHeightsOptions + ); + + /** + * Column order & visibility + */ + const [ + columnSelector, + orderedVisibleColumns, + setVisibleColumns, + switchColumnPos, + ] = useDataGridColumnSelector( + columns, + columnVisibility, + checkOrDefaultToolBarDisplayOptions( + toolbarVisibility, + 'showColumnSelector' + ), + displayValues + ); + + const visibleColCount = useMemo(() => { + return ( + orderedVisibleColumns.length + + leadingControlColumns.length + + trailingControlColumns.length + ); + }, [orderedVisibleColumns, leadingControlColumns, trailingControlColumns]); + + const visibleRows = useMemo( + () => computeVisibleRows({ pagination, rowCount }), + [pagination, rowCount] + ); + const { visibleRowCount } = visibleRows; + + /** + * Sorting + */ + const columnSorting = useDataGridColumnSorting( + orderedVisibleColumns, + sorting, + mergedSchema, + allSchemaDetectors, + displayValues + ); + + const sortingContext = useSorting({ + sorting, + inMemory, + inMemoryValues, + schema: mergedSchema, + schemaDetectors: allSchemaDetectors, + startRow: visibleRows.startRow, + }); + + /** + * Focus + */ + const { + headerIsInteractive, + handleHeaderMutation, + } = useHeaderIsInteractive(contentRef.current); + const { focusProps: wrappingDivFocusProps, ...focusContext } = useFocus( + headerIsInteractive + ); + + /** + * Toolbar & full-screen + */ + const showToolbar = !!toolbarVisibility; + const [toolbarRef, setToolbarRef] = useState(null); + const { height: toolbarHeight } = useResizeObserver(toolbarRef, 'height'); + + const [isFullScreen, setIsFullScreen] = useState(false); + const handleGridKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case keys.ESCAPE: + if (isFullScreen) { + event.preventDefault(); + setIsFullScreen(false); + } + break; + } + }; + + /** + * Expose internal APIs as ref to consumer + */ + useImperativeHandle( + ref, + () => ({ + setFocusedCell: ({ rowIndex, colIndex }) => { + focusContext.setFocusedCell([colIndex, rowIndex]); + }, + }), + [focusContext] + ); + + /** + * Classes + */ + const classes = classNames( + 'euiDataGrid', + fontSizesToClassMap[gridStyles.fontSize!], + bordersToClassMap[gridStyles.border!], + headerToClassMap[gridStyles.header!], + footerToClassMap[gridStyles.footer!], + rowHoverToClassMap[gridStyles.rowHover!], + cellPaddingsToClassMap[gridStyles.cellPadding!], + { + 'euiDataGrid--stripes': gridStyles.stripes!, + }, + { + 'euiDataGrid--stickyFooter': + gridStyles.footer && gridStyles.stickyFooter, + }, + { + 'euiDataGrid--fullScreen': isFullScreen, + }, + { + 'euiDataGrid--noControls': !toolbarVisibility, + }, + className + ); + + const controlBtnClasses = classNames('euiDataGrid__controlBtn', { + 'euiDataGrid__controlBtn--active': isFullScreen, + }); + + /** + * Accessibility + */ + const gridId = useGeneratedHtmlId(); + const interactiveCellId = useGeneratedHtmlId(); + const ariaLabelledById = useGeneratedHtmlId(); + + const ariaLabel = useEuiI18n( + 'euiDataGrid.ariaLabel', + '{label}; Page {page} of {pageCount}.', + { + label: rest['aria-label'], + page: pagination ? pagination.pageIndex + 1 : 0, + pageCount: pagination ? Math.ceil(rowCount / pagination.pageSize) : 0, + } + ); + + const ariaLabelledBy = useEuiI18n( + 'euiDataGrid.ariaLabelledBy', + 'Page {page} of {pageCount}.', + { + page: pagination ? pagination.pageIndex + 1 : 0, + pageCount: pagination ? Math.ceil(rowCount / pagination.pageSize) : 0, + } + ); + + // extract aria-label and/or aria-labelledby from `rest` + const gridAriaProps: { + 'aria-label'?: string; + 'aria-labelledby'?: string; + } = {}; + if ('aria-label' in rest) { + gridAriaProps['aria-label'] = pagination ? ariaLabel : rest['aria-label']; + delete rest['aria-label']; } - ); - - const ariaLabelledBy = useEuiI18n( - 'euiDataGrid.ariaLabelledBy', - 'Page {page} of {pageCount}.', - { - page: pagination ? pagination.pageIndex + 1 : 0, - pageCount: pagination ? Math.ceil(rowCount / pagination.pageSize) : 0, + if ('aria-labelledby' in rest) { + gridAriaProps['aria-labelledby'] = `${rest['aria-labelledby']} ${ + pagination ? ariaLabelledById : '' + }`; + delete rest['aria-labelledby']; } - ); - - // extract aria-label and/or aria-labelledby from `rest` - const gridAriaProps: { - 'aria-label'?: string; - 'aria-labelledby'?: string; - } = {}; - if ('aria-label' in rest) { - gridAriaProps['aria-label'] = pagination ? ariaLabel : rest['aria-label']; - delete rest['aria-label']; - } - if ('aria-labelledby' in rest) { - gridAriaProps['aria-labelledby'] = `${rest['aria-labelledby']} ${ - pagination ? ariaLabelledById : '' - }`; - delete rest['aria-labelledby']; - } - return ( - - - -
+ + - {showToolbar && ( - - )} - {inMemory ? ( - - ) : null} -
- -
- {pagination && props['aria-labelledby'] && ( -
+ +
+ {pagination && props['aria-labelledby'] && ( + + )} + {pagination && ( + + )} + - )} - {pagination && ( - - )} - -
-
-
-
- ); -}; + + + + + ); + } +); + +EuiDataGrid.displayName = 'EuiDataGrid'; diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 4e06beba1c0..ba23040dba9 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -283,6 +283,19 @@ export type EuiDataGridProps = OneOf< 'aria-label' | 'aria-labelledby' >; +export interface EuiDataGridRefProps { + /** + * Allows manually focusing the specified cell in the grid. + * + * Using this method is an accessibility requirement if your EuiDataGrid + * toggles a modal or flyout - focus must be restored to the grid on close + * to prevent keyboard or screen reader users from being stranded. + */ + setFocusedCell(targetCell: EuiDataGridCellLocation): void; +} + +export type EuiDataGridCellLocation = { rowIndex: number; colIndex: number }; + export interface EuiDataGridColumnResizerProps { columnId: string; columnWidth: number; @@ -307,6 +320,7 @@ export interface EuiDataGridCellPopoverProps { | JSXElementConstructor | ((props: EuiDataGridCellValueElementProps) => ReactNode); rowIndex: number; + colIndex: number; } export interface EuiDataGridColumnSortingDraggableProps { id: string; @@ -516,6 +530,10 @@ export interface EuiDataGridColumnCellActionProps { * The index of the row that contains cell's data */ rowIndex: number; + /** + * The index of the column that contains cell's data + */ + colIndex: number; /** * The id of the column that contains the cell's data */