From b9839fe2aab702b1e9c8b11d927c23da9f93cc0e Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Sun, 22 Sep 2019 20:25:08 -0700 Subject: [PATCH 01/18] cell expansion working mostly --- src-docs/src/views/datagrid/in_memory.js | 6 +- src-docs/src/views/datagrid/schema.js | 30 ++- src-docs/src/views/icon/icons.js | 1 + .../__snapshots__/data_grid.test.tsx.snap | 228 +++++++++++++++++- .../datagrid/_data_grid_data_row.scss | 65 +++++ src/components/datagrid/data_grid.test.tsx | 4 +- src/components/datagrid/data_grid_cell.tsx | 73 +++++- src/components/datagrid/data_grid_schema.ts | 11 + .../icon/__snapshots__/icon.test.tsx.snap | 16 ++ src/components/icon/assets/expandMini.js | 17 ++ src/components/icon/assets/expandMini.svg | 3 + src/components/icon/icon.tsx | 1 + 12 files changed, 437 insertions(+), 18 deletions(-) create mode 100644 src/components/icon/assets/expandMini.js create mode 100644 src/components/icon/assets/expandMini.svg diff --git a/src-docs/src/views/datagrid/in_memory.js b/src-docs/src/views/datagrid/in_memory.js index 6b68a9943e7..07270a55be0 100644 --- a/src-docs/src/views/datagrid/in_memory.js +++ b/src-docs/src/views/datagrid/in_memory.js @@ -23,7 +23,7 @@ const columns = [ id: 'amount', }, { - id: 'phone', + id: 'boolean', }, { id: 'version', @@ -46,8 +46,8 @@ for (let i = 1; i < 100; i++) { ), date: fake('{{date.past}}'), account: fake('{{finance.account}}'), - amount: fake('{{finance.currencySymbol}}{{finance.amount}}'), - phone: fake('{{phone.phoneNumber}}'), + amount: fake('${{finance.amount}}'), + boolean: fake('{{random.boolean}}'), version: fake('{{system.semver}}'), }); } diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 200170927c4..3511934a86f 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -30,7 +30,7 @@ const columns = [ dataType: 'currency', }, { - id: 'phone', + id: 'json', }, { id: 'version', @@ -54,7 +54,32 @@ for (let i = 1; i < 5; i++) { date: fake('{{date.past}}'), account: fake('{{finance.account}}'), amount: fake('${{finance.amount}}'), - phone: fake('{{phone.phoneNumber}}'), + json: JSON.stringify( + [ + { + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: fake('{{internet.email}}'), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('${{finance.amount}}'), + version: fake('{{system.semver}}'), + friends: [ + { + name: fake( + '{{name.lastName}}, {{name.firstName}} {{name.suffix}}' + ), + email: fake('{{internet.email}}'), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('${{finance.amount}}'), + version: fake('{{system.semver}}'), + }, + ], + }, + ], + null, + 2 + ), version: fake('{{system.semver}}'), }); } @@ -101,6 +126,7 @@ export default class InMemoryDataGrid extends Component { aria-label="Top EUI contributors" columns={columns} rowCount={data.length} + inMemory="sorting" renderCellValue={({ rowIndex, columnId }) => { const value = data[rowIndex][columnId]; return value; diff --git a/src-docs/src/views/icon/icons.js b/src-docs/src/views/icon/icons.js index 672ebc024bb..b5d9e640918 100644 --- a/src-docs/src/views/icon/icons.js +++ b/src-docs/src/views/icon/icons.js @@ -65,6 +65,7 @@ export const iconTypes = [ 'empty', 'exit', 'expand', + 'expandMini', 'exportAction', 'eye', 'eyeClosed', diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 3dd2294ec54..81c961d89a4 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -337,7 +337,43 @@ Array [ class="euiDataGridRowCell__content" data-js-cell-contents-container="false" > - 0, A +
+
+ 0, A +
+
+
+
+ +
+
+
+
@@ -371,7 +407,43 @@ Array [ class="euiDataGridRowCell__content" data-js-cell-contents-container="false" > - 0, B +
+
+ 0, B +
+
+
+
+ +
+
+
+
@@ -411,7 +483,43 @@ Array [ class="euiDataGridRowCell__content" data-js-cell-contents-container="false" > - 1, A +
+
+ 1, A +
+
+
+
+ +
+
+
+
@@ -445,7 +553,43 @@ Array [ class="euiDataGridRowCell__content" data-js-cell-contents-container="false" > - 1, B +
+
+ 1, B +
+
+
+
+ +
+
+
+
@@ -485,7 +629,43 @@ Array [ class="euiDataGridRowCell__content" data-js-cell-contents-container="false" > - 2, A +
+
+ 2, A +
+
+
+
+ +
+
+
+
@@ -519,7 +699,43 @@ Array [ class="euiDataGridRowCell__content" data-js-cell-contents-container="false" > - 2, B +
+
+ 2, B +
+
+
+
+ +
+
+
+
diff --git a/src/components/datagrid/_data_grid_data_row.scss b/src/components/datagrid/_data_grid_data_row.scss index cb6a3c6ceca..591ea939d35 100644 --- a/src/components/datagrid/_data_grid_data_row.scss +++ b/src/components/datagrid/_data_grid_data_row.scss @@ -36,6 +36,16 @@ border-radius: 1px; // Needed so it sits above potential striping in the next row z-index: 2; + + .euiDataGridRowCell__expandButton { + margin-left: $euiDataGridCellPaddingM; + } + + .euiDataGridRowCell__expandButtonIcon { + display: flex; + width: inherit; + visibility: visible; + } } &:focus:not(:first-of-type) { @@ -50,6 +60,11 @@ &.euiDataGridRowCell--currency { text-align: right; } + + + &.euiDataGridRowCell--boolean { + text-transform: capitalize; + } } .euiDataGridRowCell__content { @@ -57,6 +72,56 @@ overflow: hidden; white-space: nowrap; } +.euiDataGridRowCell__popover { + @include euiScrollBar; + overflow: auto; + max-width: 400px !important; + max-height: 400px !important; +} + +.euiDataGridRowCell__expand { + width: 100%; + max-width: 100%; + display: flex; + align-items: center; +} + +.euiDataGridRowCell__expandCode { + @include euiTextTruncate; + overflow: hidden; + white-space: nowrap; + flex-grow: 1; +} + +.euiDataGridRowCell__expandButton { + display: flex; + flex-grow: 0; + + &-isActive, &:focus { + margin-left: $euiDataGridCellPaddingM; + } +} + +.euiDataGridRowCell__expandButtonIcon { + height: $euiSizeM; + width: $euiSizeM; + min-height: $euiSizeM; + min-width: $euiSizeM; + background: $euiColorPrimary; + color: $euiColorGhost; + border-radius: $euiBorderRadius / 2; + padding: 0; + width: 0; + min-width: 0; + overflow: hidden; + visibility: hidden; + + &-isActive, &:focus { + width: inherit; + visibility: visible; + background: $euiColorPrimary; + } +} // Row highlights @include euiDataGridStyles(rowHoverHighlight) { diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 70646e046a5..8b3409918ad 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -498,7 +498,7 @@ Array [ .map(x => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ - "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--json", "euiDataGridRowCell euiDataGridRowCell--boolean", "euiDataGridRowCell euiDataGridRowCell--currency", "euiDataGridRowCell euiDataGridRowCell--datetime", @@ -539,7 +539,7 @@ Array [ .map(x => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ - "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--json", "euiDataGridRowCell euiDataGridRowCell--ipaddress", ] `); diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index f15e3e07029..7cca6af6d01 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -6,11 +6,18 @@ import React, { ReactNode, createRef, HTMLAttributes, + useState, } from 'react'; -import classnames from 'classnames'; +import classNames from 'classnames'; // @ts-ignore import { EuiFocusTrap } from '../focus_trap'; +import { EuiPopover } from '../popover'; +// @ts-ignore +import { EuiCodeBlock } from '../code'; import { CommonProps, Omit } from '../common'; +// @ts-ignore +import { EuiCode } from '../code'; +import { EuiButtonIcon } from '../button'; import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; @@ -50,13 +57,69 @@ const EuiDataGridCellContent: FunctionComponent< } > = memo(props => { const { renderCellValue, ...rest } = props; + const [popoverIsOpen, setPopoverIsOpen] = useState(false); // React is more permissable than the TS types indicate const CellElement = renderCellValue as JSXElementConstructor< CellValueElementProps >; - return ; + const buttonIconClasses = classNames('euiDataGridRowCell__expandButtonIcon', { + 'euiDataGridRowCell__expandButtonIcon-isActive': popoverIsOpen, + }); + + const buttonClasses = classNames('euiDataGridRowCell__expandButton', { + 'euiDataGridRowCell__expandButton-isActive': popoverIsOpen, + }); + + const expandButton = ( + setPopoverIsOpen(!popoverIsOpen)} + title="Expand cell content" + /> + ); + + // TODO: This is temporary. It's mostly just to show that different schema likely will require different + // markup. We also likely will want a way to pass a custom render to the popup and the default cell + // content as part of the data config. + let cellElement; + if (props.columnType === 'json') { + cellElement = ( + + + + ); + } else { + cellElement = ; + } + + return ( +
+
+ +
+
+ setPopoverIsOpen(false)}> + {cellElement} + +
+
+ ); }); const IS_TABBABLE_ATTR = 'data-is-tabbable'; @@ -203,17 +266,17 @@ export class EuiDataGridCell extends Component< [CELL_CONTENTS_ATTR]: isInteractive, }; - const className = classnames('euiDataGridRowCell', { + const className = classNames('euiDataGridRowCell', { [`euiDataGridRowCell--${columnType}`]: columnType, }); const cellProps = { ...this.state.cellProps, - 'data-test-subj': classnames( + 'data-test-subj': classNames( 'dataGridRowCell', this.state.cellProps['data-test-subj'] ), - className: classnames(className, this.state.cellProps.className), + className: classNames(className, this.state.cellProps.className), }; const widthStyle = width != null ? { width: `${width}px` } : {}; diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index 9dcc26f1b2e..a3b51895dd6 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -65,6 +65,17 @@ const schemaDetectors: SchemaDetector[] = [ return matchLength / value.length || 0; }, }, + { + type: 'json', + detector(value: string) { + try { + JSON.parse(value); + return 1; + } catch (e) { + return 0; + } + }, + }, ]; export interface EuiDataGridSchema { diff --git a/src/components/icon/__snapshots__/icon.test.tsx.snap b/src/components/icon/__snapshots__/icon.test.tsx.snap index 36e57333a4b..07c4f543898 100644 --- a/src/components/icon/__snapshots__/icon.test.tsx.snap +++ b/src/components/icon/__snapshots__/icon.test.tsx.snap @@ -1838,6 +1838,22 @@ exports[`EuiIcon props type expand is rendered 1`] = ` `; +exports[`EuiIcon props type expandMini is rendered 1`] = ` + + + +`; + exports[`EuiIcon props type exportAction is rendered 1`] = ` ( + + + +); + +export const icon = EuiIconExpandMini; diff --git a/src/components/icon/assets/expandMini.svg b/src/components/icon/assets/expandMini.svg new file mode 100644 index 00000000000..32bf43dedbf --- /dev/null +++ b/src/components/icon/assets/expandMini.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 1c3dc0ce8a2..b66fbc9e3f4 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -106,6 +106,7 @@ const typeToPathMap = { emsApp: 'app_ems', exit: 'exit', expand: 'expand', + expandMini: 'expandMini', exportAction: 'export', eye: 'eye', eyeClosed: 'eye_closed', From 3cfae45ce39dd3f8a904fc1d6efd6c16dca665fb Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Sun, 22 Sep 2019 20:32:46 -0700 Subject: [PATCH 02/18] fix double import --- src/components/datagrid/data_grid_cell.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 7cca6af6d01..1fb2b20427b 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -16,7 +16,6 @@ import { EuiPopover } from '../popover'; import { EuiCodeBlock } from '../code'; import { CommonProps, Omit } from '../common'; // @ts-ignore -import { EuiCode } from '../code'; import { EuiButtonIcon } from '../button'; import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; From 4209301e1bd372670f22b3fe29d0ee23aed480aa Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Sun, 22 Sep 2019 20:53:13 -0700 Subject: [PATCH 03/18] add search to field selector --- .../datagrid/_data_grid_column_selector.scss | 8 ++ src/components/datagrid/column_selector.tsx | 105 ++++++++++-------- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/components/datagrid/_data_grid_column_selector.scss b/src/components/datagrid/_data_grid_column_selector.scss index 63aab23b946..353b3d0ddf1 100644 --- a/src/components/datagrid/_data_grid_column_selector.scss +++ b/src/components/datagrid/_data_grid_column_selector.scss @@ -7,6 +7,14 @@ } } +.euiDataGridColumnSelector__columnList { + @include euiYScrollWithShadows; + max-height: 400px; + // Because we only want this to scroll vertically, we need to offset inner euiFlexGroup negative padding by adding padding + padding: $euiSizeS; + margin: 0 -$euiSizeS; +} + .euiDataGridColumnSelectorPopover { // Hack because the fixed positions of drag and drop don't work inside of transformed elements // sass-lint:disable-block no-important diff --git a/src/components/datagrid/column_selector.tsx b/src/components/datagrid/column_selector.tsx index 735f6c7ccd4..96d608b5758 100644 --- a/src/components/datagrid/column_selector.tsx +++ b/src/components/datagrid/column_selector.tsx @@ -7,13 +7,13 @@ import React, { import classNames from 'classnames'; import { EuiDataGridColumn } from './data_grid_types'; // @ts-ignore-next-line -import { EuiPopover, EuiPopoverFooter } from '../popover'; +import { EuiPopover, EuiPopoverFooter, EuiPopoverTitle } from '../popover'; import { EuiI18n } from '../i18n'; // @ts-ignore-next-line import { EuiButtonEmpty } from '../button'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; // @ts-ignore-next-line -import { EuiSwitch } from '../form'; +import { EuiSwitch, EuiFieldText } from '../form'; import { EuiDragDropContext, EuiDraggable, @@ -89,52 +89,61 @@ export const useColumnSelector = ( )} }> - - - - {sortedColumns.map(({ id }, index) => ( - - {(provided, state) => ( -
- - - ) => { - const nextVisibleColumns = sortedColumns.filter( - ({ id: columnId }) => - checked - ? visibleColumnIds.has(columnId) || - id === columnId - : visibleColumnIds.has(columnId) && - id !== columnId - ); - setVisibleColumns(nextVisibleColumns); - }} - /> - - -
- -
-
-
-
- )} - - ))} - - - + + + +
+ + + + {sortedColumns.map(({ id }, index) => ( + + {(provided, state) => ( +
+ + + ) => { + const nextVisibleColumns = sortedColumns.filter( + ({ id: columnId }) => + checked + ? visibleColumnIds.has(columnId) || + id === columnId + : visibleColumnIds.has(columnId) && + id !== columnId + ); + setVisibleColumns(nextVisibleColumns); + }} + /> + + +
+ +
+
+
+
+ )} +
+ ))} +
+
+
+
From eb3a495113701e0e5b9493521dee24ed54fef56e Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Sun, 22 Sep 2019 21:26:53 -0700 Subject: [PATCH 04/18] euitext --- src/components/datagrid/data_grid_cell.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 1fb2b20427b..72c40bdc9a1 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -10,6 +10,7 @@ import React, { } from 'react'; import classNames from 'classnames'; // @ts-ignore +import { EuiText } from '../text'; import { EuiFocusTrap } from '../focus_trap'; import { EuiPopover } from '../popover'; // @ts-ignore @@ -98,7 +99,11 @@ const EuiDataGridCellContent: FunctionComponent< ); } else { - cellElement = ; + cellElement = ( + + + + ); } return ( From 2c35acb30641f70e05ec018ac785c8f311fb327d Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Tue, 24 Sep 2019 22:23:27 -0700 Subject: [PATCH 05/18] cell epansion is now optional through a config --- src-docs/src/views/datagrid/schema.js | 3 ++ .../datagrid/_data_grid_column_selector.scss | 4 +-- .../datagrid/_data_grid_data_row.scss | 10 ++++--- src/components/datagrid/data_grid_cell.tsx | 29 ++++++++++--------- .../datagrid/data_grid_data_row.tsx | 3 ++ src/components/datagrid/data_grid_types.ts | 1 + 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 3511934a86f..3f67a51ddd3 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -11,6 +11,7 @@ import { iconTypes } from '../icon/icons'; const columns = [ { id: 'name', + isExpandable: false, }, { id: 'email', @@ -21,6 +22,7 @@ const columns = [ { id: 'account', dataType: 'numeric', + isExpandable: false, }, { id: 'date', @@ -28,6 +30,7 @@ const columns = [ { id: 'amount', dataType: 'currency', + isExpandable: false, }, { id: 'json', diff --git a/src/components/datagrid/_data_grid_column_selector.scss b/src/components/datagrid/_data_grid_column_selector.scss index 353b3d0ddf1..a339a0de063 100644 --- a/src/components/datagrid/_data_grid_column_selector.scss +++ b/src/components/datagrid/_data_grid_column_selector.scss @@ -7,12 +7,12 @@ } } +// Because we only want this to scroll vertically, we need to offset inner euiFlexGroup negative padding by adding padding .euiDataGridColumnSelector__columnList { @include euiYScrollWithShadows; max-height: 400px; - // Because we only want this to scroll vertically, we need to offset inner euiFlexGroup negative padding by adding padding padding: $euiSizeS; - margin: 0 -$euiSizeS; + margin: 0 (-$euiSizeS); } .euiDataGridColumnSelectorPopover { diff --git a/src/components/datagrid/_data_grid_data_row.scss b/src/components/datagrid/_data_grid_data_row.scss index 591ea939d35..1805553339d 100644 --- a/src/components/datagrid/_data_grid_data_row.scss +++ b/src/components/datagrid/_data_grid_data_row.scss @@ -72,9 +72,11 @@ overflow: hidden; white-space: nowrap; } + .euiDataGridRowCell__popover { @include euiScrollBar; overflow: auto; + // sass-lint:disable-block no-important max-width: 400px !important; max-height: 400px !important; } @@ -97,16 +99,15 @@ display: flex; flex-grow: 0; - &-isActive, &:focus { + &-isActive, + &:focus { margin-left: $euiDataGridCellPaddingM; } } .euiDataGridRowCell__expandButtonIcon { height: $euiSizeM; - width: $euiSizeM; min-height: $euiSizeM; - min-width: $euiSizeM; background: $euiColorPrimary; color: $euiColorGhost; border-radius: $euiBorderRadius / 2; @@ -116,7 +117,8 @@ overflow: hidden; visibility: hidden; - &-isActive, &:focus { + &-isActive, + &:focus { width: inherit; visibility: visible; background: $euiColorPrimary; diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 72c40bdc9a1..28071ceb761 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -37,6 +37,7 @@ export interface EuiDataGridCellProps { onCellFocus: Function; isGridNavigationEnabled: boolean; interactiveCellId: string; + isExpandable: boolean; renderCellValue: | JSXElementConstructor | ((props: CellValueElementProps) => ReactNode); @@ -56,7 +57,7 @@ const EuiDataGridCellContent: FunctionComponent< setCellProps: CellValueElementProps['setCellProps']; } > = memo(props => { - const { renderCellValue, ...rest } = props; + const { renderCellValue, isExpandable, ...rest } = props; const [popoverIsOpen, setPopoverIsOpen] = useState(false); // React is more permissable than the TS types indicate @@ -87,7 +88,7 @@ const EuiDataGridCellContent: FunctionComponent< // TODO: This is temporary. It's mostly just to show that different schema likely will require different // markup. We also likely will want a way to pass a custom render to the popup and the default cell // content as part of the data config. - let cellElement; + let cellElement: ReactNode; if (props.columnType === 'json') { cellElement = ( -
- setPopoverIsOpen(false)}> - {cellElement} - -
+ {isExpandable && ( +
+ setPopoverIsOpen(false)}> + {cellElement} + +
+ )} ); }); diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index c38d31f23dc..51a577164d0 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -48,6 +48,8 @@ const EuiDataGridDataRow: FunctionComponent<
{columns.map((props, i) => { const { id } = props; + const isExpandable = + props.isExpandable !== undefined ? props.isExpandable : true; const width = columnWidths[id] || defaultColumnWidth; @@ -67,6 +69,7 @@ const EuiDataGridDataRow: FunctionComponent< isFocusable={isFocusable} isGridNavigationEnabled={isGridNavigationEnabled} interactiveCellId={interactiveCellId} + isExpandable={isExpandable} /> ); })} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 5691e1bf472..2a139b34ccc 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -4,6 +4,7 @@ export interface EuiDataGridColumn { id: string; // allow devs to pass arbitrary dataType strings, but internally keep the code matching against the known types dataType?: EuiDataGridSchema['*']['columnType']; + isExpandable?: boolean; } export interface EuiDataGridColumnWidths { From fc5d03eaee1e2e0a9739338c279d8bbd580cffa4 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Wed, 25 Sep 2019 14:14:57 -0700 Subject: [PATCH 06/18] keydown event for cells --- src/components/datagrid/data_grid_cell.tsx | 34 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 28071ceb761..9e8c15f50e4 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -7,6 +7,8 @@ import React, { createRef, HTMLAttributes, useState, + KeyboardEvent, + useEffect, } from 'react'; import classNames from 'classnames'; // @ts-ignore @@ -20,6 +22,7 @@ import { CommonProps, Omit } from '../common'; import { EuiButtonIcon } from '../button'; import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; +import { keyCodes } from '../../services'; export interface CellValueElementProps { rowIndex: number; @@ -38,6 +41,7 @@ export interface EuiDataGridCellProps { isGridNavigationEnabled: boolean; interactiveCellId: string; isExpandable: boolean; + isExpanded?: boolean; renderCellValue: | JSXElementConstructor | ((props: CellValueElementProps) => ReactNode); @@ -45,6 +49,7 @@ export interface EuiDataGridCellProps { interface EuiDataGridCellState { cellProps: CommonProps & HTMLAttributes; + popoverIsOpen: boolean; } type EuiDataGridCellValueProps = Omit< @@ -57,8 +62,12 @@ const EuiDataGridCellContent: FunctionComponent< setCellProps: CellValueElementProps['setCellProps']; } > = memo(props => { - const { renderCellValue, isExpandable, ...rest } = props; - const [popoverIsOpen, setPopoverIsOpen] = useState(false); + const { renderCellValue, isExpandable, isExpanded, ...rest } = props; + const [popoverIsOpen, setPopoverIsOpen] = useState(isExpanded); + + useEffect(() => { + setPopoverIsOpen(isExpanded); + }, [isExpanded]); // React is more permissable than the TS types indicate const CellElement = renderCellValue as JSXElementConstructor< @@ -139,6 +148,7 @@ export class EuiDataGridCell extends Component< cellContentsRef = createRef(); state: EuiDataGridCellState = { cellProps: {}, + popoverIsOpen: false, }; isInteractiveCell() { @@ -262,6 +272,7 @@ export class EuiDataGridCell extends Component< const { width, isFocusable, + isExpandable, isGridNavigationEnabled, interactiveCellId, columnType, @@ -293,6 +304,22 @@ export class EuiDataGridCell extends Component< cellProps.style = widthStyle; } + const handleCellKeyDown = (e: KeyboardEvent) => { + if (isExpandable) { + switch (e.keyCode) { + case keyCodes.ENTER: + e.preventDefault(); + this.setState({ popoverIsOpen: true }); + console.log('hello'); + break; + case keyCodes.F2: + e.preventDefault(); + this.setState({ popoverIsOpen: true }); + break; + } + } + }; + return (
onCellFocus([colIndex, rowIndex])}>
From 5302ce509e70feb97a6774ad194b33614c636d81 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Mon, 30 Sep 2019 13:01:10 -0700 Subject: [PATCH 07/18] remove tabbables --- src/components/datagrid/data_grid.tsx | 11 +-- src/components/datagrid/data_grid_cell.tsx | 88 +--------------------- 2 files changed, 3 insertions(+), 96 deletions(-) diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 6b685472eb0..347923f6103 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -38,7 +38,6 @@ import { EuiTablePagination } from '../table/table_pagination'; // @ts-ignore-next-line import { EuiFocusTrap } from '../focus_trap'; import { EuiResizeObserver } from '../observer/resize_observer'; -import { CELL_CONTENTS_ATTR } from './utils'; import { EuiDataGridInMemoryRenderer } from './data_grid_inmemory_renderer'; import { getMergedSchema, @@ -110,12 +109,6 @@ const cellPaddingsToClassMap: { const ORIGIN: [number, number] = [0, 0]; // returns whether or not this element is a gridcell with CELL_CONTENTS_ATTR -const isInteractiveCell = (element: HTMLElement) => { - if (element.getAttribute('role') !== 'gridcell') { - return false; - } - return Boolean(element.querySelector(`[${CELL_CONTENTS_ATTR}="true"]`)); -}; function computeVisibleRows(props: EuiDataGridProps) { const { pagination, rowCount } = props; @@ -314,11 +307,9 @@ function createKeyDownHandler( const colCount = visibleColumns.length - 1; const [x, y] = focusedCell; const rowCount = computeVisibleRows(props); - const { keyCode, target } = event; + const { keyCode } = event; if ( - target instanceof HTMLElement && - isInteractiveCell(target) && isGridNavigationEnabled && (keyCode === keyCodes.ENTER || keyCode === keyCodes.F2) ) { diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index e320b2c18a7..3d59bec027d 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -19,7 +19,6 @@ import { EuiCodeBlock } from '../code'; import { CommonProps, Omit } from '../common'; // @ts-ignore import { EuiButtonIcon } from '../button'; -import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; import { keyCodes } from '../../services'; @@ -91,7 +90,7 @@ const EuiDataGridCellContent: FunctionComponent< color="text" iconSize="s" iconType="expandMini" - aria-label="Expand cell content" + aria-hidden onClick={() => setPopoverIsOpen(!popoverIsOpen)} title="Expand cell content" /> @@ -141,8 +140,6 @@ const EuiDataGridCellContent: FunctionComponent< ); }); -const IS_TABBABLE_ATTR = 'data-is-tabbable'; - export class EuiDataGridCell extends Component< EuiDataGridCellProps, EuiDataGridCellState @@ -154,91 +151,18 @@ export class EuiDataGridCell extends Component< popoverIsOpen: false, }; - isInteractiveCell() { - const cellContents = this.cellContentsRef.current; - - if (!cellContents) { - return false; - } - - const tabbables = getTabbables(cellContents); - - return ( - tabbables.length > 1 || - (tabbables.length === 1 && this.hasNotTabbables(cellContents)) - ); - } - updateFocus() { const cell = this.cellRef.current; const cellContents = this.cellContentsRef.current; const { isFocusable, isGridNavigationEnabled } = this.props; if (cell && isFocusable && cellContents) { - const tabbables = getTabbables(cellContents); - const isASimpleInteractiveCell = - tabbables.length === 1 && !this.hasNotTabbables(cellContents); - - if ( - !isGridNavigationEnabled || - (isGridNavigationEnabled && isASimpleInteractiveCell) - ) { - (tabbables[0] as HTMLElement).focus(); - } else { + if (isGridNavigationEnabled) { cell.focus(); } } } - setTabbablesTabIndex() { - const cellContents = this.cellContentsRef.current; - - if (cellContents) { - const { isFocusable, isGridNavigationEnabled } = this.props; - const areContentsFocusable = isFocusable && !isGridNavigationEnabled; - - getTabbables(cellContents).forEach(element => { - element.setAttribute('tabIndex', areContentsFocusable ? '0' : '-1'); - element.setAttribute(IS_TABBABLE_ATTR, 'true'); - }); - } - } - - hasNotTabbables(cellContents: Element) { - const clone = cellContents.cloneNode(true) as HTMLElement; - - // has to exist because we set the `IS_TABBABLE_ATTR` attribute on it - const tabbableElement = clone.querySelector(`[${IS_TABBABLE_ATTR}]`)!; - - if (tabbableElement) { - // IE 11 doesn't support remove - if (tabbableElement.remove) { - tabbableElement.remove(); - } else { - tabbableElement.parentNode!.removeChild(tabbableElement); - } - } - - // textContent includes not human readable text - // but innerText causes a page reflow - // so, only force a reflow if we have a strong signal that we should - if (clone.textContent && clone.textContent.length > 0) { - // Fallback to innerText if textContent isn't available - // Only documented to fallback in tests; all officially supported browsers support innerText - if (typeof clone.innerText === 'undefined') { - return clone.textContent.length > 0; - } - - return clone.innerText.length > 0; - } - - return false; - } - - componentDidMount() { - this.setTabbablesTabIndex(); - } - componentDidUpdate(prevProps: EuiDataGridCellProps) { const didFocusChange = prevProps.isFocusable !== this.props.isFocusable; const didNavigationChange = @@ -246,7 +170,6 @@ export class EuiDataGridCell extends Component< if (didFocusChange || didNavigationChange) { this.updateFocus(); - this.setTabbablesTabIndex(); } } @@ -289,10 +212,6 @@ export class EuiDataGridCell extends Component< ...rest } = this.props; const { colIndex, rowIndex } = rest; - const isInteractive = this.isInteractiveCell(); - const isInteractiveCell = { - [CELL_CONTENTS_ATTR]: isInteractive, - }; const className = classNames('euiDataGridRowCell', { [`euiDataGridRowCell--${columnType}`]: columnType, @@ -333,7 +252,6 @@ export class EuiDataGridCell extends Component< return (
{ this.updateFocus(); - this.setTabbablesTabIndex(); }} observerOptions={{ childList: true, @@ -352,7 +269,6 @@ export class EuiDataGridCell extends Component< {ref => (
Date: Wed, 2 Oct 2019 10:29:19 -0600 Subject: [PATCH 08/18] Clean up some code & tests --- src-docs/src/views/datagrid/schema.js | 4 +-- .../button/button_icon/button_icon.tsx | 2 +- .../__snapshots__/data_grid.test.tsx.snap | 18 ++++------- src/components/datagrid/data_grid.test.tsx | 30 +++++++++++++------ src/components/datagrid/data_grid.tsx | 2 -- src/components/datagrid/data_grid_cell.tsx | 11 ++++--- src/components/datagrid/data_grid_schema.ts | 5 ++++ src/components/datagrid/utils.tsx | 8 ----- 8 files changed, 40 insertions(+), 40 deletions(-) delete mode 100644 src/components/datagrid/utils.tsx diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 8f125c917c0..652943f2340 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -87,7 +87,7 @@ for (let i = 1; i < 5; i++) { }); } -export default class InMemoryDataGrid extends Component { +export default class DataGridSchema extends Component { constructor(props) { super(props); @@ -144,7 +144,7 @@ export default class InMemoryDataGrid extends Component { aria-label="Top EUI contributors" columns={columns} rowCount={data.length} - inMemory="sorting" + inMemory={{ level: 'sorting' }} renderCellValue={({ rowIndex, columnId }) => { const value = data[rowIndex][columnId]; return value; diff --git a/src/components/button/button_icon/button_icon.tsx b/src/components/button/button_icon/button_icon.tsx index 300a7746735..b1cada4b2e3 100644 --- a/src/components/button/button_icon/button_icon.tsx +++ b/src/components/button/button_icon/button_icon.tsx @@ -84,7 +84,7 @@ export const EuiButtonIcon: FunctionComponent = ({ buttonRef, ...rest }) => { - if (!rest['aria-label'] && !rest['aria-labelledby']) { + if (!rest['aria-label'] && !rest['aria-labelledby'] && !rest['aria-hidden']) { console.warn( `EuiButtonIcon requires aria-label or aria-labelledby to be specified because icon-only buttons are screen-reader-inaccessible without them.` diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 004e7201d87..89df15203f5 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -322,7 +322,6 @@ Array [
-
+ class="euiIcon euiIcon--small euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> +
@@ -369,44 +367,42 @@ Array [ role="gridcell" tabindex="-1" > -
+
- 0, B -
-
+ 0, B +
+
- + class="euiIcon euiIcon--small euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> +
@@ -425,44 +421,42 @@ Array [ role="gridcell" tabindex="-1" > -
+
- 1, A -
-
+ 1, A +
+
- + class="euiIcon euiIcon--small euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> +
@@ -475,44 +469,42 @@ Array [ role="gridcell" tabindex="-1" > -
+
- 1, B -
-
+ 1, B +
+
- + class="euiIcon euiIcon--small euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> +
@@ -531,44 +523,42 @@ Array [ role="gridcell" tabindex="-1" > -
+
- 2, A -
-
+ 2, A +
+
- + class="euiIcon euiIcon--small euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> +
@@ -581,44 +571,42 @@ Array [ role="gridcell" tabindex="-1" > -
+
- 2, B -
-
+ 2, B +
+
- + class="euiIcon euiIcon--small euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> +
diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 28b2147bea7..37b6ee47226 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -297,9 +297,7 @@ function createKeyDownHandler( props: EuiDataGridProps, visibleColumns: EuiDataGridProps['columns'], focusedCell: [number, number], - setFocusedCell: (focusedCell: [number, number]) => void, - isGridNavigationEnabled: boolean, - setIsGridNavigationEnabled: (isGridNavigationEnabled: boolean) => void + setFocusedCell: (focusedCell: [number, number]) => void ) { return (event: KeyboardEvent) => { const colCount = visibleColumns.length - 1; @@ -307,46 +305,32 @@ function createKeyDownHandler( const rowCount = computeVisibleRows(props); const { keyCode } = event; - if ( - isGridNavigationEnabled && - (keyCode === keyCodes.ENTER || keyCode === keyCodes.F2) - ) { - setIsGridNavigationEnabled(false); - } else if ( - !isGridNavigationEnabled && - (keyCode === keyCodes.ESCAPE || keyCode === keyCodes.F2) - ) { - setIsGridNavigationEnabled(true); - } - - if (isGridNavigationEnabled) { - switch (keyCode) { - case keyCodes.DOWN: - event.preventDefault(); - if (y < rowCount - 1) { - setFocusedCell([x, y + 1]); - } - break; - case keyCodes.LEFT: - event.preventDefault(); - if (x > 0) { - setFocusedCell([x - 1, y]); - } - break; - case keyCodes.UP: - event.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: - event.preventDefault(); - if (x < colCount) { - setFocusedCell([x + 1, y]); - } - break; - } + switch (keyCode) { + case keyCodes.DOWN: + event.preventDefault(); + if (y < rowCount - 1) { + setFocusedCell([x, y + 1]); + } + break; + case keyCodes.LEFT: + event.preventDefault(); + if (x > 0) { + setFocusedCell([x - 1, y]); + } + break; + case keyCodes.UP: + event.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: + event.preventDefault(); + if (x < colCount) { + setFocusedCell([x + 1, y]); + } + break; } }; } @@ -363,10 +347,6 @@ export const EuiDataGrid: FunctionComponent = props => { // enables/disables grid controls based on available width const onResize = useOnResize(setShowGridControls, isFullScreen); - const [isGridNavigationEnabled, setIsGridNavigationEnabled] = useState< - boolean - >(true); - const handleGridKeyDown = (e: KeyboardEvent) => { switch (e.keyCode) { case keyCodes.ESCAPE: @@ -500,9 +480,7 @@ export const EuiDataGrid: FunctionComponent = props => { props, visibleColumns, focusedCell, - setFocusedCell, - isGridNavigationEnabled, - setIsGridNavigationEnabled + setFocusedCell )} className="euiDataGrid__verticalScroll" ref={resizeRef} @@ -547,7 +525,6 @@ export const EuiDataGrid: FunctionComponent = props => { sorting={sorting} renderCellValue={renderCellValue} rowCount={rowCount} - isGridNavigationEnabled={isGridNavigationEnabled} interactiveCellId={interactiveCellId} />
diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 4a225733129..9bcc4f6b121 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -30,7 +30,6 @@ interface EuiDataGridBodyProps { renderCellValue: EuiDataGridCellProps['renderCellValue']; inMemory?: EuiDataGridInMemory; inMemoryValues: EuiDataGridInMemoryValues; - isGridNavigationEnabled: EuiDataGridCellProps['isGridNavigationEnabled']; interactiveCellId: EuiDataGridCellProps['interactiveCellId']; pagination?: EuiDataGridPaginationProps; sorting?: EuiDataGridSorting; @@ -50,7 +49,6 @@ export const EuiDataGridBody: FunctionComponent< renderCellValue, inMemory, inMemoryValues, - isGridNavigationEnabled, interactiveCellId, pagination, sorting, @@ -154,7 +152,6 @@ export const EuiDataGridBody: FunctionComponent< renderCellValue={renderCellValue} rowIndex={rowIndex} visibleRowIndex={i} - isGridNavigationEnabled={isGridNavigationEnabled} interactiveCellId={interactiveCellId} /> ); @@ -171,7 +168,6 @@ export const EuiDataGridBody: FunctionComponent< rowMap, visibleRowIndices, startRow, - isGridNavigationEnabled, interactiveCellId, ]); diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 94e3ec036c1..ff18f2971d2 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -6,9 +6,7 @@ import React, { ReactNode, createRef, HTMLAttributes, - useState, KeyboardEvent, - useEffect, } from 'react'; import classNames from 'classnames'; // @ts-ignore @@ -19,13 +17,14 @@ import { EuiPopover } from '../popover'; import { CommonProps, Omit } from '../common'; // @ts-ignore import { EuiButtonIcon } from '../button'; -import { EuiMutationObserver } from '../observer/mutation_observer'; import { keyCodes } from '../../services'; export interface CellValueElementProps { rowIndex: number; columnId: string; setCellProps: (props: CommonProps & HTMLAttributes) => void; + isExpandable: boolean; + isExpanded: boolean; } export interface EuiDataGridCellProps { @@ -34,12 +33,10 @@ export interface EuiDataGridCellProps { columnId: string; columnType?: string | null; width?: number; - isFocusable: boolean; + isFocused: boolean; onCellFocus: Function; - isGridNavigationEnabled: boolean; interactiveCellId: string; isExpandable: boolean; - isExpanded?: boolean; renderCellValue: | JSXElementConstructor | ((props: CellValueElementProps) => ReactNode); @@ -52,125 +49,23 @@ interface EuiDataGridCellState { type EuiDataGridCellValueProps = Omit< EuiDataGridCellProps, - | 'width' - | 'isFocusable' - | 'isGridNavigationEnabled' - | 'interactiveCellId' - | 'onCellFocus' + 'width' | 'isFocused' | 'interactiveCellId' | 'onCellFocus' >; const EuiDataGridCellContent: FunctionComponent< EuiDataGridCellValueProps & { setCellProps: CellValueElementProps['setCellProps']; + isExpanded: boolean; } > = memo(props => { - const { renderCellValue, isExpandable, isExpanded, ...rest } = props; - const [popoverIsOpen, setPopoverIsOpen] = useState(isExpanded); - - useEffect(() => { - setPopoverIsOpen(isExpanded); - }, [isExpanded]); + const { renderCellValue, ...rest } = props; - // React is more permissable than the TS types indicate + // React is more permissible than the TS types indicate const CellElement = renderCellValue as JSXElementConstructor< CellValueElementProps >; - const buttonIconClasses = classNames('euiDataGridRowCell__expandButtonIcon', { - 'euiDataGridRowCell__expandButtonIcon-isActive': popoverIsOpen, - }); - - const buttonClasses = classNames('euiDataGridRowCell__expandButton', { - 'euiDataGridRowCell__expandButton-isActive': popoverIsOpen, - }); - - const expandButton = ( - setPopoverIsOpen(!popoverIsOpen)} - title="Expand cell content" - /> - ); - - // TODO: This is temporary. It's mostly just to show that different schema likely will require different - // markup. We also likely will want a way to pass a custom render to the popup and the default cell - // content as part of the data config. - let cellElement: ReactNode; - if (props.columnType === 'json') { - cellElement = ( - /**/ - - // - ); - } else { - cellElement = ( - - - - ); - } - - if (isExpandable) { - const content = ( -
-
- -
-
- {expandButton} -
-
- ); - return ( - setPopoverIsOpen(false)}> - {cellElement} - - ); - } - - return ( -
-
- -
-
- ); - - return ( -
-
- -
- {isExpandable && ( -
- setPopoverIsOpen(false)}> - {cellElement} - -
- )} -
- ); + return ; }); export class EuiDataGridCell extends Component< @@ -185,19 +80,17 @@ export class EuiDataGridCell extends Component< updateFocus() { const cell = this.cellRef.current; - const { isFocusable, isGridNavigationEnabled } = this.props; + const { isFocused } = this.props; - if (cell && isFocusable && isGridNavigationEnabled) { + if (cell && isFocused) { cell.focus(); } } componentDidUpdate(prevProps: EuiDataGridCellProps) { - const didFocusChange = prevProps.isFocusable !== this.props.isFocusable; - const didNavigationChange = - prevProps.isGridNavigationEnabled !== this.props.isGridNavigationEnabled; + const didFocusChange = prevProps.isFocused !== this.props.isFocused; - if (didFocusChange || didNavigationChange) { + if (didFocusChange) { this.updateFocus(); } } @@ -212,15 +105,12 @@ export class EuiDataGridCell extends Component< if (nextProps.width !== this.props.width) return true; if (nextProps.renderCellValue !== this.props.renderCellValue) return true; if (nextProps.onCellFocus !== this.props.onCellFocus) return true; - if (nextProps.isFocusable !== this.props.isFocusable) return true; - if ( - nextProps.isGridNavigationEnabled !== this.props.isGridNavigationEnabled - ) - return true; + if (nextProps.isFocused !== this.props.isFocused) return true; if (nextProps.interactiveCellId !== this.props.interactiveCellId) return true; if (nextState.cellProps !== this.state.cellProps) return true; + if (nextState.popoverIsOpen !== this.state.popoverIsOpen) return true; return false; } @@ -232,9 +122,8 @@ export class EuiDataGridCell extends Component< render() { const { width, - isFocusable, + isFocused, isExpandable, - isGridNavigationEnabled, interactiveCellId, columnType, onCellFocus, @@ -277,37 +166,114 @@ export class EuiDataGridCell extends Component< } }; + const cellContentProps = { + ...rest, + setCellProps: this.setCellProps, + columnType: columnType, + isExpandable, + isExpanded: this.state.popoverIsOpen, + }; + + const buttonIconClasses = classNames( + 'euiDataGridRowCell__expandButtonIcon', + { + 'euiDataGridRowCell__expandButtonIcon-isActive': this.state + .popoverIsOpen, + } + ); + + const buttonClasses = classNames('euiDataGridRowCell__expandButton', { + 'euiDataGridRowCell__expandButton-isActive': this.state.popoverIsOpen, + }); + + const expandButton = ( + + this.setState(({ popoverIsOpen }) => ({ + popoverIsOpen: !popoverIsOpen, + })) + } + title="Expand cell content" + /> + ); + + let anchorContent = ( +
+
+ +
+
+ ); + + if (isExpandable) { + anchorContent = ( +
+
+ +
+
{expandButton}
+
+ ); + } + + let innerContent = anchorContent; + if (isExpandable) { + // TODO: This is temporary. It's mostly just to show that different schema likely will require different + // markup. We also likely will want a way to pass a custom render to the popup and the default cell + // content as part of the data config. + const CellElement = rest.renderCellValue as JSXElementConstructor< + CellValueElementProps + >; + let popoverContent: ReactNode; + if (columnType === 'json') { + popoverContent = ( + /**/ + + // + ); + } else { + popoverContent = ( + + + + ); + } + + innerContent = ( +
+ this.setState({ popoverIsOpen: false })}> + {popoverContent} + +
+ ); + } + return (
onCellFocus([colIndex, rowIndex])}> - { - this.updateFocus(); - }} - observerOptions={{ - childList: true, - subtree: true, - }}> - {ref => ( -
- -
- )} -
+ {innerContent}
); } diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index 51a577164d0..696f8076c36 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -15,7 +15,6 @@ export type EuiDataGridDataRowProps = CommonProps & defaultColumnWidth?: number | null; focusedCell: [number, number]; renderCellValue: EuiDataGridCellProps['renderCellValue']; - isGridNavigationEnabled: EuiDataGridCellProps['isGridNavigationEnabled']; onCellFocus: Function; interactiveCellId: EuiDataGridCellProps['interactiveCellId']; visibleRowIndex: number; @@ -34,7 +33,6 @@ const EuiDataGridDataRow: FunctionComponent< rowIndex, focusedCell, onCellFocus, - isGridNavigationEnabled, interactiveCellId, 'data-test-subj': _dataTestSubj, visibleRowIndex, @@ -53,7 +51,7 @@ const EuiDataGridDataRow: FunctionComponent< const width = columnWidths[id] || defaultColumnWidth; - const isFocusable = + const isFocused = focusedCell[0] === i && focusedCell[1] === visibleRowIndex; return ( @@ -66,8 +64,7 @@ const EuiDataGridDataRow: FunctionComponent< width={width || undefined} renderCellValue={renderCellValue} onCellFocus={onCellFocus} - isFocusable={isFocusable} - isGridNavigationEnabled={isGridNavigationEnabled} + isFocused={isFocused} interactiveCellId={interactiveCellId} isExpandable={isExpandable} /> diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index fa320900585..af805e38394 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -58,7 +58,8 @@ const ObservedCell: FunctionComponent<{ onCellRender: EuiDataGridInMemoryRendererProps['onCellRender']; i: number; column: EuiDataGridColumn; -}> = ({ renderCellValue, i, column, onCellRender }) => { + isExpandable: boolean; +}> = ({ renderCellValue, i, column, onCellRender, isExpandable }) => { const [ref, setRef] = useState(); useEffect(() => { @@ -88,7 +89,13 @@ const ObservedCell: FunctionComponent<{ return (
- +
); }; @@ -114,6 +121,9 @@ export const EuiDataGridInMemoryRenderer: FunctionComponent< return null; } + const isExpandable = + column.isExpandable !== undefined ? column.isExpandable : true; + return ( ); }) diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index d3b9cc38270..db20b7b2a4d 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -310,15 +310,6 @@ export class EuiPopover extends Component { if (this.props.initialFocus != null) { focusTarget = getElementFromInitialFocus(this.props.initialFocus); - if (focusTarget) { - // there's a race condition between the popover content becoming visible and this function call - // if the element isn't visible yet (due to css styling) then it can't accept focus - // so wait for another render and try again - const visibility = window.getComputedStyle(focusTarget).visibility; - if (visibility === 'hidden') { - this.updateFocus(); - } - } } else { const tabbableItems = tabbable(this.panel); if (tabbableItems.length) { @@ -326,6 +317,30 @@ export class EuiPopover extends Component { } } + // there's a race condition between the popover content becoming visible and this function call + // if the element isn't visible yet (due to css styling) then it can't accept focus + // so wait for another render and try again + if (focusTarget == null) { + // there isn't a focus target, one of two reasons: + // #1 is the whole panel hidden? If so, schedule another check + // #2 panel is visible but no tabbables exist, move focus to the panel + const panelVisibility = window.getComputedStyle(this.panel).visibility; + if (panelVisibility === 'hidden') { + // #1 + this.updateFocus(); + } else { + // #2 + focusTarget = this.panel; + } + } else { + // found an element to focus, but is it visible? + const visibility = window.getComputedStyle(focusTarget).visibility; + if (visibility === 'hidden') { + // not visible, check again next render frame + this.updateFocus(); + } + } + if (focusTarget != null) focusTarget.focus(); }); } From d35e459ffb6b51cf41f599ba2c424824e14d3e4b Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 3 Oct 2019 15:44:57 -0600 Subject: [PATCH 12/18] Restore focus to grid cell when popover was in response to mouse click --- src/components/datagrid/data_grid_cell.tsx | 7 ++++--- src/components/popover/popover.tsx | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index ff18f2971d2..c187e103e2a 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -78,14 +78,14 @@ export class EuiDataGridCell extends Component< popoverIsOpen: false, }; - updateFocus() { + updateFocus = () => { const cell = this.cellRef.current; const { isFocused } = this.props; if (cell && isFocused) { cell.focus(); } - } + }; componentDidUpdate(prevProps: EuiDataGridCellProps) { const didFocusChange = prevProps.isFocused !== this.props.isFocused; @@ -257,7 +257,8 @@ export class EuiDataGridCell extends Component< ownFocus panelClassName="euiDataGridRowCell__popover" zIndex={2000} - closePopover={() => this.setState({ popoverIsOpen: false })}> + closePopover={() => this.setState({ popoverIsOpen: false })} + onTrapDeactivation={this.updateFocus}> {popoverContent}
diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index db20b7b2a4d..37cab77a4ed 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -10,6 +10,7 @@ import tabbable from 'tabbable'; import { CommonProps, NoArgCallback, RefCallback } from '../common'; import { FocusTarget, EuiFocusTrap } from '../focus_trap'; +import { Props as ReactFocusLockProps } from 'react-focus-lock'; // eslint-disable-line import/named import { cascadingMenuKeyCodes, @@ -107,6 +108,11 @@ export interface EuiPopoverProps { /** By default, popover content inherits the z-index of the anchor * component; pass zIndex to override */ zIndex?: number; + + /** + * Function callback for when the focus trap is deactivated + */ + onTrapDeactivation?: ReactFocusLockProps['onDeactivation']; } type AnchorPosition = 'up' | 'right' | 'down' | 'left'; @@ -554,6 +560,7 @@ export class EuiPopover extends Component { initialFocus, attachToAnchor, display, + onTrapDeactivation, ...rest } = this.props; @@ -621,6 +628,7 @@ export class EuiPopover extends Component { returnFocus={!this.state.isOpening} // Ignore temporary state of indecisive focus clickOutsideDisables={true} initialFocus={initialFocus} + onDeactivation={onTrapDeactivation} disabled={!ownFocus}> {focusTrapScreenReaderText} Date: Fri, 4 Oct 2019 08:44:56 -0600 Subject: [PATCH 13/18] Allow grid column selection to be searchable --- src/components/datagrid/column_selector.tsx | 42 +++++++++++++++++---- src/components/drag_and_drop/draggable.tsx | 6 ++- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/components/datagrid/column_selector.tsx b/src/components/datagrid/column_selector.tsx index aed500572c6..47d6bafba99 100644 --- a/src/components/datagrid/column_selector.tsx +++ b/src/components/datagrid/column_selector.tsx @@ -1,4 +1,10 @@ -import React, { Fragment, useState, ReactChild, ReactElement } from 'react'; +import React, { + Fragment, + useState, + ReactChild, + ReactElement, + ChangeEvent, +} from 'react'; import classNames from 'classnames'; import { EuiDataGridColumn } from './data_grid_types'; // @ts-ignore-next-line @@ -50,10 +56,17 @@ export const useColumnSelector = ( const numberOfHiddenFields = availableColumns.length - visibleColumns.length; + const [columnSearchText, setColumnSearchText] = useState(''); + const controlBtnClasses = classNames('euiDataGrid__controlBtn', { 'euiDataGrid__controlBtn--active': numberOfHiddenFields > 0, }); + const filteredColumns = sortedColumns.filter( + ({ id }) => id.toLowerCase().indexOf(columnSearchText.toLowerCase()) !== -1 + ); + const isDragEnabled = columnSearchText.length === 0; // only allow drag-and-drop when not filtering columns + const columnSelector = ( ) => + setColumnSearchText(e.currentTarget.value) + } /> - {sortedColumns.map(({ id }, index) => ( - + {filteredColumns.map(({ id }, index) => ( + {(provided, state) => (
- -
- -
-
+ {isDragEnabled && ( + +
+ +
+
+ )}
)} diff --git a/src/components/drag_and_drop/draggable.tsx b/src/components/drag_and_drop/draggable.tsx index 87398886a51..74544e25979 100644 --- a/src/components/drag_and_drop/draggable.tsx +++ b/src/components/drag_and_drop/draggable.tsx @@ -56,7 +56,11 @@ export const EuiDraggable: FunctionComponent = ({ const { cloneItems } = useContext(EuiDroppableContext); return ( - + {(provided, snapshot) => { const classes = classNames( 'euiDraggable', From 59afd89b40d2d6b0b79aa39f5564cb25552d5d4d Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 4 Oct 2019 12:43:03 -0600 Subject: [PATCH 14/18] Refactor expansion popover formatting, allow custom ones --- src-docs/src/views/datagrid/schema.js | 46 ++++++------- src/components/datagrid/data_grid.tsx | 4 ++ src/components/datagrid/data_grid_body.tsx | 68 +++++++++++++++++++ src/components/datagrid/data_grid_cell.tsx | 39 ++++------- .../datagrid/data_grid_data_row.tsx | 23 ++++++- src/components/datagrid/data_grid_types.ts | 6 ++ 6 files changed, 133 insertions(+), 53 deletions(-) diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 652943f2340..91df66542b9 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -57,32 +57,26 @@ for (let i = 1; i < 5; i++) { date: fake('{{date.past}}'), account: fake('{{finance.account}}'), amount: fake('${{finance.amount}}'), - json: JSON.stringify( - [ - { - name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), - email: fake('{{internet.email}}'), - date: fake('{{date.past}}'), - account: fake('{{finance.account}}'), - amount: fake('${{finance.amount}}'), - version: fake('{{system.semver}}'), - friends: [ - { - name: fake( - '{{name.lastName}}, {{name.firstName}} {{name.suffix}}' - ), - email: fake('{{internet.email}}'), - date: fake('{{date.past}}'), - account: fake('{{finance.account}}'), - amount: fake('${{finance.amount}}'), - version: fake('{{system.semver}}'), - }, - ], - }, - ], - null, - 2 - ), + json: JSON.stringify([ + { + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: fake('{{internet.email}}'), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('${{finance.amount}}'), + version: fake('{{system.semver}}'), + friends: [ + { + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: fake('{{internet.email}}'), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('${{finance.amount}}'), + version: fake('{{system.semver}}'), + }, + ], + }, + ]), version: fake('{{system.semver}}'), }); } diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 37b6ee47226..7c4bdfc9e43 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -25,6 +25,7 @@ import { EuiDataGridStyleFontSizes, EuiDataGridStyleHeader, EuiDataGridStyleRowHover, + EuiDataGridExpansionFormatters, } from './data_grid_types'; import { EuiDataGridCellProps } from './data_grid_cell'; // @ts-ignore-next-line @@ -52,6 +53,7 @@ type CommonGridProps = CommonProps & HTMLAttributes & { columns: EuiDataGridColumn[]; schemaDetectors?: SchemaDetector[]; + expansionFormatters?: EuiDataGridExpansionFormatters; rowCount: number; renderCellValue: EuiDataGridCellProps['renderCellValue']; gridStyle?: EuiDataGridStyle; @@ -368,6 +370,7 @@ export const EuiDataGrid: FunctionComponent = props => { pagination, sorting, inMemory, + expansionFormatters, ...rest } = props; @@ -519,6 +522,7 @@ export const EuiDataGrid: FunctionComponent = props => { inMemory={inMemory} columns={visibleColumns} schema={mergedSchema} + expansionFormatters={expansionFormatters} focusedCell={focusedCell} onCellFocus={setFocusedCell} pagination={pagination} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 9bcc4f6b121..c081658dbc0 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -2,11 +2,17 @@ import React, { Fragment, FunctionComponent, useCallback, + useEffect, useMemo, + useRef, + useState, } from 'react'; +// @ts-ignore-next-line +import { EuiCodeBlock } from '../code'; import { EuiDataGridColumn, EuiDataGridColumnWidths, + EuiDataGridExpansionFormatters, EuiDataGridInMemory, EuiDataGridInMemoryValues, EuiDataGridPaginationProps, @@ -18,12 +24,14 @@ import { EuiDataGridDataRowProps, } from './data_grid_data_row'; import { EuiDataGridSchema } from './data_grid_schema'; +import { useInnerText } from '../inner_text'; interface EuiDataGridBodyProps { columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; + expansionFormatters?: EuiDataGridExpansionFormatters; focusedCell: EuiDataGridDataRowProps['focusedCell']; onCellFocus: EuiDataGridDataRowProps['onCellFocus']; rowCount: number; @@ -35,6 +43,57 @@ interface EuiDataGridBodyProps { sorting?: EuiDataGridSorting; } +const providedExpansionFormatters: EuiDataGridExpansionFormatters = { + json: ({ children }) => { + const invisibleRef = useRef(null); + const [ref, text] = useInnerText(); + const [isVisible, setIsVisible] = useState(false); + const formattedText = useMemo(() => { + if (text) { + try { + return JSON.stringify(JSON.parse(text), null, 2); + } catch (e) { + return text; + } + } else { + return ''; + } + }, [text]); + + useEffect(() => { + // because this content renders into a popover + // it is hidden until the popover positions into place + // but InnerText cannot inspect hidden elements, so wait + function checkVisibility() { + const style = window.getComputedStyle(invisibleRef.current!); + if (style.getPropertyValue('visibility') !== 'hidden') { + setIsVisible(true); + } else { + requestAnimationFrame(checkVisibility); + } + } + requestAnimationFrame(checkVisibility); + }, []); + + return ( + + {!formattedText && ( +
{children}
+ )} + {formattedText && ( + + {formattedText} + + )} +
+ ); + }, +}; + export const EuiDataGridBody: FunctionComponent< EuiDataGridBodyProps > = props => { @@ -43,6 +102,7 @@ export const EuiDataGridBody: FunctionComponent< defaultColumnWidth, columns, schema, + expansionFormatters, focusedCell, onCellFocus, rowCount, @@ -140,11 +200,17 @@ export const EuiDataGridBody: FunctionComponent< rowIndex = rowMap[rowIndex]; } + const mergedExpansionFormatters = { + ...providedExpansionFormatters, + ...expansionFormatters, + }; + rows.push( | ((props: CellValueElementProps) => ReactNode); @@ -49,7 +47,11 @@ interface EuiDataGridCellState { type EuiDataGridCellValueProps = Omit< EuiDataGridCellProps, - 'width' | 'isFocused' | 'interactiveCellId' | 'onCellFocus' + | 'width' + | 'isFocused' + | 'interactiveCellId' + | 'onCellFocus' + | 'expansionFormatter' >; const EuiDataGridCellContent: FunctionComponent< @@ -108,6 +110,8 @@ export class EuiDataGridCell extends Component< if (nextProps.isFocused !== this.props.isFocused) return true; if (nextProps.interactiveCellId !== this.props.interactiveCellId) return true; + if (nextProps.expansionFormatter !== this.props.expansionFormatter) + return true; if (nextState.cellProps !== this.state.cellProps) return true; if (nextState.popoverIsOpen !== this.state.popoverIsOpen) return true; @@ -124,6 +128,7 @@ export class EuiDataGridCell extends Component< width, isFocused, isExpandable, + expansionFormatter: ExpansionFormatter, interactiveCellId, columnType, onCellFocus, @@ -223,30 +228,14 @@ export class EuiDataGridCell extends Component< let innerContent = anchorContent; if (isExpandable) { - // TODO: This is temporary. It's mostly just to show that different schema likely will require different - // markup. We also likely will want a way to pass a custom render to the popup and the default cell - // content as part of the data config. const CellElement = rest.renderCellValue as JSXElementConstructor< CellValueElementProps >; - let popoverContent: ReactNode; - if (columnType === 'json') { - popoverContent = ( - /**/ + const popoverContent = ( + - // - ); - } else { - popoverContent = ( - - - - ); - } + + ); innerContent = (
diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index 696f8076c36..eb445a0a5fc 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -1,16 +1,23 @@ import React, { FunctionComponent, HTMLAttributes } from 'react'; import classnames from 'classnames'; -import { EuiDataGridColumn, EuiDataGridColumnWidths } from './data_grid_types'; +import { + EuiDataGridColumn, + EuiDataGridColumnWidths, + EuiDataGridExpansionFormatter, + EuiDataGridExpansionFormatters, +} from './data_grid_types'; import { CommonProps } from '../common'; import { EuiDataGridCell, EuiDataGridCellProps } from './data_grid_cell'; import { EuiDataGridSchema } from './data_grid_schema'; +import { EuiText } from '../text'; export type EuiDataGridDataRowProps = CommonProps & HTMLAttributes & { rowIndex: number; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; + expansionFormatters: EuiDataGridExpansionFormatters; columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; focusedCell: [number, number]; @@ -20,12 +27,19 @@ export type EuiDataGridDataRowProps = CommonProps & visibleRowIndex: number; }; +const DefaultColumnFormatter: EuiDataGridExpansionFormatter = ({ + children, +}) => { + return {children}; +}; + const EuiDataGridDataRow: FunctionComponent< EuiDataGridDataRowProps > = props => { const { columns, schema, + expansionFormatters, columnWidths, defaultColumnWidth, className, @@ -46,8 +60,12 @@ const EuiDataGridDataRow: FunctionComponent<
{columns.map((props, i) => { const { id } = props; + const columnType = schema[id] ? schema[id].columnType : null; + const isExpandable = props.isExpandable !== undefined ? props.isExpandable : true; + const expansionFormatter = + expansionFormatters[columnType as string] || DefaultColumnFormatter; const width = columnWidths[id] || defaultColumnWidth; @@ -60,7 +78,8 @@ const EuiDataGridDataRow: FunctionComponent< rowIndex={rowIndex} colIndex={i} columnId={id} - columnType={schema[id] ? schema[id].columnType : null} + columnType={columnType} + expansionFormatter={expansionFormatter} width={width || undefined} renderCellValue={renderCellValue} onCellFocus={onCellFocus} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 6a123b03d4e..d9f5525b3b4 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -1,4 +1,5 @@ import { EuiDataGridSchema } from './data_grid_schema'; +import { FunctionComponent } from 'react'; export interface EuiDataGridColumn { id: string; @@ -65,3 +66,8 @@ export interface EuiDataGridInMemory { export interface EuiDataGridInMemoryValues { [key: string]: { [key: string]: string }; } + +export type EuiDataGridExpansionFormatter = FunctionComponent<{}>; +export interface EuiDataGridExpansionFormatters { + [key: string]: EuiDataGridExpansionFormatter; +} From 78332de3a8b6f85db3f090149d40323912402d22 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 7 Oct 2019 13:31:15 -0600 Subject: [PATCH 15/18] schema-based sort comparators --- src/components/datagrid/data_grid.test.tsx | 28 +++++++ src/components/datagrid/data_grid.tsx | 9 +- src/components/datagrid/data_grid_body.tsx | 36 +++++++- src/components/datagrid/data_grid_schema.ts | 93 ++++++++++++++++----- 4 files changed, 138 insertions(+), 28 deletions(-) diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 3187a1b2a76..d51823de638 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -1045,6 +1045,34 @@ Array [ ]); }); }); + + it('uses schema information to sort', () => { + const component = mount( + + // render A 0->4 and B 12->8 + columnId === 'A' ? rowIndex : 12 - rowIndex + } + inMemory={{ level: 'sorting' }} + sorting={{ + columns: [{ id: 'B', direction: 'asc' }], + onSort: () => {}, + }} + /> + ); + + expect(extractGridData(component)).toEqual([ + ['A', 'B'], + ['4', '8'], + ['3', '9'], + ['2', '10'], + ['1', '11'], + ['0', '12'], + ]); + }); }); describe('keyboard controls', () => { diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 7c4bdfc9e43..4d75df17873 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -7,6 +7,7 @@ import React, { useEffect, Fragment, ReactChild, + useMemo, } from 'react'; import classNames from 'classnames'; import { EuiI18n } from '../i18n'; @@ -44,6 +45,7 @@ import { getMergedSchema, SchemaDetector, useDetectSchema, + schemaDetectors as providedSchemaDetectors, } from './data_grid_schema'; // When below this number the grid only shows the full screen button @@ -412,9 +414,13 @@ export const EuiDataGrid: FunctionComponent = props => { const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory, rowCount); + const allSchemaDetetors = useMemo( + () => [...providedSchemaDetectors, ...(schemaDetectors || [])], + [schemaDetectors] + ); const detectedSchema = useDetectSchema( inMemoryValues, - schemaDetectors, + allSchemaDetetors, inMemory != null ); const mergedSchema = getMergedSchema(detectedSchema, columns); @@ -522,6 +528,7 @@ export const EuiDataGrid: FunctionComponent = props => { inMemory={inMemory} columns={visibleColumns} schema={mergedSchema} + schemaDetectors={allSchemaDetetors} expansionFormatters={expansionFormatters} focusedCell={focusedCell} onCellFocus={setFocusedCell} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index c081658dbc0..9881a09bf3d 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -23,7 +23,7 @@ import { EuiDataGridDataRow, EuiDataGridDataRowProps, } from './data_grid_data_row'; -import { EuiDataGridSchema } from './data_grid_schema'; +import { EuiDataGridSchema, SchemaDetector } from './data_grid_schema'; import { useInnerText } from '../inner_text'; interface EuiDataGridBodyProps { @@ -31,6 +31,7 @@ interface EuiDataGridBodyProps { defaultColumnWidth?: number | null; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; + schemaDetectors: SchemaDetector[]; expansionFormatters?: EuiDataGridExpansionFormatters; focusedCell: EuiDataGridDataRowProps['focusedCell']; onCellFocus: EuiDataGridDataRowProps['onCellFocus']; @@ -43,6 +44,16 @@ interface EuiDataGridBodyProps { sorting?: EuiDataGridSorting; } +const defaultComparator: NonNullable = ( + a, + b, + direction +) => { + if (a < b) return direction === 'asc' ? -1 : 1; + if (a > b) return direction === 'asc' ? 1 : -1; + return 0; +}; + const providedExpansionFormatters: EuiDataGridExpansionFormatters = { json: ({ children }) => { const invisibleRef = useRef(null); @@ -102,6 +113,7 @@ export const EuiDataGridBody: FunctionComponent< defaultColumnWidth, columns, schema, + schemaDetectors, expansionFormatters, focusedCell, onCellFocus, @@ -153,8 +165,24 @@ export const EuiDataGridBody: FunctionComponent< const aValue = a.values[column.id]; const bValue = b.values[column.id]; - if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; - if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + // get the comparator, based on schema + let comparator = defaultComparator; + if (schema.hasOwnProperty(column.id)) { + const columnType = schema[column.id].columnType; + for (let i = 0; i < schemaDetectors.length; i++) { + const detector = schemaDetectors[i]; + if ( + detector.type === columnType && + detector.hasOwnProperty('comparator') + ) { + comparator = detector.comparator!; + } + } + } + + const result = comparator(aValue, bValue, column.direction); + // only return if the columns are inequal, otherwise allow the next sort-by column to run + if (result !== 0) return result; } return 0; @@ -166,7 +194,7 @@ export const EuiDataGridBody: FunctionComponent< } return rowMap; - }, [sorting, inMemory, inMemoryValues]); + }, [sorting, inMemory, inMemoryValues, schema, schemaDetectors]); const setCellFocus = useCallback( ([colIndex, rowIndex]) => { diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index da111353d30..bba5c5a105d 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -7,18 +7,42 @@ import { export interface SchemaDetector { type: string; detector: (value: string) => number; + comparator?: (a: string, b: string, direction: 'asc' | 'desc') => -1 | 0 | 1; } -const schemaDetectors: SchemaDetector[] = [ +const numericChars = new Set([ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + '-', +]); +export const schemaDetectors: SchemaDetector[] = [ { type: 'boolean', - detector(value: string) { - return value === 'true' || value === 'false' ? 1 : 0; + detector(value) { + return value.toLowerCase() === 'true' || value.toLowerCase() === 'false' + ? 1 + : 0; + }, + comparator(a, b, direction) { + const aValue = a.toLowerCase() === 'true'; + const bValue = b.toLowerCase() === 'true'; + if (aValue < bValue) return direction === 'asc' ? -1 : 1; + if (aValue > bValue) return direction === 'asc' ? 1 : -1; + return 0; }, }, { type: 'currency', - detector(value: string) { + detector(value) { const matchLength = (value.match( // currency prefers starting with 1-3 characters for the currency symbol // then it matches against numerical data + $ @@ -31,10 +55,21 @@ const schemaDetectors: SchemaDetector[] = [ return (matchLength / value.length) * confidenceAdjustment || 0; }, + comparator: (a, b, direction) => { + const aChars = a.split('').filter(char => numericChars.has(char)); + const aValue = parseFloat(aChars.join('')); + + const bChars = b.split('').filter(char => numericChars.has(char)); + const bValue = parseFloat(bChars.join('')); + + if (aValue < bValue) return direction === 'asc' ? -1 : 1; + if (aValue > bValue) return direction === 'asc' ? 1 : -1; + return 0; + }, }, { type: 'datetime', - detector(value: string) { + detector(value) { // matches the most common forms of ISO-8601 const isoTimestampMatch = value.match( // 2019 - 09 - 17 T 12 : 18 : 32 .853 Z or -0600 @@ -59,15 +94,26 @@ const schemaDetectors: SchemaDetector[] = [ }, { type: 'numeric', - detector(value: string) { + detector(value) { const matchLength = (value.match(/[%-(]*[\d,]+(\.\d*)?[%)]*/) || [''])[0] .length; return matchLength / value.length || 0; }, + comparator: (a, b, direction) => { + const aChars = a.split('').filter(char => numericChars.has(char)); + const aValue = parseFloat(aChars.join('')); + + const bChars = b.split('').filter(char => numericChars.has(char)); + const bValue = parseFloat(bChars.join('')); + + if (aValue < bValue) return direction === 'asc' ? -1 : 1; + if (aValue > bValue) return direction === 'asc' ? 1 : -1; + return 0; + }, }, { type: 'json', - detector(value: string) { + detector(value) { // does this look like it might be a JSON object? const maybeArray = value[0] === '[' && value[value.length - 1] === ']'; const maybeObject = value[0] === '{' && value[value.length - 1] === '}'; @@ -94,13 +140,12 @@ interface SchemaTypeScore { function scoreValueBySchemaType( value: string, - extraSchemaDetectors: SchemaDetector[] = [] + schemaDetectors: SchemaDetector[] = [] ) { const scores: SchemaTypeScore[] = []; - const detectors = [...schemaDetectors, ...extraSchemaDetectors]; - for (let i = 0; i < detectors.length; i++) { - const { type, detector } = detectors[i]; + for (let i = 0; i < schemaDetectors.length; i++) { + const { type, detector } = schemaDetectors[i]; const score = detector(value); scores.push({ type, score }); } @@ -217,7 +262,7 @@ export function useDetectSchema( }, {} ); - }, [inMemoryValues]); + }, [inMemoryValues, schemaDetectors]); return schema; } @@ -225,18 +270,20 @@ export function getMergedSchema( detectedSchema: EuiDataGridSchema, columns: EuiDataGridColumn[] ) { - const mergedSchema = { ...detectedSchema }; - - for (let i = 0; i < columns.length; i++) { - const { id, dataType } = columns[i]; - if (dataType != null) { - if (detectedSchema.hasOwnProperty(id)) { - mergedSchema[id] = { ...detectedSchema[id], columnType: dataType }; - } else { - mergedSchema[id] = { columnType: dataType }; + return useMemo(() => { + const mergedSchema = { ...detectedSchema }; + + for (let i = 0; i < columns.length; i++) { + const { id, dataType } = columns[i]; + if (dataType != null) { + if (detectedSchema.hasOwnProperty(id)) { + mergedSchema[id] = { ...detectedSchema[id], columnType: dataType }; + } else { + mergedSchema[id] = { columnType: dataType }; + } } } - } - return mergedSchema; + return mergedSchema; + }, [detectedSchema, columns]); } From b709d8c8fe397696d8ac6de27b273ef62fefdbc9 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Fri, 11 Oct 2019 09:27:10 -0700 Subject: [PATCH 16/18] reverse boolean sort to be true-false --- src/components/datagrid/data_grid_schema.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/datagrid/data_grid_schema.tsx b/src/components/datagrid/data_grid_schema.tsx index e205e8cf761..6e024718366 100644 --- a/src/components/datagrid/data_grid_schema.tsx +++ b/src/components/datagrid/data_grid_schema.tsx @@ -43,8 +43,8 @@ export const schemaDetectors: SchemaDetector[] = [ comparator(a, b, direction) { const aValue = a.toLowerCase() === 'true'; const bValue = b.toLowerCase() === 'true'; - if (aValue < bValue) return direction === 'asc' ? -1 : 1; - if (aValue > bValue) return direction === 'asc' ? 1 : -1; + if (aValue < bValue) return direction === 'asc' ? 1 : -1; + if (aValue > bValue) return direction === 'asc' ? -1 : 1; return 0; }, icon: 'invert', @@ -52,13 +52,13 @@ export const schemaDetectors: SchemaDetector[] = [ sortTextAsc: ( ), sortTextDesc: ( ), }, From 084c31bc03aa90228809cce98260d2bde0e5213b Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Fri, 11 Oct 2019 10:25:14 -0700 Subject: [PATCH 17/18] adds json schema sorting, fixes issue with popover --- src-docs/src/views/datagrid/schema.js | 43 +++-- src/components/datagrid/column_sorting.tsx | 192 ++++++++++--------- src/components/datagrid/data_grid_schema.tsx | 5 + 3 files changed, 131 insertions(+), 109 deletions(-) diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 91df66542b9..ac6e4997f14 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -43,21 +43,9 @@ const columns = [ const data = []; for (let i = 1; i < 5; i++) { - data.push({ - name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), - email: {fake('{{internet.email}}')}, - location: ( - - {`${fake('{{address.city}}')}, `} - - {fake('{{address.country}}')} - - - ), - date: fake('{{date.past}}'), - account: fake('{{finance.account}}'), - amount: fake('${{finance.amount}}'), - json: JSON.stringify([ + let json; + if (i < 3) { + json = JSON.stringify([ { name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), email: fake('{{internet.email}}'), @@ -76,7 +64,30 @@ for (let i = 1; i < 5; i++) { }, ], }, - ]), + ]); + } else { + json = JSON.stringify([ + { + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + }, + ]); + } + + data.push({ + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: {fake('{{internet.email}}')}, + location: ( + + {`${fake('{{address.city}}')}, `} + + {fake('{{address.country}}')} + + + ), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('${{finance.amount}}'), + json: json, version: fake('{{system.semver}}'), }); } diff --git a/src/components/datagrid/column_sorting.tsx b/src/components/datagrid/column_sorting.tsx index eab6b2bb639..898d00d0128 100644 --- a/src/components/datagrid/column_sorting.tsx +++ b/src/components/datagrid/column_sorting.tsx @@ -149,104 +149,110 @@ export const useColumnSorting = (

)} - - - - setAvailableColumnsIsOpen(false)} - anchorPosition="downLeft" - ownFocus - panelPaddingSize="s" - button={ + {(inactiveColumns.length > 0 || sorting.columns.length > 0) && ( + + + + {inactiveColumns.length > 0 && ( + setAvailableColumnsIsOpen(false)} + anchorPosition="downLeft" + ownFocus + panelPaddingSize="s" + button={ + + setAvailableColumnsIsOpen(!avilableColumnsisOpen) + }> + + + }> + + {(sortFieldAriaLabel: string) => ( +
+ {inactiveColumns.map(({ id }) => ( + + ))} +
+ )} +
+
+ )} +
+ {sorting.columns.length > 0 ? ( + - setAvailableColumnsIsOpen(!avilableColumnsisOpen) - }> + flush="right" + onClick={() => sorting.onSort([])}> - }> - - {(sortFieldAriaLabel: string) => ( -
- {inactiveColumns.map(({ id }) => ( - - ))} -
- )} -
-
-
- {sorting.columns.length > 0 ? ( - - sorting.onSort([])}> - - - - ) : null} -
-
+ + ) : null} + + + )} ); diff --git a/src/components/datagrid/data_grid_schema.tsx b/src/components/datagrid/data_grid_schema.tsx index 6e024718366..d9cf7dda4bc 100644 --- a/src/components/datagrid/data_grid_schema.tsx +++ b/src/components/datagrid/data_grid_schema.tsx @@ -181,6 +181,11 @@ export const schemaDetectors: SchemaDetector[] = [ return 0; } }, + comparator: (a, b, direction) => { + if (a.length > b.length) return direction === 'asc' ? 1 : -1; + if (a.length < b.length) return direction === 'asc' ? 1 : -1; + return 0; + }, icon: 'visVega', color: palettes.euiPaletteColorBlind.colors[3], sortTextAsc: ( From 64266e793de7f51e0edde3055539ed850d97ae91 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 11 Oct 2019 12:53:46 -0600 Subject: [PATCH 18/18] Weaken the currency type detector when values have a period in their first few characters, and fix test --- src/components/datagrid/data_grid.test.tsx | 29 +++++++++++--------- src/components/datagrid/data_grid_schema.tsx | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 3c7d3112d0a..ad6b41027f3 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -116,21 +116,24 @@ function getColumnSortDirection( columnSelectionPopover = datagrid.find( 'EuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' ); - expect(columnSelectionPopover).euiPopoverToBeOpen(); + // popover will go away if all of the columns are selected + if (columnSelectionPopover.length > 0) { + expect(columnSelectionPopover).euiPopoverToBeOpen(); + + popoverButton = columnSelectionPopover + .find('div[className="euiPopover__anchor"]') + .find('[onClick]') + .first(); + // @ts-ignore-next-line + act(() => popoverButton.props().onClick()); - popoverButton = columnSelectionPopover - .find('div[className="euiPopover__anchor"]') - .find('[onClick]') - .first(); - // @ts-ignore-next-line - act(() => popoverButton.props().onClick()); + datagrid.update(); - datagrid.update(); - - columnSelectionPopover = datagrid.find( - 'EuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' - ); - expect(columnSelectionPopover).not.euiPopoverToBeOpen(); + columnSelectionPopover = datagrid.find( + 'EuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' + ); + expect(columnSelectionPopover).not.euiPopoverToBeOpen(); + } // find the column sorter columnSelectionPopover = datagrid.find( diff --git a/src/components/datagrid/data_grid_schema.tsx b/src/components/datagrid/data_grid_schema.tsx index d9cf7dda4bc..43d0673475e 100644 --- a/src/components/datagrid/data_grid_schema.tsx +++ b/src/components/datagrid/data_grid_schema.tsx @@ -68,7 +68,7 @@ export const schemaDetectors: SchemaDetector[] = [ const matchLength = (value.match( // currency prefers starting with 1-3 characters for the currency symbol // then it matches against numerical data + $ - /(^[^-(]{1,3})?[$-(]*[\d,]+(\.\d*)?[$)]*/ + /(^[^-(.]{1,3})?[$-(]*[\d,]+(\.\d*)?[$)]*/ ) || [''])[0].length; // if there is no currency symbol then reduce the score