diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e68d3acda..78c8e666a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ - Updated the organization of `EuiDataGrid`'s toolbar/grid controls ([#5334](https://github.com/elastic/eui/pull/5334)) - Added `left.append` and `left.prepend` to `EuiDataGrid`'s `toolbarVisibility.additionalControls` prop [#5394](https://github.com/elastic/eui/pull/5394)) +- Added a row height control to `EuiDataGrid`'s toolbar ([#5372](https://github.com/elastic/eui/pull/5372)) **Bug fixes** - Fixed persistent `EuiDataGrid` full screen `` class ([#5354](https://github.com/elastic/eui/pull/5354)) +**Breaking changes** + +- Removed `toolbarVisibility`'s `showStyleSelector` prop of `EuiDataGrid` in favor of `showDisplaySelector`, which allows configuration of both grid density and row height ([#5372](https://github.com/elastic/eui/pull/5372)) + ## END FEATURE BRANCH **Bug fixes** diff --git a/cypress/support/index.js b/cypress/support/index.js index 9bc34e3acd7..6ba42a3fcf9 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -15,3 +15,10 @@ import '@cypress/code-coverage/support'; require(THEME_IMPORT); // defined by DefinePlugin in the cypress webpack config + +// @see https://github.com/quasarframework/quasar/issues/2233#issuecomment-492975745 +Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('> ResizeObserver loop limit exceeded')) { + return false; + } +}); diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index 1d8d6938431..5aba7eaecf3 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -97,7 +97,7 @@ const gridSnippet = ` // The prop also accepts a boolean if you want to toggle the entire toolbar on/off. toolbarVisibility={{ showColumnSelector: false, - showStyleSelector: false, + showDisplaySelector: false, showSortSelector: false, showFullScreenSelector: false, additionalControls: { @@ -248,6 +248,13 @@ const gridConcepts = [ Data grid row heights options {' '} for more details and examples. +
+ Settings provided may be overwritten or merged with user defined + preferences if{' '} + + toolbarVisibility.showDisplaySelector.allowRowHeight + {' '} + is set to true (which is the default). ), }, @@ -256,10 +263,16 @@ const gridConcepts = [ description: ( Defines the look of the grid. Accepts a partial{' '} - EuiDataGridStyle object. Settings provided may be - overwritten or merged with user defined preferences if{' '} - toolbarVisibility.showStyleSelector is set to true - (which is the default). + EuiDataGridStyle object. See{' '} + + Data grid styling and control + {' '} + for more details and examples. +
+ Settings provided may be overwritten or merged with user defined + preferences if{' '} + toolbarVisibility.showDisplaySelector.allowDensity is + set to true (which is the default).
), }, diff --git a/src-docs/src/views/datagrid/datagrid_height_options_example.js b/src-docs/src/views/datagrid/datagrid_height_options_example.js index 924c0b2838b..fcaf9d43aa8 100644 --- a/src-docs/src/views/datagrid/datagrid_height_options_example.js +++ b/src-docs/src/views/datagrid/datagrid_height_options_example.js @@ -75,6 +75,9 @@ const rowHeightsFullSnippet = `const rowHeightsOptions = useMemo( inMemory={{ level: 'sorting' }} sorting={{ columns: sortingColumns, onSort }} rowHeightsOptions={rowHeightsOptions} + toolbarVisibility={{ + showDisplaySelelector: { allowRowHeight: false }, + }} pagination={{ ...pagination, pageSizeOptions: [50, 250, 1000], @@ -195,26 +198,26 @@ export const DataGridRowHeightOptionsExample = { text: (

- You can change the default height for all rows by passing a line - count or pixel value to the defaultHeight{' '} - property, and customize the line height of all cells with the{' '} - lineHeight property. + You can change the default height for all rows via the{' '} + defaultHeight property. Note that the{' '} + showDisplaySelector.allowRowHeight setting in{' '} + toolbarVisibility means the user has the ability + to override this default height. Users will be able to toggle + between single rows, a configurable line count, or{' '} + "auto". +

+

+ You can also customize the line height of all cells with the{' '} + lineHeight property. However, if you wrap your + cell content with CSS that overrides/sets line-height (e.g. in an{' '} + EuiText), your row heights will not be calculated + correctly - make sure to match the passed{' '} + lineHeight property to the actual cell content + line height.

{lineHeightSnippet} - -

- If you wrap your cell content with CSS that overrides/sets - line-height (e.g. in an EuiText), your row - heights will not be calculated correctly! Make sure to match or - inherit the passed lineHeight property to the - actual cell content line height. -

-
), components: { DataGridRowLineHeight }, @@ -243,6 +246,18 @@ export const DataGridRowHeightOptionsExample = { {rowHeightsSnippet} + + When using rowHeights overrides, we recommend + setting{' '} + + toolbarVisibility.showDisplaySelector.allowRowHeight + {' '} + to false, as users will otherwise be confused + when switching row heights does not affect specific overriden rows. + ), components: { DataGridRowHeightOptions }, diff --git a/src-docs/src/views/datagrid/datagrid_styling_example.js b/src-docs/src/views/datagrid/datagrid_styling_example.js index 359f0d0cb6f..971f14a2fdb 100644 --- a/src-docs/src/views/datagrid/datagrid_styling_example.js +++ b/src-docs/src/views/datagrid/datagrid_styling_example.js @@ -56,7 +56,7 @@ const gridSnippet = `

