diff --git a/package.json b/package.json
index b834384fe05d..67b92e8f3aab 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
"react-is": "~16.3.0",
"react-virtualized": "^9.18.5",
"resize-observer-polyfill": "^1.5.0",
- "tabbable": "^1.1.0",
+ "tabbable": "^4.0.0",
"uuid": "^3.1.0"
},
"devDependencies": {
@@ -85,6 +85,7 @@
"@types/react-is": "~16.3.0",
"@types/react-virtualized": "^9.18.6",
"@types/resize-observer-browser": "^0.1.1",
+ "@types/tabbable": "^3.1.0",
"@types/uuid": "^3.4.4",
"@typescript-eslint/eslint-plugin": "^1.9.0",
"@typescript-eslint/parser": "^1.9.0",
diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss
index b934e55fddad..df2345b81f4e 100644
--- a/src-docs/src/components/guide_components.scss
+++ b/src-docs/src/components/guide_components.scss
@@ -17,6 +17,8 @@ $guideZLevelHighest: $euiZLevel9 + 1000;
top: 0;
bottom: 0;
+ display: none;
+
.guideSideNav__identity {
border-bottom: $euiBorderThin;
padding: $euiSize;
@@ -50,7 +52,7 @@ $guideZLevelHighest: $euiZLevel9 + 1000;
background-color: $euiColorEmptyShade;
border-left: $euiBorderThin;
max-width: 1000px;
- margin-left: 240px;
+ // margin-left: 240px;
}
.guideDemo__highlightLayout {
diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js
index a6594475771a..026a83078b77 100644
--- a/src-docs/src/views/datagrid/datagrid.js
+++ b/src-docs/src/views/datagrid/datagrid.js
@@ -7,7 +7,10 @@ import {
EuiFormRow,
EuiPopover,
EuiButton,
+ EuiButtonIcon,
+ EuiLink,
} from '../../../../src/components/';
+import { iconTypes } from '../../../../src-docs/src/views/icon/icons';
const columns = [
{
@@ -22,6 +25,12 @@ const columns = [
{
id: 'contributions',
},
+ {
+ id: 'actions',
+ },
+ {
+ id: 'a bug',
+ },
];
const data = [
@@ -304,6 +313,13 @@ export default class DataGrid extends Component {
pagination: { ...pagination, pageSize },
}));
+ dummyIcon = () => (
+
+ );
+
render() {
const { pagination } = this.state;
@@ -396,7 +412,36 @@ export default class DataGrid extends Component {
rowHover: this.state.rowHoverSelected,
header: this.state.headerSelected,
}}
- renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]}
+ renderCellValue={({ rowIndex, columnId }) => {
+ const value = data[rowIndex][columnId];
+
+ if (columnId === 'actions') {
+ return (
+ <>
+ {this.dummyIcon()}
+ {this.dummyIcon()}
+ >
+ );
+ }
+
+ if (columnId === 'url') {
+ return {value};
+ }
+
+ if (columnId === 'avatar_url') {
+ return (
+ <>
+ Avatar: {value}
+ >
+ );
+ }
+
+ if (columnId === 'a bug') {
+ return
check it: {this.dummyIcon()}
;
+ }
+
+ return value;
+ }}
pagination={{
...pagination,
pageSizeOptions: [5, 10, 25],
diff --git a/src-docs/src/views/icon/icons.js b/src-docs/src/views/icon/icons.js
index 79dd11541096..eb22dc918429 100644
--- a/src-docs/src/views/icon/icons.js
+++ b/src-docs/src/views/icon/icons.js
@@ -20,7 +20,7 @@ import {
EuiCopy,
} from '../../../../src/components';
-const iconTypes = [
+export const iconTypes = [
'alert',
'apmTrace',
'apps',
diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx
index d0acbc557e32..cfac180c577d 100644
--- a/src/components/datagrid/data_grid.tsx
+++ b/src/components/datagrid/data_grid.tsx
@@ -23,6 +23,7 @@ import { EuiDataGridBody } from './data_grid_body';
import { useColumnSelector } from './column_selector';
// @ts-ignore-next-line
import { EuiTablePagination } from '../table/table_pagination';
+import { getTabbables, CELL_CONTENTS_ATTR } from './utils';
// Types for styling options, passed down through the `gridStyle` prop
type EuiDataGridStyleFontSizes = 's' | 'm' | 'l';
@@ -138,13 +139,11 @@ export const EuiDataGrid: FunctionComponent = props => {
useEffect(() => {
if (gridRef.current != null) {
- const gridWidth = Math.max(
- gridRef.current!.clientWidth / props.columns.length,
- 100
- );
+ const gridWidth = gridRef.current.clientWidth;
+ const columnWidth = Math.max(gridWidth / props.columns.length, 100);
const columnWidths = props.columns.reduce(
(columnWidths: EuiDataGridColumnWidths, column) => {
- columnWidths[column.id] = gridWidth;
+ columnWidths[column.id] = columnWidth;
return columnWidths;
},
{}
@@ -154,44 +153,71 @@ export const EuiDataGrid: FunctionComponent = props => {
}, []);
const [focusedCell, setFocusedCell] = useState<[number, number]>(ORIGIN);
- const onCellFocus = useCallback(
- (x: number, y: number) => {
- setFocusedCell([x, y]);
- },
- [setFocusedCell]
- );
+ const [isGridNavigationEnabled, setIsGridNavigationEnabled] = useState<
+ boolean
+ >(true);
+
+ const isInteractiveCell = (element: HTMLElement) => {
+ if (element.getAttribute('role') !== 'gridcell') {
+ return false;
+ }
+
+ const cellContents = element.querySelector(`[${CELL_CONTENTS_ATTR}]`)!;
+ const tabbables = getTabbables(cellContents);
+ const nodeCount = cellContents.childNodes.length;
+
+ // TODO fix the bug column (should check if when removing all tabbables from cell if anything is left)
+ return tabbables.length > 1 || (tabbables.length === 1 && nodeCount > 1);
+ };
const handleKeyDown = (e: KeyboardEvent) => {
const colCount = props.columns.length - 1;
const [x, y] = focusedCell;
const rowCount = computeVisibleRows(props);
+ const key = e.keyCode;
+
+ if (
+ // @ts-ignore // TODO why do I need this ignore?
+ isInteractiveCell(e.target) &&
+ (key === keyCodes.ENTER || key === keyCodes.F2)
+ ) {
+ e.preventDefault();
+ setIsGridNavigationEnabled(false);
+ }
+
+ if (key === keyCodes.ESCAPE || key === keyCodes.F2) {
+ e.preventDefault();
+ setIsGridNavigationEnabled(true);
+ }
- switch (e.keyCode) {
- case keyCodes.DOWN:
- e.preventDefault();
- if (y < rowCount) {
- setFocusedCell([x, y + 1]);
- }
- break;
- case keyCodes.LEFT:
- e.preventDefault();
- if (x > 0) {
- setFocusedCell([x - 1, y]);
- }
- break;
- case keyCodes.UP:
- e.preventDefault();
- // TODO sort out when a user can arrow up into the column headers
- if (y > 0) {
- setFocusedCell([x, y - 1]);
- }
- break;
- case keyCodes.RIGHT:
- e.preventDefault();
- if (x < colCount) {
- setFocusedCell([x + 1, y]);
- }
- break;
+ if (isGridNavigationEnabled) {
+ switch (e.keyCode) {
+ case keyCodes.DOWN:
+ e.preventDefault();
+ if (y < rowCount) {
+ setFocusedCell([x, y + 1]);
+ }
+ break;
+ case keyCodes.LEFT:
+ e.preventDefault();
+ if (x > 0) {
+ setFocusedCell([x - 1, y]);
+ }
+ break;
+ case keyCodes.UP:
+ e.preventDefault();
+ // TODO sort out when a user can arrow up into the column headers
+ if (y > 0) {
+ setFocusedCell([x, y - 1]);
+ }
+ break;
+ case keyCodes.RIGHT:
+ e.preventDefault();
+ if (x < colCount) {
+ setFocusedCell([x + 1, y]);
+ }
+ break;
+ }
}
};
@@ -238,7 +264,6 @@ export const EuiDataGrid: FunctionComponent = props => {
role="grid"
onKeyDown={handleKeyDown}
ref={gridRef}
- // {...label}
{...rest}
className={classes}>
@@ -251,10 +276,11 @@ export const EuiDataGrid: FunctionComponent = props => {
columnWidths={columnWidths}
columns={visibleColumns}
focusedCell={focusedCell}
- onCellFocus={onCellFocus}
+ onCellFocus={useCallback(setFocusedCell, [setFocusedCell])}
pagination={pagination}
renderCellValue={renderCellValue}
rowCount={rowCount}
+ isGridNavigationEnabled={isGridNavigationEnabled}
/>
diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx
index 7e450e7dad81..9dc9fc12c5b8 100644
--- a/src/components/datagrid/data_grid_body.tsx
+++ b/src/components/datagrid/data_grid_body.tsx
@@ -18,6 +18,7 @@ interface EuiDataGridBodyProps {
rowCount: number;
renderCellValue: EuiDataGridCellProps['renderCellValue'];
pagination?: EuiDataGridPaginationProps;
+ isGridNavigationEnabled: EuiDataGridCellProps['isGridNavigationEnabled'];
}
export const EuiDataGridBody: FunctionComponent<
@@ -31,6 +32,7 @@ export const EuiDataGridBody: FunctionComponent<
rowCount,
renderCellValue,
pagination,
+ isGridNavigationEnabled,
} = props;
const startRow = pagination ? pagination.pageIndex * pagination.pageSize : 0;
@@ -51,6 +53,7 @@ export const EuiDataGridBody: FunctionComponent<
onCellFocus={onCellFocus}
renderCellValue={renderCellValue}
rowIndex={i}
+ isGridNavigationEnabled={isGridNavigationEnabled}
/>
);
}
@@ -64,6 +67,7 @@ export const EuiDataGridBody: FunctionComponent<
onCellFocus,
renderCellValue,
startRow,
+ isGridNavigationEnabled,
]);
return {rows};
diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx
index ee929840eb44..78f7b8b9cb2d 100644
--- a/src/components/datagrid/data_grid_cell.tsx
+++ b/src/components/datagrid/data_grid_cell.tsx
@@ -6,7 +6,10 @@ import React, {
ReactNode,
createRef,
} from 'react';
+// @ts-ignore
+import { EuiFocusTrap } from '../focus_trap';
import { Omit } from '../common';
+import { getTabbables, CELL_CONTENTS_ATTR } from './utils';
interface CellValueElementProps {
rowIndex: number;
@@ -20,6 +23,7 @@ export interface EuiDataGridCellProps {
width?: number;
isFocusable: boolean;
onCellFocus: Function;
+ isGridNavigationEnabled: boolean;
renderCellValue:
| JSXElementConstructor
| ((props: CellValueElementProps) => ReactNode);
@@ -29,7 +33,7 @@ interface EuiDataGridCellState {}
type EuiDataGridCellValueProps = Omit<
EuiDataGridCellProps,
- 'width' | 'isFocusable'
+ 'width' | 'isFocusable' | 'isGridNavigationEnabled'
>;
const EuiDataGridCellContent: FunctionComponent<
@@ -50,19 +54,58 @@ export class EuiDataGridCell extends Component<
EuiDataGridCellState
> {
cellRef = createRef();
+ cellContentsRef = createRef();
updateFocus() {
- if (this.cellRef.current && this.props.isFocusable) {
- this.cellRef.current.focus();
+ const cell = this.cellRef.current;
+ const cellContents = this.cellContentsRef.current;
+
+ if (cell && cellContents && this.props.isFocusable) {
+ const tabbables = getTabbables(cellContents);
+ const nodeCount = cellContents.childNodes.length;
+
+ if (this.props.isGridNavigationEnabled) {
+ if (tabbables.length === 1 && nodeCount === 1) {
+ // @ts-ignore // TODO why do I need this ignore?
+ tabbables[0].focus();
+ } else {
+ cell.focus();
+ }
+ } else {
+ // @ts-ignore // TODO why do I need this ignore?
+ tabbables[0].focus();
+ }
+ }
+ }
+
+ setTabbablesTabIndex() {
+ const { isFocusable, isGridNavigationEnabled } = this.props;
+ const areContentsFocusable = isFocusable && !isGridNavigationEnabled;
+
+ if (this.cellContentsRef.current) {
+ getTabbables(this.cellContentsRef.current).forEach(element => {
+ element.setAttribute('tabIndex', areContentsFocusable ? '0' : '-1');
+ });
}
}
- componentDidUpdate() {
- this.updateFocus();
+ componentDidMount() {
+ this.setTabbablesTabIndex();
+ }
+
+ componentDidUpdate(prevProps: EuiDataGridCellProps) {
+ const didFocusChange = prevProps.isFocusable !== this.props.isFocusable;
+ const didNavigationChange =
+ prevProps.isGridNavigationEnabled !== this.props.isGridNavigationEnabled;
+
+ if (didFocusChange || didNavigationChange) {
+ this.updateFocus();
+ this.setTabbablesTabIndex();
+ }
}
render() {
- const { width, isFocusable, ...rest } = this.props;
+ const { width, isFocusable, isGridNavigationEnabled, ...rest } = this.props;
const { colIndex, rowIndex, onCellFocus } = rest;
return (
@@ -72,9 +115,13 @@ export class EuiDataGridCell extends Component<
ref={this.cellRef}
className="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
- onFocus={() => onCellFocus(colIndex, rowIndex)}
+ onFocus={() => onCellFocus([colIndex, rowIndex])}
style={{ width: `${width}px` }}>
-
+
+
+
+
+
);
}
diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx
index 121208dd88a0..9338ad32e9b9 100644
--- a/src/components/datagrid/data_grid_data_row.tsx
+++ b/src/components/datagrid/data_grid_data_row.tsx
@@ -12,6 +12,7 @@ export type EuiDataGridDataRowProps = CommonProps &
columnWidths: EuiDataGridColumnWidths;
focusedCell: [number, number];
renderCellValue: EuiDataGridCellProps['renderCellValue'];
+ isGridNavigationEnabled: EuiDataGridCellProps['isGridNavigationEnabled'];
onCellFocus: Function;
};
@@ -26,6 +27,7 @@ const EuiDataGridDataRow: FunctionComponent<
rowIndex,
focusedCell,
onCellFocus,
+ isGridNavigationEnabled,
'data-test-subj': _dataTestSubj,
...rest
} = props;
@@ -52,6 +54,7 @@ const EuiDataGridDataRow: FunctionComponent<
renderCellValue={renderCellValue}
onCellFocus={onCellFocus}
isFocusable={isFocusable}
+ isGridNavigationEnabled={isGridNavigationEnabled}
/>
);
})}
diff --git a/src/components/datagrid/utils.tsx b/src/components/datagrid/utils.tsx
new file mode 100644
index 000000000000..7aeaecbb0309
--- /dev/null
+++ b/src/components/datagrid/utils.tsx
@@ -0,0 +1,8 @@
+import tabbable from 'tabbable';
+
+export const getTabbables = (element: Element) => [
+ ...tabbable(element),
+ ...Array.from(element.querySelectorAll('[tabIndex="-1"]')),
+];
+
+export const CELL_CONTENTS_ATTR = 'data-js-cell-contents-container';
diff --git a/src/services/key_codes.ts b/src/services/key_codes.ts
index 87afb804c106..6bc959170ecb 100644
--- a/src/services/key_codes.ts
+++ b/src/services/key_codes.ts
@@ -3,6 +3,7 @@ export const SPACE = 32;
export const ESCAPE = 27;
export const TAB = 9;
export const BACKSPACE = 8;
+export const F2 = 113;
// Arrow keys
export const DOWN = 40;
@@ -16,6 +17,7 @@ export enum keyCodes {
ESCAPE = 27,
TAB = 9,
BACKSPACE = 8,
+ F2 = 113,
DOWN = 40,
UP = 38,
diff --git a/yarn.lock b/yarn.lock
index 4ea8d442f83d..25908800c783 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1156,6 +1156,11 @@
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.1.tgz#9b7cdae9cdc8b1a7020ca7588018dac64c770866"
integrity sha512-5/bJS/uGB5kmpRrrAWXQnmyKlv+4TlPn4f+A2NBa93p+mt6Ht+YcNGkQKf8HMx28a9hox49ZXShtbGqZkk41Sw==
+"@types/tabbable@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/tabbable/-/tabbable-3.1.0.tgz#540d4c2729872560badcc220e73c9412c1d2bffe"
+ integrity sha512-LL0q/bTlzseaXQ8j91eZ+Z8FQUzo0nwkng00B8365qULvFyiSOWylxV8m31Gmee3QuidkDqR72a9NRfR8s4qTw==
+
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@@ -13988,10 +13993,10 @@ sync-exec@^0.6.2:
resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.6.2.tgz#717d22cc53f0ce1def5594362f3a89a2ebb91105"
integrity sha1-cX0izFPwzh3vVZQ2LzqJouu5EQU=
-tabbable@^1.1.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.2.tgz#b171680aea6e0a3e9281ff23532e2e5de11c0d94"
- integrity sha512-77oqsKEPrxIwgRcXUwipkj9W5ItO97L6eUT1Ar7vh+El16Zm4M6V+YU1cbipHEa6q0Yjw8O3Hoh8oRgatV5s7A==
+tabbable@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
+ integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
table@^3.7.8:
version "3.8.3"