From 5bca79afe7d54a74613ff30d060ad305f4daed7a Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 7 Feb 2020 15:07:41 -0700 Subject: [PATCH 01/13] Added action column type to EuiDataGrid; created a row selection demo --- src-docs/src/routes.js | 2 + .../datagrid/datagrid_selection_example.js | 48 ++++ src-docs/src/views/datagrid/selection.js | 143 ++++++++++ src/components/datagrid/data_grid.tsx | 8 +- .../datagrid/data_grid_action_header_cell.tsx | 173 ++++++++++++ src/components/datagrid/data_grid_body.tsx | 5 + .../datagrid/data_grid_data_row.tsx | 28 ++ .../datagrid/data_grid_header_cell.tsx | 233 +++++++++++++++++ .../datagrid/data_grid_header_row.tsx | 246 ++---------------- src/components/datagrid/data_grid_types.ts | 14 +- src/components/datagrid/index.ts | 7 +- 11 files changed, 670 insertions(+), 237 deletions(-) create mode 100644 src-docs/src/views/datagrid/datagrid_selection_example.js create mode 100644 src-docs/src/views/datagrid/selection.js create mode 100644 src/components/datagrid/data_grid_action_header_cell.tsx create mode 100644 src/components/datagrid/data_grid_header_cell.tsx diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 80ff1d72b9d..07b927f201a 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -79,6 +79,7 @@ import { DataGridExample } from './views/datagrid/datagrid_example'; import { DataGridMemoryExample } from './views/datagrid/datagrid_memory_example'; import { DataGridSchemaExample } from './views/datagrid/datagrid_schema_example'; import { DataGridStylingExample } from './views/datagrid/datagrid_styling_example'; +import { DataGridSelectionExample } from './views/datagrid/datagrid_selection_example'; import { DatePickerExample } from './views/date_picker/date_picker_example'; @@ -341,6 +342,7 @@ const navigation = [ DataGridMemoryExample, DataGridSchemaExample, DataGridStylingExample, + DataGridSelectionExample, TableExample, ].map(example => createExample(example)), }, diff --git a/src-docs/src/views/datagrid/datagrid_selection_example.js b/src-docs/src/views/datagrid/datagrid_selection_example.js new file mode 100644 index 00000000000..2e526c06f2f --- /dev/null +++ b/src-docs/src/views/datagrid/datagrid_selection_example.js @@ -0,0 +1,48 @@ +import React, { Fragment } from 'react'; + +import { renderToHtml } from '../../services'; + +import { GuideSectionTypes } from '../../components'; +import { EuiDataGrid, EuiCodeBlock } from '../../../../src/components'; + +import DataGridSelection from './selection'; +const dataGridSelectionSource = require('!!raw-loader!./selection'); +const dataGridSelectionHtml = renderToHtml(DataGridSelection); + +const gridSnippet = ` +`; + +export const DataGridSelectionExample = { + title: 'Data grid row selection', + sections: [ + { + source: [ + { + type: GuideSectionTypes.JS, + code: dataGridSelectionSource, + }, + { + type: GuideSectionTypes.HTML, + code: dataGridSelectionHtml, + }, + ], + text: ( + +

Blurb about selection API.

+ + {gridSnippet} + +
+ ), + components: { DataGridSelection }, + + props: { + EuiDataGrid, + }, + demo: , + }, + ], +}; diff --git a/src-docs/src/views/datagrid/selection.js b/src-docs/src/views/datagrid/selection.js new file mode 100644 index 00000000000..c51b44f69c7 --- /dev/null +++ b/src-docs/src/views/datagrid/selection.js @@ -0,0 +1,143 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { fake } from 'faker'; + +import { + EuiDataGrid, + EuiAvatar, + EuiCheckbox, +} from '../../../../src/components/'; + +const columns = [ + { + id: 'avatar', + initialWidth: 65, + }, + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'city', + }, + { + id: 'country', + }, + { + id: 'account', + }, +]; + +const data = []; + +for (let i = 1; i < 500; i++) { + data.push({ + avatar: ( + + ), + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: fake('{{internet.email}}'), + city: fake('{{address.city}}'), + country: fake('{{address.country}}'), + account: fake('{{finance.account}}'), + }); +} + +export default function DataGrid() { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 15, + }); + const setPageIndex = useCallback( + pageIndex => setPagination({ ...pagination, pageIndex }), + [pagination, setPagination] + ); + const setPageSize = useCallback( + pageSize => setPagination({ ...pagination, pageSize }), + [pagination, setPagination] + ); + + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }) => id) + ); + + const [selectedRows, setSelectedRows] = useState(new Set()); + + const leadingColumns = useMemo( + () => [ + { + id: 'selection', + width: 31, + headerCellRender: () => { + const isIndeterminate = + selectedRows.size > 0 && selectedRows.size < data.length; + return ( + 0} + onChange={e => { + if (isIndeterminate) { + // clear selection + setSelectedRows(new Set()); + } else { + if (e.target.checked) { + // select everything + setSelectedRows(new Set(data.map((_, index) => index))); + } else { + // clear selection + setSelectedRows(new Set()); + } + } + }} + /> + ); + }, + rowCellRender: ({ rowIndex }) => ( + { + const nextSelectedRows = new Set(selectedRows); + if (e.target.checked) { + nextSelectedRows.add(rowIndex); + } else { + nextSelectedRows.delete(rowIndex); + } + setSelectedRows(nextSelectedRows); + }} + /> + ), + popoverContent: () =>
Close me, quick!
, + }, + ], + [selectedRows, setSelectedRows] + ); + + return ( +
+ data[rowIndex][columnId]} + pagination={{ + ...pagination, + pageSizeOptions: [5, 15, 25], + onChangeItemsPerPage: setPageSize, + onChangePage: setPageIndex, + }} + /> +
+ ); +} diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 59b49b5d99f..7bd895f5c03 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -20,6 +20,7 @@ import { EuiDataGridInMemory, EuiDataGridPaginationProps, EuiDataGridInMemoryValues, + EuiDataGridActionColumn, EuiDataGridSorting, EuiDataGridStyle, EuiDataGridStyleBorders, @@ -33,15 +34,12 @@ import { EuiDataGridFocusedCell, } from './data_grid_types'; import { EuiDataGridCellProps } from './data_grid_cell'; -// @ts-ignore-next-line import { EuiButtonEmpty } from '../button'; import { keyCodes, htmlIdGenerator } from '../../services'; import { EuiDataGridBody } from './data_grid_body'; import { useColumnSelector } from './column_selector'; import { useStyleSelector, startingStyles } from './style_selector'; -// @ts-ignore-next-line import { EuiTablePagination } from '../table/table_pagination'; -// @ts-ignore-next-line import { EuiFocusTrap } from '../focus_trap'; import { EuiResizeObserver } from '../observer/resize_observer'; import { EuiDataGridInMemoryRenderer } from './data_grid_inmemory_renderer'; @@ -60,6 +58,7 @@ const MINIMUM_WIDTH_FOR_GRID_CONTROLS = 479; type CommonGridProps = CommonProps & HTMLAttributes & { + leadingColumns?: EuiDataGridActionColumn[]; /** * An array of #EuiDataGridColumn objects. Lists the columns available and the schema and settings tied to it. */ @@ -494,6 +493,7 @@ export const EuiDataGrid: FunctionComponent = props => { }; const { + leadingColumns, columns, columnVisibility, schemaDetectors, @@ -789,6 +789,7 @@ export const EuiDataGrid: FunctionComponent = props => { {ref => ( = props => { defaultColumnWidth={defaultColumnWidth} inMemoryValues={inMemoryValues} inMemory={inMemory} + leadingColumns={leadingColumns} columns={orderedVisibleColumns} schema={mergedSchema} schemaDetectors={allSchemaDetectors} diff --git a/src/components/datagrid/data_grid_action_header_cell.tsx b/src/components/datagrid/data_grid_action_header_cell.tsx new file mode 100644 index 00000000000..59cdead9956 --- /dev/null +++ b/src/components/datagrid/data_grid_action_header_cell.tsx @@ -0,0 +1,173 @@ +import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; +import classnames from 'classnames'; +import { keyCodes } from '../../services'; +import tabbable from 'tabbable'; +import { + EuiDataGridActionColumn, + EuiDataGridFocusedCell, +} from './data_grid_types'; +import { EuiDataGridDataRowProps } from './data_grid_data_row'; + +export interface EuiDataGridActionHeaderRowProps { + index: number; + actionColumn: EuiDataGridActionColumn; + focusedCell: EuiDataGridFocusedCell; + setFocusedCell: EuiDataGridDataRowProps['onCellFocus']; + headerIsInteractive: boolean; +} + +export const EuiDataGridActionHeaderCell: FunctionComponent< + EuiDataGridActionHeaderRowProps +> = props => { + const { + actionColumn, + index, + focusedCell, + setFocusedCell, + headerIsInteractive, + } = props; + + const { headerCellRender: HeaderCellRender, width, id } = actionColumn; + + const classes = classnames('euiDataGridActionHeaderCell'); + + const headerRef = useRef(null); + const isFocused = focusedCell[0] === index && focusedCell[1] === -1; + const [isCellEntered, setIsCellEntered] = useState(false); + + useEffect(() => { + if (headerRef.current) { + function enableInteractives() { + const interactiveElements = headerRef.current!.querySelectorAll( + '[data-euigrid-tab-managed]' + ); + for (let i = 0; i < interactiveElements.length; i++) { + interactiveElements[i].setAttribute('tabIndex', '0'); + } + } + + function disableInteractives() { + const tababbles = tabbable(headerRef.current!); + if (tababbles.length > 1) { + console.warn( + `EuiDataGridHeaderCell expects at most 1 tabbable element, ${ + tababbles.length + } found instead` + ); + } + for (let i = 0; i < tababbles.length; i++) { + const element = tababbles[i]; + element.setAttribute('data-euigrid-tab-managed', 'true'); + element.setAttribute('tabIndex', '-1'); + } + } + + if (isCellEntered) { + enableInteractives(); + const tabbables = tabbable(headerRef.current!); + if (tabbables.length > 0) { + tabbables[0].focus(); + } + } else { + disableInteractives(); + } + } + }, [isCellEntered]); + + useEffect(() => { + if (headerRef.current) { + if (isFocused) { + const interactives = headerRef.current.querySelectorAll( + '[data-euigrid-tab-managed]' + ); + if (interactives.length === 1) { + setIsCellEntered(true); + } else { + headerRef.current.focus(); + } + } else { + setIsCellEntered(false); + } + + // focusin bubbles while focus does not, and this needs to react to children gaining focus + function onFocusIn(e: FocusEvent) { + if (headerIsInteractive === false) { + // header is not interactive, avoid focusing + requestAnimationFrame(() => headerRef.current!.blur()); + e.preventDefault(); + return false; + } else { + // take the focus + setFocusedCell([index, -1]); + } + } + + // focusout bubbles while blur does not, and this needs to react to the children losing focus + function onFocusOut() { + // wait for the next element to receive focus, then update interactives' state + requestAnimationFrame(() => { + if (headerRef.current) { + if (headerRef.current.contains(document.activeElement) === false) { + setIsCellEntered(false); + } + } + }); + } + + function onKeyUp(e: KeyboardEvent) { + switch (e.keyCode) { + case keyCodes.ENTER: { + e.preventDefault(); + setIsCellEntered(true); + break; + } + case keyCodes.ESCAPE: { + e.preventDefault(); + // move focus to cell + setIsCellEntered(false); + headerRef.current!.focus(); + break; + } + case keyCodes.F2: { + e.preventDefault(); + if (document.activeElement === headerRef.current) { + // move focus into cell's interactives + setIsCellEntered(true); + } else { + // move focus to cell + setIsCellEntered(false); + headerRef.current!.focus(); + } + break; + } + } + } + + const headerNode = headerRef.current; + // @ts-ignore-next line TS doesn't have focusin + headerNode.addEventListener('focusin', onFocusIn); + headerNode.addEventListener('focusout', onFocusOut); + headerNode.addEventListener('keyup', onKeyUp); + return () => { + // @ts-ignore-next line TS doesn't have focusin + headerNode.removeEventListener('focusin', onFocusIn); + headerNode.removeEventListener('focusout', onFocusOut); + headerNode.removeEventListener('keyup', onKeyUp); + }; + } + }, [headerIsInteractive, isFocused, setIsCellEntered, setFocusedCell, index]); + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 7194d2e7df1..5c4249c3591 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -2,6 +2,7 @@ import React, { Fragment, FunctionComponent, useMemo } from 'react'; // @ts-ignore-next-line import { EuiCodeBlock } from '../code'; import { + EuiDataGridActionColumn, EuiDataGridColumn, EuiDataGridColumnWidths, EuiDataGridPopoverContents, @@ -24,6 +25,7 @@ import { export interface EuiDataGridBodyProps { columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; + leadingColumns?: EuiDataGridActionColumn[]; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; schemaDetectors: EuiDataGridSchemaDetector[]; @@ -74,6 +76,7 @@ export const EuiDataGridBody: FunctionComponent< const { columnWidths, defaultColumnWidth, + leadingColumns = [], columns, schema, schemaDetectors, @@ -173,6 +176,7 @@ export const EuiDataGridBody: FunctionComponent< return ( & { rowIndex: number; + leadingColumns: EuiDataGridActionColumn[]; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; popoverContents: EuiDataGridPopoverContents; @@ -34,6 +36,7 @@ const DefaultColumnFormatter: EuiDataGridPopoverContent = ({ children }) => { const EuiDataGridDataRow: FunctionComponent = memo( props => { const { + leadingColumns, columns, schema, popoverContents, @@ -59,6 +62,31 @@ const EuiDataGridDataRow: FunctionComponent = memo( className={classes} data-test-subj={dataTestSubj} {...rest}> + {leadingColumns.map((leadingColumn, i) => { + const { id, rowCellRender, popoverContent } = leadingColumn; + + const isExpandable = + leadingColumn.isExpandable !== undefined + ? leadingColumn.isExpandable + : true; + + return ( + + ); + })} {columns.map((props, i) => { const { id } = props; const columnType = schema[id] ? schema[id].columnType : null; diff --git a/src/components/datagrid/data_grid_header_cell.tsx b/src/components/datagrid/data_grid_header_cell.tsx new file mode 100644 index 00000000000..1fac6ef23f2 --- /dev/null +++ b/src/components/datagrid/data_grid_header_cell.tsx @@ -0,0 +1,233 @@ +import React, { + FunctionComponent, + HTMLAttributes, + useEffect, + useRef, + useState, +} from 'react'; +import { htmlIdGenerator } from '../../services/accessibility'; +import classnames from 'classnames'; +import { EuiDataGridHeaderRowPropsSpecificProps } from './data_grid_header_row'; +import { keyCodes } from '../../services'; +import { EuiDataGridColumnResizer } from './data_grid_column_resizer'; +import { EuiScreenReaderOnly } from '../accessibility'; +import tabbable from 'tabbable'; +import { EuiDataGridColumn } from './data_grid_types'; + +export interface EuiDataGridHeaderCellProps + extends Omit< + EuiDataGridHeaderRowPropsSpecificProps, + 'columns' | 'leadingColumns' + > { + column: EuiDataGridColumn; + index: number; +} + +export const EuiDataGridHeaderCell: FunctionComponent< + EuiDataGridHeaderCellProps +> = props => { + const { + column, + index, + columnWidths, + schema, + defaultColumnWidth, + setColumnWidth, + sorting, + focusedCell, + setFocusedCell, + headerIsInteractive, + } = props; + const { id, display } = column; + + const width = columnWidths[id] || defaultColumnWidth; + + const ariaProps: { + 'aria-sort'?: HTMLAttributes['aria-sort']; + 'aria-describedby'?: HTMLAttributes['aria-describedby']; + } = {}; + + let screenReaderId; + let sortString; + + if (sorting) { + const sortedColumnIds = new Set(sorting.columns.map(({ id }) => id)); + + if (sorting.columns.length === 1 && sortedColumnIds.has(id)) { + const sortDirection = sorting.columns[0].direction; + + let sortValue: HTMLAttributes['aria-sort'] = 'other'; + if (sortDirection === 'asc') { + sortValue = 'ascending'; + } else if (sortDirection === 'desc') { + sortValue = 'descending'; + } + + ariaProps['aria-sort'] = sortValue; + } else if (sorting.columns.length >= 2 && sortedColumnIds.has(id)) { + sortString = sorting.columns + .map(col => `Sorted by ${col.id} ${col.direction}`) + .join(' then '); + screenReaderId = htmlIdGenerator()(); + ariaProps['aria-describedby'] = screenReaderId; + } + } + + const columnType = schema[id] ? schema[id].columnType : null; + + const classes = classnames('euiDataGridHeaderCell', { + [`euiDataGridHeaderCell--${columnType}`]: columnType, + }); + + const headerRef = useRef(null); + const isFocused = focusedCell[0] === index && focusedCell[1] === -1; + const [isCellEntered, setIsCellEntered] = useState(false); + + useEffect(() => { + if (headerRef.current) { + function enableInteractives() { + const interactiveElements = headerRef.current!.querySelectorAll( + '[data-euigrid-tab-managed]' + ); + for (let i = 0; i < interactiveElements.length; i++) { + interactiveElements[i].setAttribute('tabIndex', '0'); + } + } + + function disableInteractives() { + const tababbles = tabbable(headerRef.current!); + if (tababbles.length > 1) { + console.warn( + `EuiDataGridHeaderCell expects at most 1 tabbable element, ${ + tababbles.length + } found instead` + ); + } + for (let i = 0; i < tababbles.length; i++) { + const element = tababbles[i]; + element.setAttribute('data-euigrid-tab-managed', 'true'); + element.setAttribute('tabIndex', '-1'); + } + } + + if (isCellEntered) { + enableInteractives(); + const tabbables = tabbable(headerRef.current!); + if (tabbables.length > 0) { + tabbables[0].focus(); + } + } else { + disableInteractives(); + } + } + }, [isCellEntered]); + + useEffect(() => { + if (headerRef.current) { + if (isFocused) { + const interactives = headerRef.current.querySelectorAll( + '[data-euigrid-tab-managed]' + ); + if (interactives.length === 1) { + setIsCellEntered(true); + } else { + headerRef.current.focus(); + } + } else { + setIsCellEntered(false); + } + + // focusin bubbles while focus does not, and this needs to react to children gaining focus + function onFocusIn(e: FocusEvent) { + if (headerIsInteractive === false) { + // header is not interactive, avoid focusing + requestAnimationFrame(() => headerRef.current!.blur()); + e.preventDefault(); + return false; + } else { + // take the focus + setFocusedCell([index, -1]); + } + } + + // focusout bubbles while blur does not, and this needs to react to the children losing focus + function onFocusOut() { + // wait for the next element to receive focus, then update interactives' state + requestAnimationFrame(() => { + if (headerRef.current) { + if (headerRef.current.contains(document.activeElement) === false) { + setIsCellEntered(false); + } + } + }); + } + + function onKeyUp(e: KeyboardEvent) { + switch (e.keyCode) { + case keyCodes.ENTER: { + e.preventDefault(); + setIsCellEntered(true); + break; + } + case keyCodes.ESCAPE: { + e.preventDefault(); + // move focus to cell + setIsCellEntered(false); + headerRef.current!.focus(); + break; + } + case keyCodes.F2: { + e.preventDefault(); + if (document.activeElement === headerRef.current) { + // move focus into cell's interactives + setIsCellEntered(true); + } else { + // move focus to cell + setIsCellEntered(false); + headerRef.current!.focus(); + } + break; + } + } + } + + const headerNode = headerRef.current; + // @ts-ignore-next line TS doesn't have focusin + headerNode.addEventListener('focusin', onFocusIn); + headerNode.addEventListener('focusout', onFocusOut); + headerNode.addEventListener('keyup', onKeyUp); + return () => { + // @ts-ignore-next line TS doesn't have focusin + headerNode.removeEventListener('focusin', onFocusIn); + headerNode.removeEventListener('focusout', onFocusOut); + headerNode.removeEventListener('keyup', onKeyUp); + }; + } + }, [headerIsInteractive, isFocused, setIsCellEntered, setFocusedCell, index]); + + return ( +
+ {column.isResizable !== false && width != null ? ( + + ) : null} + +
{display || id}
+ {sorting && sorting.columns.length >= 2 && ( + +
{sortString}
+
+ )} +
+ ); +}; diff --git a/src/components/datagrid/data_grid_header_row.tsx b/src/components/datagrid/data_grid_header_row.tsx index ad2cc51ac1f..a4893dcbcef 100644 --- a/src/components/datagrid/data_grid_header_row.tsx +++ b/src/components/datagrid/data_grid_header_row.tsx @@ -1,28 +1,20 @@ -import React, { - HTMLAttributes, - forwardRef, - FunctionComponent, - useRef, - useEffect, - useState, -} from 'react'; +import React, { HTMLAttributes, forwardRef } from 'react'; import classnames from 'classnames'; -import tabbable from 'tabbable'; import { EuiDataGridColumnWidths, EuiDataGridColumn, EuiDataGridSorting, EuiDataGridFocusedCell, + EuiDataGridActionColumn, } from './data_grid_types'; import { CommonProps } from '../common'; -import { EuiDataGridColumnResizer } from './data_grid_column_resizer'; -import { htmlIdGenerator } from '../../services/accessibility'; -import { EuiScreenReaderOnly } from '../accessibility'; import { EuiDataGridSchema } from './data_grid_schema'; import { EuiDataGridDataRowProps } from './data_grid_data_row'; -import { keyCodes } from '../../services'; +import { EuiDataGridHeaderCell } from './data_grid_header_cell'; +import { EuiDataGridActionHeaderCell } from './data_grid_action_header_cell'; -interface EuiDataGridHeaderRowPropsSpecificProps { +export interface EuiDataGridHeaderRowPropsSpecificProps { + leadingColumns?: EuiDataGridActionColumn[]; columns: EuiDataGridColumn[]; columnWidths: EuiDataGridColumnWidths; schema: EuiDataGridSchema; @@ -38,226 +30,12 @@ export type EuiDataGridHeaderRowProps = CommonProps & HTMLAttributes & EuiDataGridHeaderRowPropsSpecificProps; -export interface EuiDataGridHeaderCellProps - extends Omit { - column: EuiDataGridColumn; - index: number; -} -const EuiDataGridHeaderCell: FunctionComponent< - EuiDataGridHeaderCellProps -> = props => { - const { - column, - index, - columnWidths, - schema, - defaultColumnWidth, - setColumnWidth, - sorting, - focusedCell, - setFocusedCell, - headerIsInteractive, - } = props; - const { id, display } = column; - - const width = columnWidths[id] || defaultColumnWidth; - - const ariaProps: { - 'aria-sort'?: HTMLAttributes['aria-sort']; - 'aria-describedby'?: HTMLAttributes['aria-describedby']; - } = {}; - - let screenReaderId; - let sortString; - - if (sorting) { - const sortedColumnIds = new Set(sorting.columns.map(({ id }) => id)); - - if (sorting.columns.length === 1 && sortedColumnIds.has(id)) { - const sortDirection = sorting.columns[0].direction; - - let sortValue: HTMLAttributes['aria-sort'] = 'other'; - if (sortDirection === 'asc') { - sortValue = 'ascending'; - } else if (sortDirection === 'desc') { - sortValue = 'descending'; - } - - ariaProps['aria-sort'] = sortValue; - } else if (sorting.columns.length >= 2 && sortedColumnIds.has(id)) { - sortString = sorting.columns - .map(col => `Sorted by ${col.id} ${col.direction}`) - .join(' then '); - screenReaderId = htmlIdGenerator()(); - ariaProps['aria-describedby'] = screenReaderId; - } - } - - const columnType = schema[id] ? schema[id].columnType : null; - - const classes = classnames('euiDataGridHeaderCell', { - [`euiDataGridHeaderCell--${columnType}`]: columnType, - }); - - const headerRef = useRef(null); - const isFocused = focusedCell[0] === index && focusedCell[1] === -1; - const [isCellEntered, setIsCellEntered] = useState(false); - - useEffect(() => { - if (headerRef.current) { - function enableInteractives() { - const interactiveElements = headerRef.current!.querySelectorAll( - '[data-euigrid-tab-managed]' - ); - for (let i = 0; i < interactiveElements.length; i++) { - interactiveElements[i].setAttribute('tabIndex', '0'); - } - } - - function disableInteractives() { - const tababbles = tabbable(headerRef.current!); - if (tababbles.length > 1) { - console.warn( - `EuiDataGridHeaderCell expects at most 1 tabbable element, ${ - tababbles.length - } found instead` - ); - } - for (let i = 0; i < tababbles.length; i++) { - const element = tababbles[i]; - element.setAttribute('data-euigrid-tab-managed', 'true'); - element.setAttribute('tabIndex', '-1'); - } - } - - if (isCellEntered) { - enableInteractives(); - const tabbables = tabbable(headerRef.current!); - if (tabbables.length > 0) { - tabbables[0].focus(); - } - } else { - disableInteractives(); - } - } - }, [isCellEntered]); - - useEffect(() => { - if (headerRef.current) { - if (isFocused) { - const interactives = headerRef.current.querySelectorAll( - '[data-euigrid-tab-managed]' - ); - if (interactives.length === 1) { - setIsCellEntered(true); - } else { - headerRef.current.focus(); - } - } else { - setIsCellEntered(false); - } - - // focusin bubbles while focus does not, and this needs to react to children gaining focus - function onFocusIn(e: FocusEvent) { - if (headerIsInteractive === false) { - // header is not interactive, avoid focusing - requestAnimationFrame(() => headerRef.current!.blur()); - e.preventDefault(); - return false; - } else { - // take the focus - setFocusedCell([index, -1]); - } - } - - // focusout bubbles while blur does not, and this needs to react to the children losing focus - function onFocusOut() { - // wait for the next element to receive focus, then update interactives' state - requestAnimationFrame(() => { - if (headerRef.current) { - if (headerRef.current.contains(document.activeElement) === false) { - setIsCellEntered(false); - } - } - }); - } - - function onKeyUp(e: KeyboardEvent) { - switch (e.keyCode) { - case keyCodes.ENTER: { - e.preventDefault(); - setIsCellEntered(true); - break; - } - case keyCodes.ESCAPE: { - e.preventDefault(); - // move focus to cell - setIsCellEntered(false); - headerRef.current!.focus(); - break; - } - case keyCodes.F2: { - e.preventDefault(); - if (document.activeElement === headerRef.current) { - // move focus into cell's interactives - setIsCellEntered(true); - } else { - // move focus to cell - setIsCellEntered(false); - headerRef.current!.focus(); - } - break; - } - } - } - - const headerNode = headerRef.current; - // @ts-ignore-next line TS doesn't have focusin - headerNode.addEventListener('focusin', onFocusIn); - headerNode.addEventListener('focusout', onFocusOut); - headerNode.addEventListener('keyup', onKeyUp); - return () => { - // @ts-ignore-next line TS doesn't have focusin - headerNode.removeEventListener('focusin', onFocusIn); - headerNode.removeEventListener('focusout', onFocusOut); - headerNode.removeEventListener('keyup', onKeyUp); - }; - } - }, [headerIsInteractive, isFocused, setIsCellEntered, setFocusedCell, index]); - - return ( -
- {column.isResizable !== false && width != null ? ( - - ) : null} - -
{display || id}
- {sorting && sorting.columns.length >= 2 && ( - -
{sortString}
-
- )} -
- ); -}; - const EuiDataGridHeaderRow = forwardRef< HTMLDivElement, EuiDataGridHeaderRowProps >((props, ref) => { const { + leadingColumns = [], columns, schema, columnWidths, @@ -282,6 +60,16 @@ const EuiDataGridHeaderRow = forwardRef< className={classes} data-test-subj={dataTestSubj} {...rest}> + {leadingColumns.map((actionColumn, index) => ( + + ))} {columns.map((column, index) => ( ; export interface EuiDataGridPopoverContents { diff --git a/src/components/datagrid/index.ts b/src/components/datagrid/index.ts index 338564da16f..64e115915da 100644 --- a/src/components/datagrid/index.ts +++ b/src/components/datagrid/index.ts @@ -10,10 +10,11 @@ export { } from './data_grid_cell'; export { EuiDataGridColumnResizerProps } from './data_grid_column_resizer'; export { EuiDataGridDataRowProps } from './data_grid_data_row'; +export { EuiDataGridHeaderRowProps } from './data_grid_header_row'; +export { EuiDataGridHeaderCellProps } from './data_grid_header_cell'; export { - EuiDataGridHeaderCellProps, - EuiDataGridHeaderRowProps, -} from './data_grid_header_row'; + EuiDataGridActionHeaderRowProps, +} from './data_grid_action_header_cell'; export { EuiDataGridInMemoryRendererProps, } from './data_grid_inmemory_renderer'; From 5fc043007a20297fc3917a6f90d34859faf30a52 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 12 Feb 2020 10:05:29 -0700 Subject: [PATCH 02/13] Renamed action columns to control columns, finished functionality, demo, and docs --- src-docs/src/routes.js | 4 +- .../src/views/datagrid/control_columns.js | 231 ++++++++++++++++++ src-docs/src/views/datagrid/datagrid.js | 36 ++- .../datagrid_controlcolumns_example.js | 83 +++++++ .../src/views/datagrid/datagrid_example.js | 18 ++ .../datagrid/datagrid_selection_example.js | 48 ---- src-docs/src/views/datagrid/props.tsx | 5 + src-docs/src/views/datagrid/selection.js | 143 ----------- .../tables/in_memory/in_memory_selection.js | 2 +- src/components/datagrid/data_grid.tsx | 50 +++- src/components/datagrid/data_grid_body.tsx | 14 +- ....tsx => data_grid_control_header_cell.tsx} | 16 +- .../datagrid/data_grid_data_row.tsx | 43 +++- .../datagrid/data_grid_header_cell.tsx | 2 +- .../datagrid/data_grid_header_row.tsx | 30 ++- src/components/datagrid/data_grid_types.ts | 20 +- src/components/datagrid/index.ts | 4 +- 17 files changed, 509 insertions(+), 240 deletions(-) create mode 100644 src-docs/src/views/datagrid/control_columns.js create mode 100644 src-docs/src/views/datagrid/datagrid_controlcolumns_example.js delete mode 100644 src-docs/src/views/datagrid/datagrid_selection_example.js delete mode 100644 src-docs/src/views/datagrid/selection.js rename src/components/datagrid/{data_grid_action_header_cell.tsx => data_grid_control_header_cell.tsx} (93%) diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 07b927f201a..41a0c6088b6 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -79,7 +79,7 @@ import { DataGridExample } from './views/datagrid/datagrid_example'; import { DataGridMemoryExample } from './views/datagrid/datagrid_memory_example'; import { DataGridSchemaExample } from './views/datagrid/datagrid_schema_example'; import { DataGridStylingExample } from './views/datagrid/datagrid_styling_example'; -import { DataGridSelectionExample } from './views/datagrid/datagrid_selection_example'; +import { DataGridControlColumnsExample } from './views/datagrid/datagrid_controlcolumns_example'; import { DatePickerExample } from './views/date_picker/date_picker_example'; @@ -342,7 +342,7 @@ const navigation = [ DataGridMemoryExample, DataGridSchemaExample, DataGridStylingExample, - DataGridSelectionExample, + DataGridControlColumnsExample, TableExample, ].map(example => createExample(example)), }, diff --git a/src-docs/src/views/datagrid/control_columns.js b/src-docs/src/views/datagrid/control_columns.js new file mode 100644 index 00000000000..385b69f5c18 --- /dev/null +++ b/src-docs/src/views/datagrid/control_columns.js @@ -0,0 +1,231 @@ +import React, { + createContext, + useContext, + useCallback, + useEffect, + useReducer, + useState, +} from 'react'; +import { fake } from 'faker'; + +import { + EuiDataGrid, + EuiAvatar, + EuiCheckbox, + EuiButtonIcon, + EuiPopover, +} from '../../../../src/components/'; + +import { euiColorHighlight as selectedRowColorLight } from '!!sass-vars-to-js-loader!../../../../src/global_styling/variables/_colors.scss'; +import { euiColorHighlight as selectedRowColorDark } from '!!sass-vars-to-js-loader!../../../../src/themes/eui/eui_colors_dark.scss'; +import { withTheme } from '../../components/with_theme'; + +const columns = [ + { + id: 'avatar', + initialWidth: 65, + }, + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'city', + }, + { + id: 'country', + }, + { + id: 'account', + }, +]; + +const data = []; + +for (let i = 1; i < 500; i++) { + data.push({ + avatar: ( + + ), + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: fake('{{internet.email}}'), + city: fake('{{address.city}}'), + country: fake('{{address.country}}'), + account: fake('{{finance.account}}'), + }); +} + +const SelectionContext = createContext(); + +const SelectionHeaderCell = () => { + const [selectedRows, updateSelectedRows] = useContext(SelectionContext); + const isIndeterminate = + selectedRows.size > 0 && selectedRows.size < data.length; + return ( + 0} + onChange={e => { + if (isIndeterminate) { + // clear selection + updateSelectedRows({ action: 'clear' }); + } else { + if (e.target.checked) { + // select everything + updateSelectedRows({ action: 'selectAll' }); + } else { + // clear selection + updateSelectedRows({ action: 'clear' }); + } + } + }} + /> + ); +}; + +const SelectionRowCell = withTheme(({ theme, rowIndex, setCellProps }) => { + const [selectedRows, updateSelectedRows] = useContext(SelectionContext); + const isChecked = selectedRows.has(rowIndex); + + useEffect(() => { + const highlightColor = + theme === 'light' + ? selectedRowColorLight.rgba + : selectedRowColorDark.rgba; + setCellProps({ + style: { + backgroundColor: isChecked ? highlightColor : 'transparent', + }, + }); + }, [isChecked, setCellProps, theme]); + + return ( +
+ { + if (e.target.checked) { + updateSelectedRows({ action: 'add', rowIndex }); + } else { + updateSelectedRows({ action: 'delete', rowIndex }); + } + }} + /> +
+ ); +}); + +const leadingControlColumns = [ + { + id: 'selection', + width: 31, + headerCellRender: SelectionHeaderCell, + rowCellRender: SelectionRowCell, + }, +]; + +const trailingControlColumns = [ + { + id: 'actions', + width: 40, + headerCellRender: () => null, + rowCellRender: function RowCellRender() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + return ( +
+ setIsPopoverOpen(!isPopoverOpen)} + /> + } + closePopover={() => setIsPopoverOpen(false)} + ownFocus={true}> + + + + +
+ ); + }, + }, +]; + +export default function DataGrid() { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 15, + }); + const setPageIndex = useCallback( + pageIndex => setPagination({ ...pagination, pageIndex }), + [pagination, setPagination] + ); + const setPageSize = useCallback( + pageSize => setPagination({ ...pagination, pageSize }), + [pagination, setPagination] + ); + + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }) => id) + ); + + const rowSelection = useReducer((rowSelection, { action, rowIndex }) => { + if (action === 'add') { + const nextRowSelection = new Set(rowSelection); + nextRowSelection.add(rowIndex); + return nextRowSelection; + } else if (action === 'delete') { + const nextRowSelection = new Set(rowSelection); + nextRowSelection.delete(rowIndex); + return nextRowSelection; + } else if (action === 'clear') { + return new Set(); + } else if (action === 'selectAll') { + return new Set(data.map((_, index) => index)); + } + return rowSelection; + }, new Set()); + + const renderCellValue = useCallback( + ({ rowIndex, columnId }) => data[rowIndex][columnId], + [] + ); + + return ( + +
+ +
+
+ ); +} diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 243988fd1ca..9cdd367885b 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -12,8 +12,9 @@ import { EuiLink, EuiFlexGroup, EuiFlexItem, + EuiPopover, + EuiButtonIcon, } from '../../../../src/components/'; -import { EuiButtonIcon } from '../../../../src/components/button/button_icon'; const columns = [ { @@ -82,6 +83,38 @@ for (let i = 1; i < 100; i++) { }); } +const trailingControlColumns = [ + { + id: 'actions', + width: 40, + headerCellRender: () => null, + rowCellRender: function RowCellRender() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + return ( +
+ setIsPopoverOpen(!isPopoverOpen)} + /> + } + closePopover={() => setIsPopoverOpen(false)} + ownFocus={true}> + + + + +
+ ); + }, + }, +]; + export default () => { // ** Pagination config const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); @@ -137,6 +170,7 @@ export default () => { aria-label="Data grid demo" columns={columns} columnVisibility={{ visibleColumns, setVisibleColumns }} + trailingControlColumns={trailingControlColumns} rowCount={raw_data.length} renderCellValue={renderCellValue} inMemory={{ level: 'sorting' }} diff --git a/src-docs/src/views/datagrid/datagrid_controlcolumns_example.js b/src-docs/src/views/datagrid/datagrid_controlcolumns_example.js new file mode 100644 index 00000000000..9b77f41e279 --- /dev/null +++ b/src-docs/src/views/datagrid/datagrid_controlcolumns_example.js @@ -0,0 +1,83 @@ +import React, { Fragment } from 'react'; + +import { renderToHtml } from '../../services'; + +import { GuideSectionTypes } from '../../components'; +import { EuiDataGrid, EuiCodeBlock, EuiCode } from '../../../../src/components'; + +import DataGridControlColumns from './control_columns'; +const dataGridControlColumnsSource = require('!!raw-loader!./control_columns'); +const dataGridControlColumnsHtml = renderToHtml(DataGridControlColumns); + +import { DataGridControlColumn as EuiDataGridControlColumn } from './props'; + +const gridSnippet = ` Select a Row, + rowCellRender: () =>
, + }, + ]} + trailingControlColumns={[ + { + id: 'actions', + width: 40, + headerCellRender: () => null, + rowCellRender: MyGridActionsComponent, + }, + ]} +/> +`; + +export const DataGridControlColumnsExample = { + title: 'Data grid control columns', + sections: [ + { + source: [ + { + type: GuideSectionTypes.JS, + code: dataGridControlColumnsSource, + }, + { + type: GuideSectionTypes.HTML, + code: dataGridControlColumnsHtml, + }, + ], + text: ( + +

+ Control columns can be used to include ancillary cells not based on + the dataset, such as row selection checkboxes or action buttons. + These columns can be placed at either side of the data grid, and + users are unable to resize, sort, or rearrange them. +

+

+ These custom columns are defined by passing an array of + EuiDataGridControlColumn objects (see Props tab below) to{' '} + leadingControlColumns and/or{' '} + trailingControlColumns. +

+

+ As with the data grid's renderCellValue, the + control columns' headerCellRender and{' '} + rowCellRender props are treated as React + components. +

+ + {gridSnippet} + +
+ ), + components: { DataGridControlColumns }, + + props: { + EuiDataGrid, + EuiDataGridControlColumn, + }, + demo: , + }, + ], +}; diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index 4541aa3ee29..376b7e4b713 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -29,6 +29,7 @@ import { DataGridToolbarVisibilityOptions, DataGridColumnVisibility, DataGridPopoverContent, + DataGridControlColumn, } from './props'; const gridSnippet = ` @@ -46,6 +47,22 @@ const gridSnippet = ` visibleColumns: ['A', 'C'], setVisibleColumns: () => {}, }} + leadingControlColumns={[ + { + id: 'selection', + width: 31, + headerCellRender: () => Select a Row, + rowCellRender: () =>
, + }, + ]} + trailingControlColumns={[ + { + id: 'actions', + width: 40, + headerCellRender: () => null, + rowCellRender: MyGridActionsComponent, + }, + ]} // Optional. Customize the content inside the cell. The current example outputs the row and column position. // Often used in combination with useEffect() to dynamically change the render. renderCellValue={({ rowIndex, columnId }) => @@ -307,6 +324,7 @@ export const DataGridExample = { EuiDataGrid, EuiDataGridColumn: DataGridColumn, EuiDataGridColumnVisibility: DataGridColumnVisibility, + EuiDataGridControlColumn: DataGridControlColumn, EuiDataGridInMemory: DataGridInMemory, EuiDataGridPagination: DataGridPagination, EuiDataGridSorting: DataGridSorting, diff --git a/src-docs/src/views/datagrid/datagrid_selection_example.js b/src-docs/src/views/datagrid/datagrid_selection_example.js deleted file mode 100644 index 2e526c06f2f..00000000000 --- a/src-docs/src/views/datagrid/datagrid_selection_example.js +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Fragment } from 'react'; - -import { renderToHtml } from '../../services'; - -import { GuideSectionTypes } from '../../components'; -import { EuiDataGrid, EuiCodeBlock } from '../../../../src/components'; - -import DataGridSelection from './selection'; -const dataGridSelectionSource = require('!!raw-loader!./selection'); -const dataGridSelectionHtml = renderToHtml(DataGridSelection); - -const gridSnippet = ` -`; - -export const DataGridSelectionExample = { - title: 'Data grid row selection', - sections: [ - { - source: [ - { - type: GuideSectionTypes.JS, - code: dataGridSelectionSource, - }, - { - type: GuideSectionTypes.HTML, - code: dataGridSelectionHtml, - }, - ], - text: ( - -

Blurb about selection API.

- - {gridSnippet} - -
- ), - components: { DataGridSelection }, - - props: { - EuiDataGrid, - }, - demo: , - }, - ], -}; diff --git a/src-docs/src/views/datagrid/props.tsx b/src-docs/src/views/datagrid/props.tsx index 0f08f8aa051..465561a044d 100644 --- a/src-docs/src/views/datagrid/props.tsx +++ b/src-docs/src/views/datagrid/props.tsx @@ -8,6 +8,7 @@ import { EuiDataGridToolBarVisibilityOptions, EuiDataGridColumnVisibility, EuiDataGridPopoverContentProps, + EuiDataGridControlColumn, } from '../../../../src/components/datagrid/data_grid_types'; import { EuiDataGridCellValueElementProps } from '../../../../src/components/datagrid/data_grid_cell'; import { EuiDataGridSchemaDetector } from '../../../../src/components/datagrid/data_grid_schema'; @@ -49,3 +50,7 @@ export const DataGridColumnVisibility: FunctionComponent< export const DataGridPopoverContent: FunctionComponent< EuiDataGridPopoverContentProps > = () =>
; + +export const DataGridControlColumn: FunctionComponent< + EuiDataGridControlColumn +> = ({ isExpandable: _isExpandable = false }) =>
; diff --git a/src-docs/src/views/datagrid/selection.js b/src-docs/src/views/datagrid/selection.js deleted file mode 100644 index c51b44f69c7..00000000000 --- a/src-docs/src/views/datagrid/selection.js +++ /dev/null @@ -1,143 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { fake } from 'faker'; - -import { - EuiDataGrid, - EuiAvatar, - EuiCheckbox, -} from '../../../../src/components/'; - -const columns = [ - { - id: 'avatar', - initialWidth: 65, - }, - { - id: 'name', - }, - { - id: 'email', - }, - { - id: 'city', - }, - { - id: 'country', - }, - { - id: 'account', - }, -]; - -const data = []; - -for (let i = 1; i < 500; i++) { - data.push({ - avatar: ( - - ), - name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), - email: fake('{{internet.email}}'), - city: fake('{{address.city}}'), - country: fake('{{address.country}}'), - account: fake('{{finance.account}}'), - }); -} - -export default function DataGrid() { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 15, - }); - const setPageIndex = useCallback( - pageIndex => setPagination({ ...pagination, pageIndex }), - [pagination, setPagination] - ); - const setPageSize = useCallback( - pageSize => setPagination({ ...pagination, pageSize }), - [pagination, setPagination] - ); - - const [visibleColumns, setVisibleColumns] = useState(() => - columns.map(({ id }) => id) - ); - - const [selectedRows, setSelectedRows] = useState(new Set()); - - const leadingColumns = useMemo( - () => [ - { - id: 'selection', - width: 31, - headerCellRender: () => { - const isIndeterminate = - selectedRows.size > 0 && selectedRows.size < data.length; - return ( - 0} - onChange={e => { - if (isIndeterminate) { - // clear selection - setSelectedRows(new Set()); - } else { - if (e.target.checked) { - // select everything - setSelectedRows(new Set(data.map((_, index) => index))); - } else { - // clear selection - setSelectedRows(new Set()); - } - } - }} - /> - ); - }, - rowCellRender: ({ rowIndex }) => ( - { - const nextSelectedRows = new Set(selectedRows); - if (e.target.checked) { - nextSelectedRows.add(rowIndex); - } else { - nextSelectedRows.delete(rowIndex); - } - setSelectedRows(nextSelectedRows); - }} - /> - ), - popoverContent: () =>
Close me, quick!
, - }, - ], - [selectedRows, setSelectedRows] - ); - - return ( -
- data[rowIndex][columnId]} - pagination={{ - ...pagination, - pageSizeOptions: [5, 15, 25], - onChangeItemsPerPage: setPageSize, - onChangePage: setPageIndex, - }} - /> -
- ); -} diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection.js b/src-docs/src/views/tables/in_memory/in_memory_selection.js index 53d6158b88a..44c6721872e 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_selection.js +++ b/src-docs/src/views/tables/in_memory/in_memory_selection.js @@ -95,7 +95,7 @@ export class Table extends Component { } renderToolsLeft() { - const selection = this.state.selection; + const selection = this.state.control_columns; if (selection.length === 0) { return; diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 7bd895f5c03..e6166675816 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -20,7 +20,7 @@ import { EuiDataGridInMemory, EuiDataGridPaginationProps, EuiDataGridInMemoryValues, - EuiDataGridActionColumn, + EuiDataGridControlColumn, EuiDataGridSorting, EuiDataGridStyle, EuiDataGridStyleBorders, @@ -58,7 +58,14 @@ const MINIMUM_WIDTH_FOR_GRID_CONTROLS = 479; type CommonGridProps = CommonProps & HTMLAttributes & { - leadingColumns?: EuiDataGridActionColumn[]; + /** + * An array of #EuiDataGridControlColumn objects. Used to define ancillary columns on the left side of the data grid. + */ + leadingControlColumns?: EuiDataGridControlColumn[]; + /** + * An array of #EuiDataGridControlColumn objects. Used to define ancillary columns on the right side of the data grid. + */ + trailingControlColumns?: EuiDataGridControlColumn[]; /** * An array of #EuiDataGridColumn objects. Lists the columns available and the schema and settings tied to it. */ @@ -210,6 +217,8 @@ function renderPagination(props: EuiDataGridProps) { function useDefaultColumnWidth( container: HTMLElement | null, + leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [], + trailingControlColumns: EuiDataGridProps['leadingControlColumns'] = [], columns: EuiDataGridProps['columns'] ): number | null { const [defaultColumnWidth, setDefaultColumnWidth] = useState( @@ -220,20 +229,32 @@ function useDefaultColumnWidth( if (container != null) { const gridWidth = container.clientWidth; + const controlColumnWidths = [ + ...leadingControlColumns, + ...trailingControlColumns, + ].reduce( + (claimedWidth, controlColumn: EuiDataGridControlColumn) => + claimedWidth + controlColumn.width, + 0 + ); + const columnsWithWidths = columns.filter< EuiDataGridColumn & { initialWidth: number } >(doesColumnHaveAnInitialWidth); - const claimedWidth = columnsWithWidths.reduce( + + const definedColumnsWidth = columnsWithWidths.reduce( (claimedWidth, column) => claimedWidth + column.initialWidth, 0 ); + const claimedWidth = controlColumnWidths + definedColumnsWidth; + const widthToFill = gridWidth - claimedWidth; const unsizedColumnCount = columns.length - columnsWithWidths.length; const columnWidth = Math.max(widthToFill / unsizedColumnCount, 100); setDefaultColumnWidth(columnWidth); } - }, [container, columns]); + }, [container, columns, leadingControlColumns, trailingControlColumns]); return defaultColumnWidth; } @@ -318,13 +339,19 @@ function useInMemoryValues( function createKeyDownHandler( props: EuiDataGridProps, visibleColumns: EuiDataGridProps['columns'], + leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [], + trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = [], focusedCell: EuiDataGridFocusedCell, headerIsInteractive: boolean, setFocusedCell: (focusedCell: EuiDataGridFocusedCell) => void, updateFocus: Function ) { return (event: KeyboardEvent) => { - const colCount = visibleColumns.length - 1; + const colCount = + visibleColumns.length + + leadingControlColumns.length + + trailingControlColumns.length - + 1; const [x, y] = focusedCell; const rowCount = computeVisibleRows(props); const { keyCode, ctrlKey } = event; @@ -493,7 +520,8 @@ export const EuiDataGrid: FunctionComponent = props => { }; const { - leadingColumns, + leadingControlColumns, + trailingControlColumns, columns, columnVisibility, schemaDetectors, @@ -556,6 +584,8 @@ export const EuiDataGrid: FunctionComponent = props => { // compute the default column width from the container's clientWidth and count of visible columns const defaultColumnWidth = useDefaultColumnWidth( containerRef, + leadingControlColumns, + trailingControlColumns, orderedVisibleColumns ); @@ -752,6 +782,8 @@ export const EuiDataGrid: FunctionComponent = props => { onKeyDown={createKeyDownHandler( props, orderedVisibleColumns, + leadingControlColumns, + trailingControlColumns, realizedFocusedCell, headerIsInteractive, setFocusedCell, @@ -789,7 +821,8 @@ export const EuiDataGrid: FunctionComponent = props => { {ref => ( = props => { defaultColumnWidth={defaultColumnWidth} inMemoryValues={inMemoryValues} inMemory={inMemory} - leadingColumns={leadingColumns} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} columns={orderedVisibleColumns} schema={mergedSchema} schemaDetectors={allSchemaDetectors} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 5c4249c3591..4abf985f2e9 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -2,7 +2,7 @@ import React, { Fragment, FunctionComponent, useMemo } from 'react'; // @ts-ignore-next-line import { EuiCodeBlock } from '../code'; import { - EuiDataGridActionColumn, + EuiDataGridControlColumn, EuiDataGridColumn, EuiDataGridColumnWidths, EuiDataGridPopoverContents, @@ -25,7 +25,8 @@ import { export interface EuiDataGridBodyProps { columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; - leadingColumns?: EuiDataGridActionColumn[]; + leadingControlColumns?: EuiDataGridControlColumn[]; + trailingControlColumns?: EuiDataGridControlColumn[]; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; schemaDetectors: EuiDataGridSchemaDetector[]; @@ -76,7 +77,8 @@ export const EuiDataGridBody: FunctionComponent< const { columnWidths, defaultColumnWidth, - leadingColumns = [], + leadingControlColumns = [], + trailingControlColumns = [], columns, schema, schemaDetectors, @@ -176,7 +178,8 @@ export const EuiDataGridBody: FunctionComponent< return ( = props => { const { - actionColumn, + controlColumn, index, focusedCell, setFocusedCell, headerIsInteractive, } = props; - const { headerCellRender: HeaderCellRender, width, id } = actionColumn; + const { headerCellRender: HeaderCellRender, width, id } = controlColumn; - const classes = classnames('euiDataGridActionHeaderCell'); + const classes = classnames('euiDataGridHeaderCell'); const headerRef = useRef(null); const isFocused = focusedCell[0] === index && focusedCell[1] === -1; diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index 06516968210..e923ff3af31 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent, HTMLAttributes, memo } from 'react'; import classnames from 'classnames'; import { - EuiDataGridActionColumn, + EuiDataGridControlColumn, EuiDataGridColumn, EuiDataGridColumnWidths, EuiDataGridPopoverContent, @@ -16,7 +16,8 @@ import { EuiText } from '../text'; export type EuiDataGridDataRowProps = CommonProps & HTMLAttributes & { rowIndex: number; - leadingColumns: EuiDataGridActionColumn[]; + leadingControlColumns: EuiDataGridControlColumn[]; + trailingControlColumns: EuiDataGridControlColumn[]; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; popoverContents: EuiDataGridPopoverContents; @@ -36,7 +37,8 @@ const DefaultColumnFormatter: EuiDataGridPopoverContent = ({ children }) => { const EuiDataGridDataRow: FunctionComponent = memo( props => { const { - leadingColumns, + leadingControlColumns, + trailingControlColumns, columns, schema, popoverContents, @@ -62,13 +64,9 @@ const EuiDataGridDataRow: FunctionComponent = memo( className={classes} data-test-subj={dataTestSubj} {...rest}> - {leadingColumns.map((leadingColumn, i) => { + {leadingControlColumns.map((leadingColumn, i) => { const { id, rowCellRender, popoverContent } = leadingColumn; - - const isExpandable = - leadingColumn.isExpandable !== undefined - ? leadingColumn.isExpandable - : true; + const isExpandable = leadingColumn.isExpandable === true; return ( = memo( popoverContents[columnType as string] || DefaultColumnFormatter; const width = columnWidths[id] || defaultColumnWidth; + const columnPosition = i + leadingControlColumns.length; return ( + ); + })} + {trailingControlColumns.map((leadingColumn, i) => { + const { id, rowCellRender, popoverContent } = leadingColumn; + const isExpandable = leadingColumn.isExpandable === true; + const colIndex = i + columns.length + leadingControlColumns.length; + + return ( + diff --git a/src/components/datagrid/data_grid_header_cell.tsx b/src/components/datagrid/data_grid_header_cell.tsx index 1fac6ef23f2..f22e418fee7 100644 --- a/src/components/datagrid/data_grid_header_cell.tsx +++ b/src/components/datagrid/data_grid_header_cell.tsx @@ -17,7 +17,7 @@ import { EuiDataGridColumn } from './data_grid_types'; export interface EuiDataGridHeaderCellProps extends Omit< EuiDataGridHeaderRowPropsSpecificProps, - 'columns' | 'leadingColumns' + 'columns' | 'leadingControlColumns' > { column: EuiDataGridColumn; index: number; diff --git a/src/components/datagrid/data_grid_header_row.tsx b/src/components/datagrid/data_grid_header_row.tsx index a4893dcbcef..ef05380fa01 100644 --- a/src/components/datagrid/data_grid_header_row.tsx +++ b/src/components/datagrid/data_grid_header_row.tsx @@ -5,16 +5,17 @@ import { EuiDataGridColumn, EuiDataGridSorting, EuiDataGridFocusedCell, - EuiDataGridActionColumn, + EuiDataGridControlColumn, } from './data_grid_types'; import { CommonProps } from '../common'; import { EuiDataGridSchema } from './data_grid_schema'; import { EuiDataGridDataRowProps } from './data_grid_data_row'; import { EuiDataGridHeaderCell } from './data_grid_header_cell'; -import { EuiDataGridActionHeaderCell } from './data_grid_action_header_cell'; +import { EuiDataGridControlHeaderCell } from './data_grid_control_header_cell'; export interface EuiDataGridHeaderRowPropsSpecificProps { - leadingColumns?: EuiDataGridActionColumn[]; + leadingControlColumns?: EuiDataGridControlColumn[]; + trailingControlColumns?: EuiDataGridControlColumn[]; columns: EuiDataGridColumn[]; columnWidths: EuiDataGridColumnWidths; schema: EuiDataGridSchema; @@ -35,7 +36,8 @@ const EuiDataGridHeaderRow = forwardRef< EuiDataGridHeaderRowProps >((props, ref) => { const { - leadingColumns = [], + leadingControlColumns = [], + trailingControlColumns = [], columns, schema, columnWidths, @@ -60,11 +62,11 @@ const EuiDataGridHeaderRow = forwardRef< className={classes} data-test-subj={dataTestSubj} {...rest}> - {leadingColumns.map((actionColumn, index) => ( - ( + ))} + {trailingControlColumns.map((controlColumn, index) => ( + + ))}
); }); diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 988d5838a93..0661908a49a 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -1,12 +1,30 @@ import { ComponentType, ReactNode } from 'react'; import { EuiDataGridCellProps } from './data_grid_cell'; -export interface EuiDataGridActionColumn { +export interface EuiDataGridControlColumn { + /** + * Used as the React `key` when rendering content + */ id: string; + /** + * Component to render in the column header + */ headerCellRender: ComponentType; + /** + * Component to render for each row in the column + */ rowCellRender: EuiDataGridCellProps['renderCellValue']; + /** + * Whether or not the column is expandable + */ isExpandable?: boolean; + /** + * When expanded, Component used for rendering the popover contents + */ popoverContent?: EuiDataGridPopoverContent; + /** + * Width of the column, uses are unable to change this + */ width: number; } diff --git a/src/components/datagrid/index.ts b/src/components/datagrid/index.ts index 64e115915da..662c2c4120b 100644 --- a/src/components/datagrid/index.ts +++ b/src/components/datagrid/index.ts @@ -13,8 +13,8 @@ export { EuiDataGridDataRowProps } from './data_grid_data_row'; export { EuiDataGridHeaderRowProps } from './data_grid_header_row'; export { EuiDataGridHeaderCellProps } from './data_grid_header_cell'; export { - EuiDataGridActionHeaderRowProps, -} from './data_grid_action_header_cell'; + EuiDataGridControlHeaderRowProps, +} from './data_grid_control_header_cell'; export { EuiDataGridInMemoryRendererProps, } from './data_grid_inmemory_renderer'; From c2ea7dc7a4eb40e0eac2b880ed348ec2199a4419 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 12 Feb 2020 12:21:04 -0700 Subject: [PATCH 03/13] Don't allow control columns to be expandable --- src-docs/src/views/datagrid/props.tsx | 2 +- src/components/datagrid/data_grid_data_row.tsx | 14 ++++++-------- src/components/datagrid/data_grid_types.ts | 8 -------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src-docs/src/views/datagrid/props.tsx b/src-docs/src/views/datagrid/props.tsx index 465561a044d..bd828719b24 100644 --- a/src-docs/src/views/datagrid/props.tsx +++ b/src-docs/src/views/datagrid/props.tsx @@ -53,4 +53,4 @@ export const DataGridPopoverContent: FunctionComponent< export const DataGridControlColumn: FunctionComponent< EuiDataGridControlColumn -> = ({ isExpandable: _isExpandable = false }) =>
; +> = () =>
; diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index e923ff3af31..9e0218e1a1c 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -65,8 +65,7 @@ const EuiDataGridDataRow: FunctionComponent = memo( data-test-subj={dataTestSubj} {...rest}> {leadingControlColumns.map((leadingColumn, i) => { - const { id, rowCellRender, popoverContent } = leadingColumn; - const isExpandable = leadingColumn.isExpandable === true; + const { id, rowCellRender } = leadingColumn; return ( = memo( visibleRowIndex={visibleRowIndex} colIndex={i} columnId={id} - popoverContent={popoverContent || DefaultColumnFormatter} + popoverContent={DefaultColumnFormatter} width={leadingColumn.width} renderCellValue={rowCellRender} onCellFocus={onCellFocus} isFocused={focusedCellPositionInTheRow === i} interactiveCellId={interactiveCellId} - isExpandable={isExpandable} + isExpandable={false} /> ); })} @@ -116,8 +115,7 @@ const EuiDataGridDataRow: FunctionComponent = memo( ); })} {trailingControlColumns.map((leadingColumn, i) => { - const { id, rowCellRender, popoverContent } = leadingColumn; - const isExpandable = leadingColumn.isExpandable === true; + const { id, rowCellRender } = leadingColumn; const colIndex = i + columns.length + leadingControlColumns.length; return ( @@ -127,13 +125,13 @@ const EuiDataGridDataRow: FunctionComponent = memo( visibleRowIndex={visibleRowIndex} colIndex={colIndex} columnId={id} - popoverContent={popoverContent || DefaultColumnFormatter} + popoverContent={DefaultColumnFormatter} width={leadingColumn.width} renderCellValue={rowCellRender} onCellFocus={onCellFocus} isFocused={focusedCellPositionInTheRow === colIndex} interactiveCellId={interactiveCellId} - isExpandable={isExpandable} + isExpandable={false} /> ); })} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 0661908a49a..7a97f44e852 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -14,14 +14,6 @@ export interface EuiDataGridControlColumn { * Component to render for each row in the column */ rowCellRender: EuiDataGridCellProps['renderCellValue']; - /** - * Whether or not the column is expandable - */ - isExpandable?: boolean; - /** - * When expanded, Component used for rendering the popover contents - */ - popoverContent?: EuiDataGridPopoverContent; /** * Width of the column, uses are unable to change this */ From 0f4e07e26530436b7f9d97642202ea5ea2916cf8 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 12 Feb 2020 13:44:25 -0700 Subject: [PATCH 04/13] Added a render test for data grid control columns --- .../__snapshots__/data_grid.test.tsx.snap | 763 ++++++++++++++++++ src/components/datagrid/data_grid.test.tsx | 36 + 2 files changed, 799 insertions(+) diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index ed1397c1409..63a59cb70f4 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -725,6 +725,769 @@ Array [ ] `; +exports[`EuiDataGrid rendering renders control columns 1`] = ` +Array [ +
, +
, +
+
+
+
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ + leading heading + +
+
+
+
+ A +
+
+
+
+ B +
+
+
+
+ + trailing heading + +
+
+
+
+
+
+
+

+ + Row: 1, Column: 1: + +

+
+ 0 +
+
+
+
+
+
+
+
+
+
+

+ + Row: 1, Column: 2: + +

+
+ 0, A +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+

+ + Row: 1, Column: 3: + +

+
+ 0, B +
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + Row: 1, Column: 4: + +

+
+ 0 +
+
+
+
+
+
+
+
+
+

+ + Row: 2, Column: 1: + +

+
+ 1 +
+
+
+
+
+
+
+
+
+
+

+ + Row: 2, Column: 2: + +

+
+ 1, A +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+

+ + Row: 2, Column: 3: + +

+
+ 1, B +
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + Row: 2, Column: 4: + +

+
+ 1 +
+
+
+
+
+
+
+
+
+

+ + Row: 3, Column: 1: + +

+
+ 2 +
+
+
+
+
+
+
+
+
+
+

+ + Row: 3, Column: 2: + +

+
+ 2, A +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+

+ + Row: 3, Column: 3: + +

+
+ 2, B +
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + Row: 3, Column: 4: + +

+
+ 2 +
+
+
+
+
+
+
+
+ +
+
, +
, +] +`; + exports[`EuiDataGrid rendering renders custom column headers 1`] = ` Array [
{ + const component = render( + {}, + }} + leadingControlColumns={[ + { + id: 'leading', + width: 50, + headerCellRender: () => leading heading, + rowCellRender: ({ rowIndex }) => rowIndex, + }, + ]} + trailingControlColumns={[ + { + id: 'trailing', + width: 50, + headerCellRender: () => trailing heading, + rowCellRender: ({ rowIndex }) => rowIndex, + }, + ]} + rowCount={3} + renderCellValue={({ rowIndex, columnId }) => + `${rowIndex}, ${columnId}` + } + toolbarVisibility={{ additionalControls: }} + /> + ); + + expect(component).toMatchSnapshot(); + }); + it('can hide the toolbar', () => { const component = mount( Date: Wed, 12 Feb 2020 20:57:51 -0800 Subject: [PATCH 05/13] allow classNames on more inner grid components and make truncation skip control columns --- .../src/views/datagrid/control_columns.js | 109 +++++++++++++++++- .../__snapshots__/data_grid.test.tsx.snap | 6 +- .../datagrid/_data_grid_data_row.scss | 24 ++-- .../datagrid/_data_grid_header_row.scss | 17 +-- src/components/datagrid/data_grid.tsx | 12 +- src/components/datagrid/data_grid_cell.tsx | 14 ++- .../data_grid_control_header_cell.tsx | 4 +- .../datagrid/data_grid_data_row.tsx | 2 + .../datagrid/data_grid_header_cell.tsx | 12 +- .../datagrid/data_grid_header_row.tsx | 2 + 10 files changed, 160 insertions(+), 42 deletions(-) diff --git a/src-docs/src/views/datagrid/control_columns.js b/src-docs/src/views/datagrid/control_columns.js index 385b69f5c18..3bd96adcccd 100644 --- a/src-docs/src/views/datagrid/control_columns.js +++ b/src-docs/src/views/datagrid/control_columns.js @@ -14,6 +14,11 @@ import { EuiCheckbox, EuiButtonIcon, EuiPopover, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPopoverTitle, + EuiSpacer, } from '../../../../src/components/'; import { euiColorHighlight as selectedRowColorLight } from '!!sass-vars-to-js-loader!../../../../src/global_styling/variables/_colors.scss'; @@ -23,7 +28,9 @@ import { withTheme } from '../../components/with_theme'; const columns = [ { id: 'avatar', - initialWidth: 65, + initialWidth: 38, + isExpandable: false, + isResizable: false, }, { id: 'name', @@ -63,6 +70,65 @@ for (let i = 1; i < 500; i++) { const SelectionContext = createContext(); +const SelectionButton = () => { + const [selectedRows] = useContext(SelectionContext); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + if (selectedRows.size > 0) { + return ( + setIsPopoverOpen(!isPopoverOpen)}> + {selectedRows.size} {selectedRows.size > 1 ? 'items' : 'item'}{' '} + selected + + } + closePopover={() => setIsPopoverOpen(false)} + ownFocus={true}> + + {selectedRows.size} {selectedRows.size > 1 ? 'items' : 'item'} + +
+ + + +
+
+ ); + } else { + return null; + } +}; + const SelectionHeaderCell = () => { const [selectedRows, updateSelectedRows] = useContext(SelectionContext); const isIndeterminate = @@ -144,6 +210,7 @@ const trailingControlColumns = [ setIsPopoverOpen(false)} ownFocus={true}> - - - + Actions +
+ + + +
); @@ -224,6 +322,9 @@ export default function DataGrid() { onChangeItemsPerPage: setPageSize, onChangePage: setPageIndex, }} + toolbarVisibility={{ + additionalControls: , + }} />
diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 63a59cb70f4..6d64fcac3aa 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -183,6 +183,9 @@ Array [ class="euiDataGrid__controls" data-test-sub="dataGridControls" > +
-
From 2683f5224a58f81e27328c97df00211c29f546e0 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Thu, 13 Feb 2020 06:34:01 -0800 Subject: [PATCH 06/13] snaps --- .../__snapshots__/data_grid.test.tsx.snap | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 6d64fcac3aa..a999237e25a 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -748,6 +748,9 @@ Array [ class="euiDataGrid__controls" data-test-sub="dataGridControls" > +
- + + +
); diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index 376b7e4b713..5139383942b 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -52,7 +52,7 @@ const gridSnippet = ` id: 'selection', width: 31, headerCellRender: () => Select a Row, - rowCellRender: () =>
, + rowCellRender: () =>
, }, ]} trailingControlColumns={[ @@ -178,6 +178,16 @@ const gridConcepts = [ ), }, + { + title: 'leading and trailing controlColumns', + description: ( + + An array of EuiDataGridControlColumn objects. Used to + define ancillary columns on the left side of the data grid. Useful for + adding items like checkboxes and buttons. + + ), + }, { title: 'schemaDetectors', description: ( @@ -316,6 +326,13 @@ export const DataGridExample = { can be controlled by the engineer, but augmented by user preference depending upon the features you enable. +
  • + + Control columns + {' '} + allow you to add repeatable actions and controls like checkboxes + or buttons to your grid. +
  • ), From 2c811bbc9fd011a34f7650b12ede900b906a7aeb Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 14 Feb 2020 11:59:40 -0700 Subject: [PATCH 10/13] undo an accidental change that snuck in --- src/components/form/field_search/field_search.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/form/field_search/field_search.tsx b/src/components/form/field_search/field_search.tsx index 584e38fdce1..b88728504b1 100644 --- a/src/components/form/field_search/field_search.tsx +++ b/src/components/form/field_search/field_search.tsx @@ -173,7 +173,7 @@ export class EuiFieldSearch extends Component { fullWidth={fullWidth} isLoading={isLoading} clear={ - isClearable && this.inputElement && this.inputElement.value && !rest.readOnly && !rest.disabled + isClearable && value && !rest.readOnly && !rest.disabled ? { onClick: this.onClear } : undefined } From 3c4d059ff5cbcfbda2432e3b2053b7b3bc451929 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 14 Feb 2020 13:48:05 -0700 Subject: [PATCH 11/13] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index baf6c159505..842b5e60a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Converted `EuiFilePicker` to TypeScript ([#2832](https://github.com/elastic/eui/issues/2832)) - Exported `EuiSelectOptionProps` type ([#2830](https://github.com/elastic/eui/pull/2830)) +- Added control columns to `EuiDataGrid` to support non-data columns like row selection and actions ([#2846](https://github.com/elastic/eui/pull/2846)) ## [`19.0.0`](https://github.com/elastic/eui/tree/v19.0.0) From 097a4ace7fc054e35d143799970d07214117b04f Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Tue, 18 Feb 2020 11:25:43 -0800 Subject: [PATCH 12/13] fix docs --- src-docs/src/views/datagrid/control_columns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-docs/src/views/datagrid/control_columns.js b/src-docs/src/views/datagrid/control_columns.js index 406826366fb..171f08c73ca 100644 --- a/src-docs/src/views/datagrid/control_columns.js +++ b/src-docs/src/views/datagrid/control_columns.js @@ -87,7 +87,7 @@ const SelectionButton = () => { button={ setIsPopoverOpen(!isPopoverOpen)}> From bdaaee63b4554c3eb3d7bf6f96a423cfd7f07b5d Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 19 Feb 2020 09:08:53 -0700 Subject: [PATCH 13/13] AriaAttributes instead of HTMLAttribute to access aria attributes --- src/components/datagrid/data_grid_header_cell.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/datagrid/data_grid_header_cell.tsx b/src/components/datagrid/data_grid_header_cell.tsx index 717ded94910..4c1279bc9b0 100644 --- a/src/components/datagrid/data_grid_header_cell.tsx +++ b/src/components/datagrid/data_grid_header_cell.tsx @@ -1,4 +1,5 @@ import React, { + AriaAttributes, FunctionComponent, HTMLAttributes, useEffect, @@ -45,8 +46,8 @@ export const EuiDataGridHeaderCell: FunctionComponent< const width = columnWidths[id] || defaultColumnWidth; const ariaProps: { - 'aria-sort'?: HTMLAttributes['aria-sort']; - 'aria-describedby'?: HTMLAttributes['aria-describedby']; + 'aria-sort'?: AriaAttributes['aria-sort']; + 'aria-describedby'?: AriaAttributes['aria-describedby']; } = {}; let screenReaderId;