From 2142dfcd48cd64a1c424fa9d32425853fc24ed89 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 13 Sep 2019 10:01:46 -0600 Subject: [PATCH] EuiDataGrid hooks cleanup (#2331) * Refactored EuiDataGrid's hooks * Fix datagrid to react to gridStyle changes --- src-docs/src/views/datagrid/styling.js | 11 +- .../__snapshots__/data_grid.test.tsx.snap | 8 - src/components/datagrid/data_grid.tsx | 214 +++++++++++------- src/components/datagrid/data_grid_body.tsx | 4 + src/components/datagrid/data_grid_cell.tsx | 2 +- .../datagrid/data_grid_data_row.tsx | 6 +- .../datagrid/data_grid_header_row.tsx | 6 +- src/components/datagrid/style_selector.tsx | 87 +++---- 8 files changed, 180 insertions(+), 158 deletions(-) diff --git a/src-docs/src/views/datagrid/styling.js b/src-docs/src/views/datagrid/styling.js index f9302702ce3..300413c5eba 100644 --- a/src-docs/src/views/datagrid/styling.js +++ b/src-docs/src/views/datagrid/styling.js @@ -135,8 +135,8 @@ export default class DataGrid extends Component { borderSelected: 'none', fontSizeSelected: 's', cellPaddingSelected: 's', - stripesSelected: 'true', - rowHoverSelected: 'underline', + stripesSelected: true, + rowHoverSelected: 'highlight', isPopoverOpen: false, headerSelected: 'shade', @@ -167,8 +167,7 @@ export default class DataGrid extends Component { onStripesChange = optionId => { this.setState({ - stripesSelected: optionId, - stripes: !this.state.stripes, + stripesSelected: optionId === 'true', }); }; @@ -259,7 +258,7 @@ export default class DataGrid extends Component { @@ -294,7 +293,7 @@ export default class DataGrid extends Component { border: this.state.borderSelected, fontSize: this.state.fontSizeSelected, cellPadding: this.state.cellPaddingSelected, - stripes: this.state.stripes, + stripes: this.state.stripesSelected, rowHover: this.state.rowHoverSelected, header: this.state.headerSelected, }} diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 71b200daef1..3dd2294ec54 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -289,7 +289,6 @@ Array [ class="euiDataGridHeaderCell" data-test-subj="dataGridHeaderCell-A" role="columnheader" - style="width:undefinedpx" >
{ + if (element.getAttribute('role') !== 'gridcell') { + return false; + } + return Boolean(element.querySelector(`[${CELL_CONTENTS_ATTR}="true"]`)); +}; + function computeVisibleRows(props: EuiDataGridProps) { const { pagination, rowCount } = props; @@ -213,62 +220,83 @@ function renderSorting(props: EuiDataGridProps) { ); } -export const EuiDataGrid: FunctionComponent = props => { - const [isFullScreen, setIsFullScreen] = useState(false); - const [showGridControls, setShowGridControls] = useState(true); - const [focusedCell, setFocusedCell] = useState<[number, number]>(ORIGIN); - const containerRef = useRef(null); - const [interactiveCellId] = useState(htmlIdGenerator()()); - const [columnWidths, setColumnWidths] = useState({}); - const setColumnWidth = (columnId: string, width: number) => { - setColumnWidths({ ...columnWidths, [columnId]: width }); - }; +function useDefaultColumnWidth( + container: HTMLElement | null, + columns: EuiDataGridProps['columns'] +): number | null { + const [defaultColumnWidth, setDefaultColumnWidth] = useState( + null + ); useEffect(() => { - if (containerRef.current != null) { - const gridWidth = containerRef.current.clientWidth; - const columnWidth = Math.max(gridWidth / props.columns.length, 100); - const columnWidths = props.columns.reduce( - (columnWidths: EuiDataGridColumnWidths, column) => { - columnWidths[column.id] = columnWidth; - return columnWidths; - }, - {} - ); - setColumnWidths(columnWidths); + if (container != null) { + const gridWidth = container.clientWidth; + const columnWidth = Math.max(gridWidth / columns.length, 100); + setDefaultColumnWidth(columnWidth); } - // @TODO: come back to this hook lifecycle - }, []); // eslint-disable-line react-hooks/exhaustive-deps + }, [container, columns]); + + return defaultColumnWidth; +} - const onResize = ({ width }: { width: number }) => { - setShowGridControls( - width > MINIMUM_WIDTH_FOR_GRID_CONTROLS || isFullScreen - ); +function useColumnWidths(): [ + EuiDataGridColumnWidths, + (columnId: string, width: number) => void +] { + const [columnWidths, setColumnWidths] = useState({}); + const setColumnWidth = (columnId: string, width: number) => { + setColumnWidths({ ...columnWidths, [columnId]: width }); }; + return [columnWidths, setColumnWidth]; +} - const [isGridNavigationEnabled, setIsGridNavigationEnabled] = useState< - boolean - >(true); +function useOnResize( + setShowGridControls: (showGridControls: boolean) => void, + isFullScreen: boolean +) { + return useCallback( + ({ width }: { width: number }) => { + setShowGridControls( + width > MINIMUM_WIDTH_FOR_GRID_CONTROLS || isFullScreen + ); + }, + [setShowGridControls, isFullScreen] + ); +} - const isInteractiveCell = (element: HTMLElement) => { - if (element.getAttribute('role') !== 'gridcell') { - return false; - } +function useInMemoryValues(): [ + EuiDataGridInMemoryValues, + (rowIndex: number, column: EuiDataGridColumn, value: string) => void +] { + const [inMemoryValues, setInMemoryValues] = useState< + EuiDataGridInMemoryValues + >({}); - return Boolean(element.querySelector(`[${CELL_CONTENTS_ATTR}="true"]`)); - }; + const onCellRender = useCallback( + (rowIndex, column, value) => { + setInMemoryValues(inMemoryValues => { + const nextInMemoryVaues = { ...inMemoryValues }; + nextInMemoryVaues[rowIndex] = nextInMemoryVaues[rowIndex] || {}; + nextInMemoryVaues[rowIndex][column.id] = value; + return nextInMemoryVaues; + }); + }, + [inMemoryValues, setInMemoryValues] + ); - const handleGridKeyDown = (e: KeyboardEvent) => { - switch (e.keyCode) { - case keyCodes.ESCAPE: - e.preventDefault(); - setIsFullScreen(false); - break; - } - }; + return [inMemoryValues, onCellRender]; +} - const handleKeyDown = (event: KeyboardEvent) => { - const colCount = props.columns.length - 1; +function createKeyDownHandler( + props: EuiDataGridProps, + visibleColumns: EuiDataGridProps['columns'], + focusedCell: [number, number], + setFocusedCell: (focusedCell: [number, number]) => void, + isGridNavigationEnabled: boolean, + setIsGridNavigationEnabled: (isGridNavigationEnabled: boolean) => void +) { + return (event: KeyboardEvent) => { + const colCount = visibleColumns.length - 1; const [x, y] = focusedCell; const rowCount = computeVisibleRows(props); const { keyCode, target } = event; @@ -317,39 +345,58 @@ export const EuiDataGrid: FunctionComponent = props => { } } }; +} + +export const EuiDataGrid: FunctionComponent = props => { + const [isFullScreen, setIsFullScreen] = useState(false); + const [showGridControls, setShowGridControls] = useState(true); + const [focusedCell, setFocusedCell] = useState<[number, number]>(ORIGIN); + const [containerRef, setContainerRef] = useState(null); + const [interactiveCellId] = useState(htmlIdGenerator()()); + + const [columnWidths, setColumnWidth] = useColumnWidths(); + + // 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: + if (isFullScreen) { + e.preventDefault(); + setIsFullScreen(false); + } + break; + } + }; const { columns, rowCount, renderCellValue, className, - gridStyle = startingStyles, + gridStyle, pagination, sorting, inMemory = false, ...rest } = props; + // apply style props on top of defaults + const gridStyleWithDefaults = { ...startingStyles, ...gridStyle }; + const [ColumnSelector, visibleColumns] = useColumnSelector(columns); - const [StyleSelector, gridStyles, setGridStyles] = useStyleSelector(); + const [StyleSelector, gridStyles] = useStyleSelector(gridStyleWithDefaults); - useEffect(() => { - if (gridStyle) { - const oldStyles = gridStyles; - /*eslint-disable */ - const mergedStyle = Object.assign( - /*eslint-enable */ - {}, - oldStyles, - // @ts-ignore - gridStyle - ); - setGridStyles(mergedStyle); - } else { - setGridStyles(startingStyles); - } - // @TODO: come back to this hook lifecycle - }, [gridStyle]); // eslint-disable-line react-hooks/exhaustive-deps + // compute the default column width from the container's clientWidth and count of visible columns + const defaultColumnWidth = useDefaultColumnWidth( + containerRef, + visibleColumns + ); const classes = classNames( 'euiDataGrid', @@ -375,21 +422,8 @@ export const EuiDataGrid: FunctionComponent = props => { className ); - const [inMemoryValues, setInMemoryValues] = useState< - EuiDataGridInMemoryValues - >({}); + const [inMemoryValues, onCellRender] = useInMemoryValues(); - const onCellRender = useCallback( - (rowIndex, column, value) => { - setInMemoryValues(inMemoryValues => { - const nextInMemoryVaues = { ...inMemoryValues }; - nextInMemoryVaues[rowIndex] = nextInMemoryVaues[rowIndex] || {}; - nextInMemoryVaues[rowIndex][column.id] = value; - return nextInMemoryVaues; - }); - }, - [inMemoryValues, setInMemoryValues] - ); // These grid controls will only show when there is room. Check the resize observer callback const gridControls = ( @@ -405,8 +439,6 @@ export const EuiDataGrid: FunctionComponent = props => { document.body.classList.remove('euiDataGrid__restrictBody'); } - const onCellFocus = useCallback(setFocusedCell, [setFocusedCell]); - // extract aria-label and/or aria-labelledby from `rest` const gridAriaProps: { 'aria-label'?: string; @@ -423,7 +455,10 @@ export const EuiDataGrid: FunctionComponent = props => { return ( -
+
{showGridControls ? gridControls : null} = props => { {resizeRef => (
@@ -470,15 +512,17 @@ export const EuiDataGrid: FunctionComponent = props => { = props => { const { columnWidths, + defaultColumnWidth, columns, focusedCell, onCellFocus, @@ -136,6 +138,7 @@ export const EuiDataGridBody: FunctionComponent< key={rowIndex} columns={columns} columnWidths={columnWidths} + defaultColumnWidth={defaultColumnWidth} focusedCell={focusedCell} onCellFocus={setCellFocus} renderCellValue={renderCellValue} @@ -151,6 +154,7 @@ export const EuiDataGridBody: FunctionComponent< }, [ columns, columnWidths, + defaultColumnWidth, focusedCell, onCellFocus, renderCellValue, diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 17d66e27fe9..c13c40fe197 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -196,7 +196,7 @@ export class EuiDataGridCell extends Component< className="euiDataGridRowCell" data-test-subj="dataGridRowCell" onFocus={() => onCellFocus([colIndex, rowIndex])} - style={{ width: `${width}px` }}> + style={width != null ? { width: `${width}px` } : {}}> { diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index c388b16fe36..4a0aa1525de 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -10,6 +10,7 @@ export type EuiDataGridDataRowProps = CommonProps & rowIndex: number; columns: EuiDataGridColumn[]; columnWidths: EuiDataGridColumnWidths; + defaultColumnWidth?: number | null; focusedCell: [number, number]; renderCellValue: EuiDataGridCellProps['renderCellValue']; isGridNavigationEnabled: EuiDataGridCellProps['isGridNavigationEnabled']; @@ -24,6 +25,7 @@ const EuiDataGridDataRow: FunctionComponent< const { columns, columnWidths, + defaultColumnWidth, className, renderCellValue, rowIndex, @@ -44,7 +46,7 @@ const EuiDataGridDataRow: FunctionComponent< {columns.map((props, i) => { const { id } = props; - const width = columnWidths[id]; + const width = columnWidths[id] || defaultColumnWidth; const isFocusable = focusedCell[0] === i && focusedCell[1] === visibleRowIndex; @@ -55,7 +57,7 @@ const EuiDataGridDataRow: FunctionComponent< rowIndex={rowIndex} colIndex={i} columnId={id} - width={width} + width={width || undefined} renderCellValue={renderCellValue} onCellFocus={onCellFocus} isFocusable={isFocusable} diff --git a/src/components/datagrid/data_grid_header_row.tsx b/src/components/datagrid/data_grid_header_row.tsx index 346cebde7d7..e415e7ca6d4 100644 --- a/src/components/datagrid/data_grid_header_row.tsx +++ b/src/components/datagrid/data_grid_header_row.tsx @@ -14,6 +14,7 @@ type EuiDataGridHeaderRowProps = CommonProps & HTMLAttributes & { columns: EuiDataGridColumn[]; columnWidths: EuiDataGridColumnWidths; + defaultColumnWidth?: number | null; setColumnWidth: (columnId: string, width: number) => void; sorting?: EuiDataGridSorting; }; @@ -24,6 +25,7 @@ const EuiDataGridHeaderRow: FunctionComponent< const { columns, columnWidths, + defaultColumnWidth, className, setColumnWidth, sorting, @@ -39,7 +41,7 @@ const EuiDataGridHeaderRow: FunctionComponent< {columns.map(props => { const { id } = props; - const width = columnWidths[id]; + const width = columnWidths[id] || defaultColumnWidth; const ariaProps: { 'aria-sort'?: HTMLAttributes['aria-sort']; @@ -82,7 +84,7 @@ const EuiDataGridHeaderRow: FunctionComponent< key={id} className="euiDataGridHeaderCell" data-test-subj={`dataGridHeaderCell-${id}`} - style={{ width: `${width}px` }}> + style={width != null ? { width: `${width}px` } : {}}> {width ? ( , - EuiDataGridStyle, - Dispatch> -] => { - const [gridStyles, setGridStyles] = useState(startingStyles); +const densityStyles: { [key: string]: Partial } = { + expanded: { + fontSize: 'l', + cellPadding: 'l', + }, + normal: { + fontSize: 'm', + cellPadding: 'm', + }, + compact: { + fontSize: 's', + cellPadding: 's', + }, +}; - const [isOpen, setIsOpen] = useState(false); +export const useStyleSelector = ( + initialStyles: EuiDataGridStyle +): [FunctionComponent<{}>, EuiDataGridStyle] => { + // track styles specified by the user at run time + const [userGridStyles, setUserGridStyles] = useState({}); - const densityStyles = { - expanded: { - fontSize: 'l', - cellPadding: 'l', - }, - normal: { - fontSize: 'm', - cellPadding: 'm', - }, - compact: { - fontSize: 's', - cellPadding: 's', - }, - }; + const [isOpen, setIsOpen] = useState(false); // These are the available options. They power the gridDensity hook and also the options in the render const densityOptions: string[] = ['expanded', 'normal', 'compact']; - // Normal is the defaul density - const [gridDensity, setGridDensity] = useState(densityOptions[1]); - - const onChangeDensity = (optionId: string) => { - const selectedDensity = densityOptions.filter(options => { - return options === optionId; - })[0]; - - setGridDensity(selectedDensity); + // Normal is the default density + const [gridDensity, _setGridDensity] = useState(densityOptions[1]); + const setGridDensity = (density: string) => { + _setGridDensity(density); + setUserGridStyles(densityStyles[density]); }; - useEffect(() => { - const oldStyles = gridStyles; - - // eslint doesn't like the way the object.assign here is set up. - /*eslint-disable */ - const mergedStyle = Object.assign( - {}, - oldStyles, - // @ts-ignore - densityStyles[gridDensity] - ); - /*eslint-enable */ - setGridStyles(mergedStyle); - // @TODO: come back to this hook lifecycle - }, [gridDensity]); // eslint-disable-line react-hooks/exhaustive-deps + // merge the developer-specified styles with any user overrides + const gridStyles = { + ...initialStyles, + ...userGridStyles, + }; const StyleSelector = () => ( @@ -143,5 +122,5 @@ export const useStyleSelector = (): [ ); - return [StyleSelector, gridStyles, setGridStyles]; + return [StyleSelector, gridStyles]; };