- With the default settings, the showStyleSelector{' '} - setting in toolbarVisibility means the user has - the ability to override the padding and font size passed into{' '} + With the default settings, the{' '} + showDisplaySelector.allowDensity setting in{' '} + toolbarVisibility means the user has the ability + to override the padding and font size passed into{' '} gridStyle by the engineer. The font size overriding only works with text or elements that can inherit the parent font size or elements that use units relative to the parent diff --git a/src-docs/src/views/datagrid/row_height_fixed.tsx b/src-docs/src/views/datagrid/row_height_fixed.tsx index 91df500a1ca..ff6230e1e6a 100644 --- a/src-docs/src/views/datagrid/row_height_fixed.tsx +++ b/src-docs/src/views/datagrid/row_height_fixed.tsx @@ -215,6 +215,9 @@ export default () => { inMemory={{ level: 'sorting' }} sorting={{ columns: sortingColumns, onSort }} rowHeightsOptions={rowHeightsOptions} + toolbarVisibility={{ + showDisplaySelector: { allowRowHeight: false }, + }} virtualizationOptions={{ // rough average of the cell heights in the example // accurately setting this smooths out the scrolling experience diff --git a/src-docs/src/views/datagrid/styling.js b/src-docs/src/views/datagrid/styling.js index 0750486c839..addddfae77d 100644 --- a/src-docs/src/views/datagrid/styling.js +++ b/src-docs/src/views/datagrid/styling.js @@ -1,4 +1,4 @@ -import React, { useState, Fragment, useCallback } from 'react'; +import React, { useState, Fragment, useCallback, useMemo } from 'react'; import { fake } from 'faker'; import { @@ -155,7 +155,7 @@ const DataGrid = () => { }, ]; - const showSortSelectorOptions = [ + const showColumnSelectorOptions = [ { id: 'true', label: 'True', @@ -165,8 +165,17 @@ const DataGrid = () => { label: 'False', }, ]; - - const showStyleSelectorOptions = [ + const allowHideColumnsOptions = [ + { + id: 'true', + label: 'True', + }, + { + id: 'false', + label: 'False', + }, + ]; + const allowOrderingColumnsOptions = [ { id: 'true', label: 'True', @@ -177,7 +186,7 @@ const DataGrid = () => { }, ]; - const showColumnSelectorOptions = [ + const showSortSelectorOptions = [ { id: 'true', label: 'True', @@ -188,7 +197,7 @@ const DataGrid = () => { }, ]; - const allowHideColumnsOptions = [ + const showDisplaySelectorOptions = [ { id: 'true', label: 'True', @@ -199,7 +208,17 @@ const DataGrid = () => { }, ]; - const allowOrderingColumnsOptions = [ + const allowDensityOptions = [ + { + id: 'true', + label: 'True', + }, + { + id: 'false', + label: 'False', + }, + ]; + const allowRowHeightOptions = [ { id: 'true', label: 'True', @@ -252,7 +271,9 @@ const DataGrid = () => { const [headerSelected, setHeaderSelected] = useState('underline'); const [footerSelected, setFooterSelected] = useState('overline'); const [showSortSelector, setShowSortSelector] = useState(true); - const [showStyleSelector, setShowStyleSelector] = useState(true); + const [showDisplaySelector, setShowDisplaySelector] = useState(true); + const [allowDensity, setAllowDensity] = useState(true); + const [allowRowHeight, setAllowRowHeight] = useState(true); const [showColumnSelector, setShowColumnSelector] = useState(true); const [allowHideColumns, setAllowHideColumns] = useState(true); const [allowOrderingColumns, setAllowOrderingColumns] = useState(true); @@ -297,26 +318,30 @@ const DataGrid = () => { setFooterSelected(optionId); }; - const onShowSortSelectorChange = (optionId) => { - setShowSortSelector(optionId === 'true'); - }; - - const onShowStyleSelectorChange = (optionId) => { - setShowStyleSelector(optionId === 'true'); - }; - const onShowColumnSelectorChange = (optionId) => { setShowColumnSelector(optionId === 'true'); }; - const onAllowHideColumnsChange = (optionId) => { setAllowHideColumns(optionId === 'true'); }; - const onAllowOrderingColumnsChange = (optionId) => { setAllowOrderingColumns(optionId === 'true'); }; + const onShowSortSelectorChange = (optionId) => { + setShowSortSelector(optionId === 'true'); + }; + + const onShowDisplaySelectorChange = (optionId) => { + setShowDisplaySelector(optionId === 'true'); + }; + const onAllowDensityChange = (optionId) => { + setAllowDensity(optionId === 'true'); + }; + const onAllowRowHeightChange = (optionId) => { + setAllowRowHeight(optionId === 'true'); + }; + const onShowFullScreenSelectorChange = (optionId) => { setShowFullScreenSelector(optionId === 'true'); }; @@ -389,20 +414,35 @@ const DataGrid = () => { toolbarVisibility options ); - let displayColumnSelector = showColumnSelector; - if ( - displayColumnSelector === true && - (allowHideColumns === false || allowOrderingColumns === false) - ) { - displayColumnSelector = { - allowHide: allowHideColumns, - allowReorder: allowOrderingColumns, - }; - } + + const toggleColumnSelector = useMemo(() => { + if ( + showColumnSelector === true && + (allowHideColumns === false || allowOrderingColumns === false) + ) { + return { + allowHide: allowHideColumns, + allowReorder: allowOrderingColumns, + }; + } else { + return showColumnSelector; + } + }, [showColumnSelector, allowHideColumns, allowOrderingColumns]); + + const toggleDisplaySelector = useMemo(() => { + if ( + showDisplaySelector === true && + (allowDensity === false || allowRowHeight === false) + ) { + return { allowDensity, allowRowHeight }; + } else { + return showDisplaySelector; + } + }, [showDisplaySelector, allowDensity, allowRowHeight]); const toolbarVisibilityOptions = { - showColumnSelector: displayColumnSelector, - showStyleSelector: showStyleSelector, + showColumnSelector: toggleColumnSelector, + showDisplaySelector: toggleDisplaySelector, showSortSelector: showSortSelector, showFullScreenSelector: showFullScreenSelector, }; @@ -453,7 +493,7 @@ const DataGrid = () => { { { + {toggleColumnSelector && ( + <> + + + + + + + + )} { { - - - - - {displayColumnSelector && ( + {toggleDisplaySelector && ( <> )} + + + + ) : (

* { max-width: 100%; width: 100%; + height: 100%; } &.euiDataGridRowCell--firstColumn { @@ -139,8 +140,12 @@ .euiDataGridRowCell__expandFlex { position: relative; // for positioning expand button display: flex; - align-items: center; + align-items: baseline; height: 100%; + + .euiDataGridRowCell--controlColumn & { + align-items: center; + } } .euiDataGridRowCell__expandContent { @@ -152,21 +157,21 @@ height: 100%; } -.euiDataGridRowCell__alignBaseLine { - align-items: baseline; -} - +// Cell actions +// Could probably be more precisely named than '__expandButton', since there can be multiple actions/buttons +// TODO: Consider renaming this when working on https://github.com/elastic/eui/issues/5132 .euiDataGridRowCell__expandButton { display: flex; +} +@include euiDataGridRowCellActions($definedHeight: false) { flex-grow: 0; - - .euiDataGridRowCell__contentByHeight + & { - background-color: $euiColorEmptyShade; - position: absolute; - right: 0; - top: 0; - padding: $euiDataGridCellPaddingM 0; - } +} +@include euiDataGridRowCellActions($definedHeight: true) { + background-color: $euiColorEmptyShade; + position: absolute; + right: 0; + top: 0; + padding: $euiDataGridCellPaddingM 0; } .euiDataGridRowCell__expandButtonIcon { @@ -202,6 +207,11 @@ // Needed to overtake striping background-color: $euiColorHighlight !important; } + @include euiDataGridRowCellActions($definedHeight: true) { + // sass-lint:disable-block no-important + // Needed to overtake striping + background-color: $euiColorHighlight !important; + } } } @@ -209,6 +219,9 @@ @include euiDataGridStyles(stripes) { @include euiDataGridRowCell { &.euiDataGridRowCell--stripe { + @include euiDataGridRowCellActions($definedHeight: true) { + background-color: $euiColorLightestShade; + } background: $euiColorLightestShade; } } @@ -255,6 +268,16 @@ } } +// Compressed density grids - height tweaks +@include euiDataGridStyles(fontSizeSmall, paddingSmall) { + @include euiDataGridRowCellActions($definedHeight: true) { + padding: ($euiDataGridCellPaddingS / 2) 0; + } + @include euiDataGridRowCellActions($definedHeight: false) { + transform: translateY(1px); + } +} + @keyframes euiDataGridCellButtonSlideIn { from { margin-left: 0; diff --git a/src/components/datagrid/_mixins.scss b/src/components/datagrid/_mixins.scss index c50a74f26b3..87f53255081 100644 --- a/src/components/datagrid/_mixins.scss +++ b/src/components/datagrid/_mixins.scss @@ -82,3 +82,18 @@ $euiDataGridStyles: ( @content; } } + +@mixin euiDataGridRowCellActions($definedHeight: false) { + @if $definedHeight { + // Defined heights are cells with row heights of auto, lineCount, or a static height + // that set the __contentByHeight class + .euiDataGridRowCell__contentByHeight + .euiDataGridRowCell__expandButton { + @content; + } + } @else { + // Otherwise, an undefined height (single flex row) will set __expandContent + .euiDataGridRowCell__expandContent + .euiDataGridRowCell__expandButton { + @content; + } + } +} diff --git a/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap b/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap index a2fce7944c6..0f574bea5be 100644 --- a/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap +++ b/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap @@ -67,6 +67,7 @@ exports[`EuiDataGridCell renders 1`] = ` { }); }); - describe('recalculateLineCountHeight', () => { + describe('recalculateLineHeight', () => { const setRowHeight = jest.fn(); const callMethod = (component: ReactWrapper) => - (component.instance() as any).recalculateLineCountHeight(); + (component.instance() as any).recalculateLineHeight(); describe('default height', () => { it('observes the first cell for size changes and calls this.props.setRowHeight on change', () => { @@ -245,7 +245,32 @@ describe('EuiDataGridCell', () => { }); }); - it('does nothing if cell height is not set to lineCount', () => { + it('recalculates when rowHeightsOptions.defaultHeight.lineCount changes', () => { + const component = mountEuiDataGridCellWithContext({ + rowHeightsOptions: { defaultHeight: { lineCount: 7 } }, + setRowHeight, + }); + + component.setProps({ + rowHeightsOptions: { defaultHeight: { lineCount: 6 } }, + }); + expect(setRowHeight).toHaveBeenCalled(); + }); + + it('calculates undefined heights as single rows with a lineCount of 1', () => { + const component = mountEuiDataGridCellWithContext({ + rowHeightsOptions: { defaultHeight: undefined }, + setRowHeight, + }); + + callMethod(component); + expect( + mockRowHeightUtils.calculateHeightForLineCount + ).toHaveBeenCalledWith(expect.any(HTMLElement), 1, false); + expect(setRowHeight).toHaveBeenCalled(); + }); + + it('does nothing if cell height is not lineCount or undefined', () => { const component = mountEuiDataGridCellWithContext({ rowHeightsOptions: { defaultHeight: 34 }, setRowHeight, diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index a3d20d06672..9b253a403d4 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -39,8 +39,9 @@ import { IS_JEST_ENVIRONMENT } from '../../../test'; const EuiDataGridCellContent: FunctionComponent< EuiDataGridCellValueProps & { setCellProps: EuiDataGridCellValueElementProps['setCellProps']; - isExpanded: boolean; setCellContentsRef: EuiDataGridCell['setCellContentsRef']; + isExpanded: boolean; + isDefinedHeight: boolean; } > = memo( ({ @@ -51,6 +52,7 @@ const EuiDataGridCellContent: FunctionComponent< rowIndex, colIndex, rowHeightUtils, + isDefinedHeight, ...rest }) => { // React is more permissible than the TS types indicate @@ -64,11 +66,6 @@ const EuiDataGridCellContent: FunctionComponent< { row: rowIndex + 1, col: colIndex + 1 } ); - const isDefinedHeight = !!rowHeightUtils?.getRowHeightOption( - rowIndex, - rowHeightsOptions - ); - return ( <>
{ + recalculateLineHeight = () => { if (!this.props.setRowHeight) return; // setRowHeight is only passed by data_grid_body into one cell per row if (!this.cellContentsRef) return; @@ -196,7 +193,10 @@ export class EuiDataGridCell extends Component< rowIndex, rowHeightsOptions ); - const lineCount = rowHeightUtils?.getLineCount(rowHeightOption); + const isSingleLine = rowHeightOption == null; // Undefined rowHeightsOptions default to a single line + const lineCount = isSingleLine + ? 1 + : rowHeightUtils?.getLineCount(rowHeightOption); if (lineCount) { const shouldUseHeightsCache = rowHeightUtils?.isRowHeightOverride( @@ -249,6 +249,13 @@ export class EuiDataGridCell extends Component< componentDidUpdate(prevProps: EuiDataGridCellProps) { this.recalculateAutoHeight(); + if ( + this.props.rowHeightsOptions?.defaultHeight !== + prevProps.rowHeightsOptions?.defaultHeight + ) { + this.recalculateLineHeight(); + } + if (this.props.columnId !== prevProps.columnId) { this.setCellProps({}); } @@ -303,7 +310,7 @@ export class EuiDataGridCell extends Component< if (ref && hasResizeObserver) { this.contentObserver = new (window as any).ResizeObserver(() => { this.recalculateAutoHeight(); - this.recalculateLineCountHeight(); + this.recalculateLineHeight(); }); this.contentObserver.observe(ref); } else if (this.contentObserver) { @@ -380,6 +387,7 @@ export class EuiDataGridCell extends Component< className, column, style, + rowHeightUtils, rowHeightsOptions, rowManager, ...rest @@ -474,6 +482,11 @@ export class EuiDataGridCell extends Component< } }; + const isDefinedHeight = !!rowHeightUtils?.getRowHeightOption( + rowIndex, + rowHeightsOptions + ); + const cellContentProps = { ...rest, setCellProps: this.setCellProps, @@ -483,14 +496,13 @@ export class EuiDataGridCell extends Component< isExpanded: this.state.popoverIsOpen, isDetails: false, setCellContentsRef: this.setCellContentsRef, - rowHeightsOptions: this.props.rowHeightsOptions, - rowHeightUtils: this.props.rowHeightUtils, + rowHeightsOptions, + rowHeightUtils, + isDefinedHeight, }; - const anchorClass = classNames('euiDataGridRowCell__expandFlex', { - euiDataGridRowCell__alignBaseLine: this.props.rowHeightsOptions, - }); - const expandClass = this.props.rowHeightsOptions + const anchorClass = 'euiDataGridRowCell__expandFlex'; + const expandClass = isDefinedHeight ? 'euiDataGridRowCell__contentByHeight' : 'euiDataGridRowCell__expandContent'; @@ -501,7 +513,7 @@ export class EuiDataGridCell extends Component< onDeactivation={() => { this.setState({ isEntered: false }, this.preventTabbing); }} - style={this.props.rowHeightsOptions ? { height: '100%' } : {}} + style={isDefinedHeight ? { height: '100%' } : {}} clickOutsideDisables={true} >
@@ -549,7 +561,7 @@ export class EuiDataGridCell extends Component< innerContent = (
} closePopover={[Function]} - data-test-subj="dataGridStyleSelectorPopover" + data-test-subj="dataGridDisplaySelectorPopover" display="inlineBlock" hasArrow={true} isOpen={false} @@ -42,10 +42,32 @@ exports[`useDataGridStyleSelector styleSelector renders a toolbar button/popover } tokens={ Array [ - "euiStyleSelector.densityLabel", - "euiStyleSelector.labelCompact", - "euiStyleSelector.labelNormal", - "euiStyleSelector.labelExpanded", + "euiDisplaySelector.densityLabel", + "euiDisplaySelector.labelCompact", + "euiDisplaySelector.labelNormal", + "euiDisplaySelector.labelExpanded", + ] + } + > + + + diff --git a/src/components/datagrid/controls/column_selector.test.tsx b/src/components/datagrid/controls/column_selector.test.tsx index 31f4957cdfb..7fdb2ae8cd8 100644 --- a/src/components/datagrid/controls/column_selector.test.tsx +++ b/src/components/datagrid/controls/column_selector.test.tsx @@ -83,36 +83,16 @@ describe('useDataGridColumnSelector', () => { closePopover(component); }); - describe('getShowColumnSelectorValue', () => { - it('renders both hiding and reordering functionality when showColumnSelector is true', () => { - const component = mount(); - openPopover(component); - - expect(component.find('EuiSwitch')).toHaveLength(2); - expect(component.find('EuiIcon[type="grab"]')).toHaveLength(2); - }); - - it('defaults to enabling all functionality when showColumnSelector is not defined', () => { - const component = mount( - // @ts-ignore - normally this would be undefined and not null, but we have = fallbacks up above for testing QOL - - ); - openPopover(component); - - expect(component.find('EuiSwitch')).toHaveLength(2); - expect(component.find('EuiIcon[type="grab"]')).toHaveLength(2); - }); - - it('does not render either hiding or reordering when showColumnSelector is false', () => { - const component = mount(); - openPopover(component); - - expect(component.find('EuiSwitch')).toHaveLength(0); - expect(component.find('EuiIcon[type="grab"]')).toHaveLength(0); - expect(component.find('EuiDroppable').prop('isDropDisabled')).toEqual( - true - ); - }); + it('does not render if all valid sub-options are disabled', () => { + const component = shallow( + + ); + expect(component.text()).toEqual(''); }); describe('column filtering', () => { diff --git a/src/components/datagrid/controls/column_selector.tsx b/src/components/datagrid/controls/column_selector.tsx index a701bab82e6..52d22129abd 100644 --- a/src/components/datagrid/controls/column_selector.tsx +++ b/src/components/datagrid/controls/column_selector.tsx @@ -11,16 +11,10 @@ import React, { useState, useMemo, useCallback, - ReactElement, + ReactNode, ChangeEvent, } from 'react'; import classNames from 'classnames'; -import { - EuiDataGridColumn, - EuiDataGridColumnVisibility, - EuiDataGridToolBarVisibilityColumnSelectorOptions, - EuiDataGridToolBarVisibilityOptions, -} from '../data_grid_types'; import { EuiPopover, EuiPopoverFooter, EuiPopoverTitle } from '../../popover'; import { EuiI18n } from '../../i18n'; import { EuiButtonEmpty } from '../../button'; @@ -36,15 +30,12 @@ import { DropResult } from 'react-beautiful-dnd'; import { EuiIcon } from '../../icon'; import { useDependentState } from '../../../services'; -const getShowColumnSelectorValue = ( - showColumnSelector: EuiDataGridToolBarVisibilityOptions['showColumnSelector'], - valueName: keyof EuiDataGridToolBarVisibilityColumnSelectorOptions -) => { - if (showColumnSelector === false) return false; - if (showColumnSelector == null) return true; - if (showColumnSelector === true) return true; - return showColumnSelector[valueName] !== false; -}; +import { + EuiDataGridColumn, + EuiDataGridColumnVisibility, + EuiDataGridToolBarVisibilityOptions, +} from '../data_grid_types'; +import { getNestedObjectOptions } from './data_grid_toolbar'; export const useDataGridColumnSelector = ( availableColumns: EuiDataGridColumn[], @@ -52,16 +43,16 @@ export const useDataGridColumnSelector = ( showColumnSelector: EuiDataGridToolBarVisibilityOptions['showColumnSelector'], displayValues: { [key: string]: string } ): [ - ReactElement, + ReactNode, EuiDataGridColumn[], (columns: string[]) => void, (colFrom: string, colTo: string) => void ] => { - const allowColumnHiding = getShowColumnSelectorValue( + const allowColumnHiding = getNestedObjectOptions( showColumnSelector, 'allowHide' ); - const allowColumnReorder = getShowColumnSelectorValue( + const allowColumnReorder = getNestedObjectOptions( showColumnSelector, 'allowReorder' ); @@ -148,159 +139,163 @@ export const useDataGridColumnSelector = ( ); } - const columnSelector = ( - setIsOpen(false)} - anchorPosition="downLeft" - panelPaddingSize="s" - panelClassName="euiDataGrid__controlPopoverWithDragDrop" - button={ - setIsOpen(!isOpen)} - > - {buttonText} - - } - > -
- {allowColumnHiding && ( - - - {([search, searchcolumns]: string[]) => ( - ) => - setColumnSearchText(e.currentTarget.value) - } - data-test-subj="dataGridColumnSelectorSearch" - /> - )} - - - )} -
- - - - {filteredColumns.map((id, index) => ( - - {(provided, state) => ( -
- - - {allowColumnHiding ? ( - { - const { - target: { checked }, - } = event; - const nextVisibleColumns = sortedColumns.filter( - (columnId) => - checked - ? visibleColumnIds.has(columnId) || - id === columnId - : visibleColumnIds.has(columnId) && - id !== columnId - ); - setVisibleColumns(nextVisibleColumns); - }} - data-test-subj={`dataGridColumnSelectorToggleColumnVisibility-${id}`} - /> - ) : ( - - {id} - - )} - - {isDragEnabled && ( - - - - )} - -
- )} -
- ))} -
-
-
-
-
- {allowColumnHiding && ( - - setIsOpen(false)} + anchorPosition="downLeft" + panelPaddingSize="s" + panelClassName="euiDataGrid__controlPopoverWithDragDrop" + button={ + setIsOpen(!isOpen)} > - - setVisibleColumns(sortedColumns)} - data-test-subj="dataGridColumnSelectorShowAllButton" + {buttonText} + + } + > +
+ {allowColumnHiding && ( + + - - - - - setVisibleColumns([])} - data-test-subj="dataGridColumnSelectorHideAllButton" + {([search, searchcolumns]: string[]) => ( + ) => + setColumnSearchText(e.currentTarget.value) + } + data-test-subj="dataGridColumnSelectorSearch" + /> + )} + + + )} +
+ + - - - - - - )} - - ); + + {filteredColumns.map((id, index) => ( + + {(provided, state) => ( +
+ + + {allowColumnHiding ? ( + { + const { + target: { checked }, + } = event; + const nextVisibleColumns = sortedColumns.filter( + (columnId) => + checked + ? visibleColumnIds.has(columnId) || + id === columnId + : visibleColumnIds.has(columnId) && + id !== columnId + ); + setVisibleColumns(nextVisibleColumns); + }} + data-test-subj={`dataGridColumnSelectorToggleColumnVisibility-${id}`} + /> + ) : ( + + {id} + + )} + + {isDragEnabled && ( + + + + )} + +
+ )} +
+ ))} +
+
+
+
+
+ {allowColumnHiding && ( + + + + setVisibleColumns(sortedColumns)} + data-test-subj="dataGridColumnSelectorShowAllButton" + > + + + + + setVisibleColumns([])} + data-test-subj="dataGridColumnSelectorHideAllButton" + > + + + + + + )} +
+ ) : null; const orderedVisibleColumns = useMemo( () => diff --git a/src/components/datagrid/controls/data_grid_toolbar.test.tsx b/src/components/datagrid/controls/data_grid_toolbar.test.tsx index a1c513df46b..411fa9b891d 100644 --- a/src/components/datagrid/controls/data_grid_toolbar.test.tsx +++ b/src/components/datagrid/controls/data_grid_toolbar.test.tsx @@ -13,6 +13,7 @@ import { EuiDataGridToolbar, checkOrDefaultToolBarDisplayOptions, renderAdditionalControls, + getNestedObjectOptions, } from './data_grid_toolbar'; describe('EuiDataGridToolbar', () => { @@ -20,9 +21,9 @@ describe('EuiDataGridToolbar', () => { gridWidth: 500, toolbarVisibility: true, isFullScreen: false, - styleSelector: React.createElement('div', null, 'mock style selector'), + displaySelector:
mock style selector
, controlBtnClasses: '', - columnSelector: React.createElement('div', null, 'mock column selector'), + columnSelector:
mock column selector
, columnSorting:
mock column sorting
, setRef: jest.fn(), setIsFullScreen: jest.fn(), @@ -99,7 +100,7 @@ describe('EuiDataGridToolbar', () => { {...requiredProps} toolbarVisibility={{ showColumnSelector: false, - showStyleSelector: false, + showDisplaySelector: false, showSortSelector: false, showFullScreenSelector: false, additionalControls: { @@ -159,7 +160,7 @@ describe('EuiDataGridToolbar', () => { }); describe('checkOrDefaultToolBarDisplayOptions', () => { - const key = 'showStyleSelector'; + const key = 'showDisplaySelector'; it('returns boolean `toolbarVisibility`s as-is', () => { expect(checkOrDefaultToolBarDisplayOptions(true, key)).toEqual(true); @@ -326,3 +327,41 @@ describe('renderAdditionalControls', () => { }); }); }); + +describe('getNestedObjectOptions', () => { + interface MockOptions { + someKey?: boolean; + } + + describe('non-object configuration', () => { + it('returns passed booleans', () => { + expect(getNestedObjectOptions(true, 'someKey')).toEqual( + true + ); + expect(getNestedObjectOptions(false, 'someKey')).toEqual( + false + ); + }); + + it('returns true if the option is undefined', () => { + expect(getNestedObjectOptions(undefined, 'someKey')).toEqual( + true + ); + }); + }); + + describe('object configuration', () => { + it('returns nested object booleans', () => { + expect( + getNestedObjectOptions({ someKey: true }, 'someKey') + ).toEqual(true); + expect( + getNestedObjectOptions({ someKey: false }, 'someKey') + ).toEqual(false); + }); + + it('returns true if the nested object key is undefined', () => { + expect(getNestedObjectOptions({}, 'someKey')).toEqual(true); + }); + }); +}); diff --git a/src/components/datagrid/controls/data_grid_toolbar.tsx b/src/components/datagrid/controls/data_grid_toolbar.tsx index fe3e5344794..81b610aadb0 100644 --- a/src/components/datagrid/controls/data_grid_toolbar.tsx +++ b/src/components/datagrid/controls/data_grid_toolbar.tsx @@ -33,7 +33,7 @@ export const EuiDataGridToolbar = ({ toolbarVisibility, isFullScreen, controlBtnClasses, - styleSelector, + displaySelector, columnSelector, columnSorting, setRef, @@ -114,9 +114,9 @@ export const EuiDataGridToolbar = ({ {renderAdditionalControls(toolbarVisibility, 'right')} {checkOrDefaultToolBarDisplayOptions( toolbarVisibility, - 'showStyleSelector' + 'showDisplaySelector' ) - ? styleSelector + ? displaySelector : null} {checkOrDefaultToolBarDisplayOptions( toolbarVisibility, @@ -200,3 +200,20 @@ export function renderAdditionalControls( return null; } + +/** + * Utility helper for selectors/controls that allow nested options + * (e.g. column selector, display selector) + */ + +export function getNestedObjectOptions( + controlOption: undefined | boolean | T, + objectKey: keyof T +): boolean { + // If the config is a boolean, nested options follow that boolean + if (controlOption === false || controlOption === true) return controlOption; + // If config is not defined, default to enabled + if (controlOption == null) return true; + // Otherwise, type should be an object of boolean values - dive into it and return the value + return !!(controlOption[objectKey] ?? true); +} diff --git a/src/components/datagrid/controls/display_selector.test.tsx b/src/components/datagrid/controls/display_selector.test.tsx new file mode 100644 index 00000000000..88b699a8d01 --- /dev/null +++ b/src/components/datagrid/controls/display_selector.test.tsx @@ -0,0 +1,391 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { shallow, mount, ShallowWrapper, ReactWrapper } from 'enzyme'; + +import { + EuiDataGridToolBarVisibilityOptions, + EuiDataGridRowHeightsOptions, +} from '../data_grid_types'; + +import { useDataGridDisplaySelector, startingStyles } from './display_selector'; + +describe('useDataGridDisplaySelector', () => { + describe('displaySelector', () => { + // Hooks can only be called inside function components + const MockComponent = ({ + showDisplaySelector = true as EuiDataGridToolBarVisibilityOptions['showDisplaySelector'], + gridStyles = {}, + rowHeightsOptions = undefined as EuiDataGridRowHeightsOptions | undefined, + }) => { + const [displaySelector] = useDataGridDisplaySelector( + showDisplaySelector, + gridStyles, + rowHeightsOptions + ); + return <>{displaySelector}; + }; + const openPopover = (component: ReactWrapper) => { + component + .find('[data-test-subj="dataGridDisplaySelectorButton"]') + .last() + .simulate('click'); + }; + const closePopover = (component: ReactWrapper) => { + act(() => { + (component + .find('[data-test-subj="dataGridDisplaySelectorPopover"]') + .first() + .prop('closePopover') as Function)(); + }); + }; + + it('renders a toolbar button/popover allowing users to customize display settings', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('does not render if all valid sub-options are disabled', () => { + const component = shallow( + + ); + expect(component.text()).toEqual(''); + }); + + describe('density', () => { + const getSelection = (component: ReactWrapper) => + component + .find('EuiButtonGroup[data-test-subj="densityButtonGroup"]') + .prop('idSelected'); + + it('renders display density buttons that change grid density on click', () => { + const component = mount(); + openPopover(component); + + // Click density 'buttons' (actually hidden radios) + component.find('[data-test-subj="expanded"]').simulate('change'); + expect(getSelection(component)).toEqual('expanded'); + component.find('[data-test-subj="normal"]').simulate('change'); + expect(getSelection(component)).toEqual('normal'); + component.find('[data-test-subj="compact"]').simulate('change'); + expect(getSelection(component)).toEqual('compact'); + + // Should have changed the main toolbar icon accordingly + closePopover(component); + expect( + component + .find('[data-test-subj="dataGridDisplaySelectorButton"]') + .first() + .prop('iconType') + ).toEqual('tableDensityCompact'); + }); + + it('hides the density buttongroup if allowDensity is set to false', () => { + const component = mount( + + ); + openPopover(component); + + expect( + component.find('[data-test-subj="densityButtonGroup"]') + ).toHaveLength(0); + }); + + describe('convertGridStylesToSelection (loading initial state from passed gridStyles', () => { + it('should set compact state if both fontSize and cellPadding are s', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual('compact'); + }); + + it('should set normal state if both fontSize and cellPadding are m', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual('normal'); + }); + + it('should set compact state if both fontSize and cellPadding are l', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual('expanded'); + }); + + it('should not select any buttons if fontSize and cellPadding do not match a set density state', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual(''); + }); + }); + }); + + describe('row height', () => { + const getSelection = (component: ReactWrapper) => + component + .find('EuiButtonGroup[data-test-subj="rowHeightButtonGroup"]') + .prop('idSelected'); + + it('renders row height buttons that toggle betwen undefined, auto, and lineCount', () => { + const component = mount(); + openPopover(component); + expect(getSelection(component)).toEqual('undefined'); + + component.find('[data-test-subj="auto"]').simulate('change'); + expect(getSelection(component)).toEqual('auto'); + }); + + it('hides the row height buttongroup if allowRowHeight is set to false', () => { + const component = mount( + + ); + openPopover(component); + + expect( + component.find('[data-test-subj="rowHeightButtonGroup"]') + ).toHaveLength(0); + }); + + describe('convertRowHeightsOptionsToSelection (loading initial state from passed rowHeightsOptions)', () => { + test('auto', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual('auto'); + }); + + test('lineCount', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual('lineCount'); + }); + + test('undefined', () => { + const component = mount( + + ); + openPopover(component); + expect(getSelection(component)).toEqual('undefined'); + }); + + test('height should not select any buttons', () => { + const component1 = mount( + + ); + openPopover(component1); + expect(getSelection(component1)).toEqual(''); + + const component2 = mount( + + ); + openPopover(component2); + expect(getSelection(component2)).toEqual(''); + }); + }); + + describe('lineCount', () => { + const getLineCountNumber = (component: ReactWrapper) => + component + .find('EuiRange[data-test-subj="lineCountNumber"]') + .prop('value'); + const setLineCountNumber = (component: ReactWrapper, number: number) => + component + .find('input[type="range"][data-test-subj="lineCountNumber"]') + .simulate('change', { target: { value: number } }); + + it('conditionally displays a line count number input when the lineCount button is selected', () => { + const component = mount(); + openPopover(component); + expect( + component.find('[data-test-subj="lineCountNumber"]').exists() + ).toBe(false); + + component.find('[data-test-subj="lineCount"]').simulate('change'); + expect(getSelection(component)).toEqual('lineCount'); + + expect( + component.find('[data-test-subj="lineCountNumber"]').exists() + ).toBe(true); + }); + + it('displays the defaultHeight.lineCount passed in by the developer', () => { + const component = mount( + + ); + openPopover(component); + + expect(getLineCountNumber(component)).toEqual(5); + }); + + it('defaults to a lineCount of 2 when no developer settings have been passed', () => { + const component = mount(); + openPopover(component); + component.find('[data-test-subj="lineCount"]').simulate('change'); + + expect(getLineCountNumber(component)).toEqual(2); + }); + + it('increments the rowHeightOptions line count number', () => { + const component = mount( + + ); + openPopover(component); + + setLineCountNumber(component, 3); + expect(getLineCountNumber(component)).toEqual(3); + }); + + it('does not allow zero or negative line count values', () => { + const component = mount( + + ); + openPopover(component); + + setLineCountNumber(component, 0); + expect(getLineCountNumber(component)).toEqual(2); + + setLineCountNumber(component, -50); + expect(getLineCountNumber(component)).toEqual(2); + }); + }); + }); + }); + + describe('gridStyles', () => { + it('returns an object of grid styles with user overrides', () => { + const initialStyles = { ...startingStyles, stripes: true }; + const MockComponent = () => { + const [, gridStyles] = useDataGridDisplaySelector( + true, + initialStyles, + {} + ); + return ; + }; + const component = shallow(); + + expect(component).toMatchInlineSnapshot(` +
+ `); + }); + }); + + describe('rowHeightsOptions', () => { + // Test helpers + const MockComponent = ({ + initialRowHeightsOptions = undefined as + | EuiDataGridRowHeightsOptions + | undefined, + }) => { + const [displaySelector, , rowHeightsOptions] = useDataGridDisplaySelector( + true, + {}, + initialRowHeightsOptions + ); + return ( + <> + {displaySelector} +
{JSON.stringify(rowHeightsOptions)}
+ + ); + }; + const diveIntoEuiI18n = (component: ShallowWrapper) => { + return (component + .find('EuiI18n') + .last() + .renderProp('children') as Function)(['', '', '', '']); + }; + const setRowHeight = (component: ShallowWrapper, selection = '') => { + diveIntoEuiI18n(component) + .find('[data-test-subj="rowHeightButtonGroup"]') + .simulate('change', selection); + }; + const setLineCount = (component: ShallowWrapper, lineCount = 1) => { + diveIntoEuiI18n(component) + .find('[data-test-subj="lineCountNumber"]') + .simulate('change', { target: { value: lineCount } }); + }; + const getOutput = (component: ShallowWrapper) => { + return JSON.parse(component.find('[data-test-subj="output"]').text()); + }; + + it('returns an object of rowHeightsOptions with user overrides', () => { + const component = shallow( + + ); + + setRowHeight(component, 'lineCount'); + setLineCount(component, 5); + + expect(getOutput(component)).toEqual({ + lineHeight: '2em', + defaultHeight: { lineCount: 5 }, + }); + }); + + it('handles undefined rowHeightsObjects (from the developer)', () => { + const component = shallow( + + ); + expect(getOutput(component)).toEqual({}); + + setRowHeight(component, 'auto'); + + expect(getOutput(component)).toEqual({ + defaultHeight: 'auto', + }); + }); + + it('handles undefined rowHeightsOptions (from the user)', () => { + const component = shallow( + + ); + + setRowHeight(component, 'undefined'); + + expect(getOutput(component)).toEqual({ + lineHeight: '2em', + }); + }); + }); +}); diff --git a/src/components/datagrid/controls/display_selector.tsx b/src/components/datagrid/controls/display_selector.tsx new file mode 100644 index 00000000000..bafc4591c46 --- /dev/null +++ b/src/components/datagrid/controls/display_selector.tsx @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactNode, useState, useMemo, useCallback } from 'react'; + +import { EuiI18n, useEuiI18n } from '../../i18n'; +import { EuiPopover } from '../../popover'; +import { EuiButtonIcon, EuiButtonGroup } from '../../button'; +import { EuiFormRow, EuiRange } from '../../form'; +import { EuiToolTip } from '../../tool_tip'; + +import { + EuiDataGridToolBarVisibilityOptions, + EuiDataGridStyle, + EuiDataGridRowHeightsOptions, +} from '../data_grid_types'; +import { getNestedObjectOptions } from './data_grid_toolbar'; + +export const startingStyles: EuiDataGridStyle = { + cellPadding: 'm', + fontSize: 'm', + border: 'all', + stripes: false, + rowHover: 'highlight', + header: 'shade', + footer: 'overline', + stickyFooter: true, +}; + +// These are the available options. They power the gridDensity hook and also the options in the render +const densityOptions: string[] = ['compact', 'normal', 'expanded']; +const densityStyles: { [key: string]: Partial } = { + expanded: { + fontSize: 'l', + cellPadding: 'l', + }, + normal: { + fontSize: 'm', + cellPadding: 'm', + }, + compact: { + fontSize: 's', + cellPadding: 's', + }, +}; +const convertGridStylesToSelection = (gridStyles: EuiDataGridStyle) => { + if (gridStyles?.fontSize === 's' && gridStyles?.cellPadding === 's') + return 'compact'; + if (gridStyles?.fontSize === 'm' && gridStyles?.cellPadding === 'm') + return 'normal'; + if (gridStyles?.fontSize === 'l' && gridStyles?.cellPadding === 'l') + return 'expanded'; + return ''; +}; + +// Used to correctly format the icon name for the grid density icon +const capitalizeDensityString = (s: string) => s[0].toUpperCase() + s.slice(1); + +// Row height options and utilities +const rowHeightButtonOptions: string[] = ['undefined', 'auto', 'lineCount']; +const convertRowHeightsOptionsToSelection = ( + rowHeightsOptions?: EuiDataGridRowHeightsOptions +) => { + if (rowHeightsOptions) { + const { defaultHeight } = rowHeightsOptions; + + if (defaultHeight === 'auto') { + return rowHeightButtonOptions[1]; + } + if (typeof defaultHeight === 'object' && defaultHeight?.lineCount) { + return rowHeightButtonOptions[2]; + } + if ( + typeof defaultHeight === 'number' || + (typeof defaultHeight === 'object' && defaultHeight.height) + ) { + return ''; + } + } + return rowHeightButtonOptions[0]; +}; + +export const useDataGridDisplaySelector = ( + showDisplaySelector: EuiDataGridToolBarVisibilityOptions['showDisplaySelector'], + initialStyles: EuiDataGridStyle, + initialRowHeightsOptions?: EuiDataGridRowHeightsOptions +): [ReactNode, EuiDataGridStyle, EuiDataGridRowHeightsOptions] => { + const [isOpen, setIsOpen] = useState(false); + + const showDensityControls = getNestedObjectOptions( + showDisplaySelector, + 'allowDensity' + ); + + const showRowHeightControls = getNestedObjectOptions( + showDisplaySelector, + 'allowRowHeight' + ); + + // track styles specified by the user at run time + const [userGridStyles, setUserGridStyles] = useState({}); + const [userRowHeightsOptions, setUserRowHeightsOptions] = useState({}); + + // Normal is the default density + const [gridDensity, _setGridDensity] = useState( + convertGridStylesToSelection(initialStyles) + ); + const setGridDensity = (density: string) => { + _setGridDensity(density); + setUserGridStyles(densityStyles[density]); + }; + + // Row height state + const [lineCount, setLineCount] = useState( + // @ts-ignore - optional chaining operator handles types & cases that aren't lineCount + initialRowHeightsOptions?.defaultHeight?.lineCount || 2 + ); + const [rowHeightSelection, setRowHeightSelection] = useState( + convertRowHeightsOptionsToSelection(initialRowHeightsOptions) + ); + const setRowHeight = useCallback( + (option: string) => { + let rowHeightsOptions: EuiDataGridRowHeightsOptions | undefined; + + if (option === 'auto') { + rowHeightsOptions = { defaultHeight: 'auto' }; + } else if (option === 'lineCount') { + rowHeightsOptions = { defaultHeight: { lineCount } }; + } else { + rowHeightsOptions = { defaultHeight: undefined }; + } + + setRowHeightSelection(option); + setUserRowHeightsOptions(rowHeightsOptions); + }, + [lineCount] + ); + const setLineCountHeight = useCallback((event) => { + const newLineCount = Number(event.target.value); + if (newLineCount < 1) return; // Don't let users set a 0 or negative line count + + setLineCount(newLineCount); + setUserRowHeightsOptions({ defaultHeight: { lineCount: newLineCount } }); + }, []); + + // merge the developer-specified styles with any user overrides + const gridStyles = useMemo(() => { + return { + ...initialStyles, + ...userGridStyles, + }; + }, [initialStyles, userGridStyles]); + + const rowHeightsOptions = useMemo(() => { + return { + ...initialRowHeightsOptions, + ...userRowHeightsOptions, + }; + }, [initialRowHeightsOptions, userRowHeightsOptions]); + + const buttonLabel = useEuiI18n( + 'euiDisplaySelector.buttonText', + 'Display options' + ); + + const displaySelector = + showDensityControls || showRowHeightControls ? ( + setIsOpen(false)} + anchorPosition="downRight" + panelPaddingSize="s" + panelClassName="euiDataGrid__displayPopoverPanel" + button={ + + setIsOpen(!isOpen)} + aria-label={buttonLabel} + /> + + } + > + {showDensityControls && ( + + {([ + densityLabel, + labelCompact, + labelNormal, + labelExpanded, + ]: string[]) => ( + + + + )} + + )} + {showRowHeightControls && ( + + {([ + rowHeightLabel, + labelSingle, + labelAuto, + labelCustom, + lineCountLabel, + ]: string[]) => ( + <> + + + + {rowHeightSelection === rowHeightButtonOptions[2] && ( + + + + )} + + )} + + )} + + ) : null; + + return [displaySelector, gridStyles, rowHeightsOptions]; +}; diff --git a/src/components/datagrid/controls/index.ts b/src/components/datagrid/controls/index.ts index c3c5cce82f7..0c0ca128208 100644 --- a/src/components/datagrid/controls/index.ts +++ b/src/components/datagrid/controls/index.ts @@ -8,7 +8,7 @@ export { useDataGridColumnSelector } from './column_selector'; export { useDataGridColumnSorting } from './column_sorting'; -export { useDataGridStyleSelector, startingStyles } from './style_selector'; +export { useDataGridDisplaySelector, startingStyles } from './display_selector'; export { checkOrDefaultToolBarDisplayOptions, EuiDataGridToolbar, diff --git a/src/components/datagrid/controls/style_selector.test.tsx b/src/components/datagrid/controls/style_selector.test.tsx deleted file mode 100644 index 509667f00ff..00000000000 --- a/src/components/datagrid/controls/style_selector.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { shallow, mount } from 'enzyme'; - -import { useDataGridStyleSelector, startingStyles } from './style_selector'; - -describe('useDataGridStyleSelector', () => { - describe('styleSelector', () => { - // Hooks can only be called inside function components - const MockComponent = () => { - const [styleSelector] = useDataGridStyleSelector({}); - return <>{styleSelector}; - }; - - it('renders a toolbar button/popover allowing users to customize styles', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('renders display density buttons that change grid density on click', () => { - const component = mount(); - - // Open popover - component - .find('[data-test-subj="dataGridStyleSelectorButton"]') - .last() - .simulate('click'); - - // Click density 'buttons' (actually hidden radios) - component.find('[data-test-subj="expanded"]').simulate('change'); - component.find('[data-test-subj="normal"]').simulate('change'); - component.find('[data-test-subj="compact"]').simulate('change'); - expect(component.find('EuiButtonGroup').prop('idSelected')).toEqual( - 'compact' - ); - - // Close popover - act(() => { - (component - .find('[data-test-subj="dataGridStyleSelectorPopover"]') - .first() - .prop('closePopover') as Function)(); - }); - }); - }); - - describe('gridStyles', () => { - it('returns an object of grid styles with user overrides', () => { - const initialStyles = { ...startingStyles, stripes: true }; - const MockComponent = () => { - const [, gridStyles] = useDataGridStyleSelector(initialStyles); - return
; - }; - const component = shallow(); - - expect(component).toMatchInlineSnapshot(` -
- `); - }); - }); -}); diff --git a/src/components/datagrid/controls/style_selector.tsx b/src/components/datagrid/controls/style_selector.tsx deleted file mode 100644 index 7d46e2a2eb4..00000000000 --- a/src/components/datagrid/controls/style_selector.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { ReactElement, useState } from 'react'; -import { EuiDataGridStyle } from '../data_grid_types'; -import { EuiI18n, useEuiI18n } from '../../i18n'; -import { EuiPopover } from '../../popover'; -import { EuiButtonIcon, EuiButtonGroup } from '../../button'; -import { EuiFormRow } from '../../form'; -import { EuiToolTip } from '../../tool_tip'; - -export const startingStyles: EuiDataGridStyle = { - cellPadding: 'm', - fontSize: 'm', - border: 'all', - stripes: false, - rowHover: 'highlight', - header: 'shade', - footer: 'overline', - stickyFooter: true, -}; - -// These are the available options. They power the gridDensity hook and also the options in the render -const densityOptions: string[] = ['compact', 'normal', 'expanded']; -const densityStyles: { [key: string]: Partial } = { - expanded: { - fontSize: 'l', - cellPadding: 'l', - }, - normal: { - fontSize: 'm', - cellPadding: 'm', - }, - compact: { - fontSize: 's', - cellPadding: 's', - }, -}; -// Used to correctly format the icon name for the grid density icon -const capitalizeDensityString = (s: string) => s[0].toUpperCase() + s.slice(1); - -export const useDataGridStyleSelector = ( - initialStyles: EuiDataGridStyle -): [ReactElement, EuiDataGridStyle] => { - // track styles specified by the user at run time - const [userGridStyles, setUserGridStyles] = useState({}); - - const [isOpen, setIsOpen] = useState(false); - - // Normal is the default density - const [gridDensity, _setGridDensity] = useState(densityOptions[1]); - const setGridDensity = (density: string) => { - _setGridDensity(density); - setUserGridStyles(densityStyles[density]); - }; - - // merge the developer-specified styles with any user overrides - const gridStyles = { - ...initialStyles, - ...userGridStyles, - }; - - const buttonLabel = useEuiI18n( - 'euiStyleSelector.buttonText', - 'Display options' - ); - - const styleSelector = ( - setIsOpen(false)} - anchorPosition="downRight" - panelPaddingSize="s" - panelClassName="euiDataGrid__displayPopoverPanel" - button={ - - setIsOpen(!isOpen)} - aria-label={buttonLabel} - /> - - } - > - - {([ - densityLabel, - labelCompact, - labelNormal, - labelExpanded, - ]: string[]) => ( - - - - )} - - - ); - - return [styleSelector, gridStyles]; -}; diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index a05f1df80be..bd58ee30e25 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -752,7 +752,7 @@ describe('EuiDataGrid', () => { toolbarVisibility: { showFullScreenSelector: false, showSortSelector: false, - showStyleSelector: true, + showDisplaySelector: true, }, }); @@ -769,7 +769,7 @@ describe('EuiDataGrid', () => { // style selector component.debug(); expect( - findTestSubject(component, 'dataGridStyleSelectorButton').length + findTestSubject(component, 'dataGridDisplaySelectorButton').length ).toBe(1); // column selector diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index e9908d8b22c..af29264c086 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -27,7 +27,7 @@ import { EuiDataGridBody, VIRTUALIZED_CONTAINER_CLASS } from './body'; import { useDataGridColumnSelector, useDataGridColumnSorting, - useDataGridStyleSelector, + useDataGridDisplaySelector, startingStyles, checkOrDefaultToolBarDisplayOptions, EuiDataGridToolbar, @@ -488,7 +488,7 @@ export const EuiDataGrid: FunctionComponent = (props) => { minSizeForControls, height, width, - rowHeightsOptions, + rowHeightsOptions: _rowHeightsOptions, virtualizationOptions, ...rest } = props; @@ -648,8 +648,17 @@ export const EuiDataGrid: FunctionComponent = (props) => { allSchemaDetectors, displayValues ); - const [styleSelector, gridStyles] = useDataGridStyleSelector( - gridStyleWithDefaults + const [ + displaySelector, + gridStyles, + rowHeightsOptions, + ] = useDataGridDisplaySelector( + checkOrDefaultToolBarDisplayOptions( + toolbarVisibility, + 'showDisplaySelector' + ), + gridStyleWithDefaults, + _rowHeightsOptions ); // compute the default column width from the container's clientWidth and count of visible columns @@ -808,7 +817,7 @@ export const EuiDataGrid: FunctionComponent = (props) => { gridWidth={gridWidth} minSizeForControls={minSizeForControls} toolbarVisibility={toolbarVisibility} - styleSelector={styleSelector} + displaySelector={displaySelector} isFullScreen={isFullScreen} setIsFullScreen={setIsFullScreen} controlBtnClasses={controlBtnClasses} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index c3a5eeff132..9b1b15ab9e8 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -30,10 +30,10 @@ export interface EuiDataGridToolbarProps { gridWidth: number; minSizeForControls?: number; toolbarVisibility: boolean | EuiDataGridToolBarVisibilityOptions; - styleSelector: ReactElement; + displaySelector: ReactNode; isFullScreen: boolean; controlBtnClasses: string; - columnSelector: ReactElement; + columnSelector: ReactNode; columnSorting: ReactNode; setRef: RefCallback; setIsFullScreen: Dispatch>; @@ -589,6 +589,17 @@ export interface EuiDataGridToolBarVisibilityColumnSelectorOptions { allowReorder?: boolean; } +export interface EuiDataGridToolBarVisibilityDisplaySelectorOptions { + /** + * When `false`, removes the ability to change density display through the UI + */ + allowDensity?: boolean; + /** + * When `false`, removes the ability to change row height display through the UI + */ + allowRowHeight?: boolean; +} + export interface EuiDataGridToolBarVisibilityOptions { /** * Allows the ability for the user to hide fields and sort columns, boolean or a #EuiDataGridToolBarVisibilityColumnSelectorOptions @@ -597,9 +608,12 @@ export interface EuiDataGridToolBarVisibilityOptions { | boolean | EuiDataGridToolBarVisibilityColumnSelectorOptions; /** - * Allows the ability for the user to set the grid density. If on, this merges against what is provided in #EuiDataGridStyle + * Allows the ability for the user to customize display settings such as grid density and row heights. + * User changes will override what is provided in #EuiDataGridStyle and #EuiDataGridRowHeightsOptions */ - showStyleSelector?: boolean; + showDisplaySelector?: + | boolean + | EuiDataGridToolBarVisibilityDisplaySelectorOptions; /** * Allows the ability for the user to sort rows based upon column values */ @@ -744,6 +758,9 @@ export interface EuiDataGridRowHeightsOptions { defaultHeight?: EuiDataGridRowHeightOption; /** * Defines the height for a specific row. It can be line count or just height. + * + * When using row height overrides, we strongly setting the `showDisplaySelector: allowRowHeight` + * toolbar control to `false` in #EuiDataGridToolBarVisibilityOptions */ rowHeights?: Record; /** diff --git a/src/components/datagrid/index.ts b/src/components/datagrid/index.ts index 8dad0388361..7d32fd2d869 100644 --- a/src/components/datagrid/index.ts +++ b/src/components/datagrid/index.ts @@ -10,7 +10,7 @@ export { EuiDataGrid } from './data_grid'; export { useDataGridColumnSelector, useDataGridColumnSorting, - useDataGridStyleSelector, + useDataGridDisplaySelector, } from './controls'; export * from './data_grid_types';