Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiDataGrid] Add onChange callbacks for display selector changes #5424

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Updated `EuiDataGrid`'s full screen mode to use the `fullScreenExit` icon ([#5415](https://github.com/elastic/eui/pull/5415))
- 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))
- Added `onChange` callbacks to `EuiDataGrid`'s `gridStyle` and `rowHeightOptions` settings ([#5424](https://github.com/elastic/eui/pull/5424))

**Bug fixes**

Expand Down
23 changes: 21 additions & 2 deletions src-docs/src/views/datagrid/datagrid_height_options_example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';

import { GuideSectionTypes } from '../../components';
import {
Expand Down Expand Up @@ -136,8 +137,8 @@ export const DataGridRowHeightOptionsExample = {
By default, all rows get a height of <strong>34 pixels</strong>, but
there are scenarios where you might want to adjust the height to fit
more content. To do that, you can pass an object to the{' '}
<EuiCode>rowHeightsOptions</EuiCode> prop. This object accepts three
properties:
<EuiCode>rowHeightsOptions</EuiCode> prop. This object accepts the
following properties:
</p>
<ul>
<li>
Expand Down Expand Up @@ -173,6 +174,24 @@ export const DataGridRowHeightOptionsExample = {
</li>
</ul>
</li>
<li>
<EuiCode>onChange</EuiCode>
<ul>
<li>
Optional callback when the user changes the data grid&apos;s
internal <EuiCode>rowHeightsOptions</EuiCode> (e.g., via the
toolbar display selector).
</li>
<li>
Can be used to store and preserve user display preferences on
page refresh - see this{' '}
<Link to="/tabular-content/data-grid-styling-and-control#adjusting-your-grid-to-usertoolbar-changes">
data grid styling and control example
</Link>
.
</li>
</ul>
</li>
</ul>
</EuiText>
<EuiSpacer />
Expand Down
29 changes: 29 additions & 0 deletions src-docs/src/views/datagrid/datagrid_styling_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
EuiListGroupItem,
} from '../../../../src/components';

import DataGridDisplayCallbacks from './display_callbacks';
const dataGridDisplayCallbacksSource = require('!!raw-loader!./display_callbacks');

import DataGridContainer from './container';
const dataGridContainerSource = require('!!raw-loader!./container');
const dataGridContainerHtml = renderToHtml(DataGridContainer);
Expand Down Expand Up @@ -220,6 +223,32 @@ export const DataGridStylingExample = {
},
demo: <DataGridStyling />,
},
{
source: [
{
type: GuideSectionTypes.JS,
code: dataGridDisplayCallbacksSource,
},
],
title: 'Adjusting your grid to user/toolbar changes',
text: (
<>
<p>
You can use the optional <EuiCode>gridStyle.onChange</EuiCode> and{' '}
<EuiCode>rowHeightsOptions.onChange</EuiCode> callbacks to adjust
your data grid based on user density or row height changes.
</p>
<p>
For example, if the user changes the grid density to compressed, you
may want to adjust a cell&apos;s content sizing in response. Or you
could store user settings in localStorage or other database to
preserve display settings on page refresh, like the below example
does.
</p>
</>
),
demo: <DataGridDisplayCallbacks />,
},
{
source: [
{
Expand Down
95 changes: 95 additions & 0 deletions src-docs/src/views/datagrid/display_callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState, useCallback, useMemo } from 'react';
import { fake } from 'faker';

import { EuiDataGrid, EuiIcon } from '../../../../src/components/';

const columns = [
{ id: 'name' },
{ id: 'email' },
{ id: 'city' },
{ id: 'country' },
{ id: 'account' },
];
const data = [];
for (let i = 1; i <= 5; i++) {
data.push({
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 GRID_STYLES_KEY = 'euiDataGridStyles';
const INITIAL_STYLES = JSON.stringify({ stripes: true });

const ROW_HEIGHTS_KEY = 'euiDataGridRowHeightsOptions';
const INITIAL_ROW_HEIGHTS = JSON.stringify({});

export default () => {
const [densitySize, setDensitySize] = useState('');
const responsiveIcon = useCallback(
() => <EuiIcon type="user" size={densitySize} />,
[densitySize]
);
const responsiveIconWidth = useMemo(() => {
if (densitySize === 'l') return 44;
if (densitySize === 's') return 24;
return 32;
}, [densitySize]);
const leadingControlColumns = useMemo(
() => [
{
id: 'icon',
width: responsiveIconWidth,
headerCellRender: responsiveIcon,
rowCellRender: responsiveIcon,
},
],
[responsiveIcon, responsiveIconWidth]
);

const storedRowHeightsOptions = useMemo(
() =>
JSON.parse(localStorage.getItem(ROW_HEIGHTS_KEY) || INITIAL_ROW_HEIGHTS),
[]
);
const storeRowHeightsOptions = useCallback((updatedRowHeights) => {
console.log(updatedRowHeights);
localStorage.setItem(ROW_HEIGHTS_KEY, JSON.stringify(updatedRowHeights));
}, []);

const storedGridStyles = useMemo(
() => JSON.parse(localStorage.getItem(GRID_STYLES_KEY) || INITIAL_STYLES),
[]
);
const storeGridStyles = useCallback((updatedStyles) => {
console.log(updatedStyles);
localStorage.setItem(GRID_STYLES_KEY, JSON.stringify(updatedStyles));
setDensitySize(updatedStyles.fontSize);
}, []);

const [visibleColumns, setVisibleColumns] = useState(() =>
columns.map(({ id }) => id)
);

return (
<EuiDataGrid
aria-label="DataGrid demonstrating display selector callbacks"
leadingControlColumns={leadingControlColumns}
rowHeightsOptions={{
...storedRowHeightsOptions,
onChange: storeRowHeightsOptions,
}}
gridStyle={{
...storedGridStyles,
onChange: storeGridStyles,
}}
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
rowCount={data.length}
renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]}
/>
);
};
37 changes: 36 additions & 1 deletion src/components/datagrid/controls/display_selector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ describe('useDataGridDisplaySelector', () => {
).toEqual('tableDensityCompact');
});

it('calls the gridStyles.onDensityChange callback on user change', () => {
const onDensityChange = jest.fn();
const component = mount(
<MockComponent
gridStyles={{ stripes: true, onChange: onDensityChange }}
/>
);

openPopover(component);
component.find('[data-test-subj="expanded"]').simulate('change');

expect(onDensityChange).toHaveBeenCalledWith({
stripes: true,
fontSize: 'l',
cellPadding: 'l',
});
});

it('hides the density buttongroup if allowDensity is set to false', () => {
const component = mount(
<MockComponent showDisplaySelector={{ allowDensity: false }} />
Expand Down Expand Up @@ -153,6 +171,23 @@ describe('useDataGridDisplaySelector', () => {
expect(getSelection(component)).toEqual('auto');
});

it('calls the rowHeightsOptions.onChange callback on user change', () => {
const onRowHeightChange = jest.fn();
const component = mount(
<MockComponent
rowHeightsOptions={{ lineHeight: '3', onChange: onRowHeightChange }}
/>
);

openPopover(component);
component.find('[data-test-subj="auto"]').simulate('change');

expect(onRowHeightChange).toHaveBeenCalledWith({
defaultHeight: 'auto',
lineHeight: '3',
});
});

it('hides the row height buttongroup if allowRowHeight is set to false', () => {
const component = mount(
<MockComponent showDisplaySelector={{ allowRowHeight: false }} />
Expand Down Expand Up @@ -286,7 +321,7 @@ describe('useDataGridDisplaySelector', () => {
it('returns an object of grid styles with user overrides', () => {
const initialStyles = { ...startingStyles, stripes: true };
const MockComponent = () => {
const [, gridStyles] = useDataGridDisplaySelector(
const [, { onChange, ...gridStyles }] = useDataGridDisplaySelector(
true,
initialStyles,
{}
Expand Down
12 changes: 12 additions & 0 deletions src/components/datagrid/controls/display_selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import React, { ReactNode, useState, useMemo, useCallback } from 'react';

import { useUpdateEffect } from '../../../services';
import { EuiI18n, useEuiI18n } from '../../i18n';
import { EuiPopover } from '../../popover';
import { EuiButtonIcon, EuiButtonGroup } from '../../button';
Expand Down Expand Up @@ -163,6 +164,17 @@ export const useDataGridDisplaySelector = (
};
}, [initialRowHeightsOptions, userRowHeightsOptions]);

// Invoke onChange callbacks on user input (removing the callback value itself, so that only configuration values are returned)
useUpdateEffect(() => {
const { onChange, ...currentGridStyles } = gridStyles;
initialStyles?.onChange?.(currentGridStyles);
}, [userGridStyles]);

useUpdateEffect(() => {
const { onChange, ...currentRowHeightsOptions } = rowHeightsOptions;
initialRowHeightsOptions?.onChange?.(currentRowHeightsOptions);
}, [userRowHeightsOptions]);

const buttonLabel = useEuiI18n(
'euiDisplaySelector.buttonText',
'Display options'
Expand Down
10 changes: 10 additions & 0 deletions src/components/datagrid/data_grid_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,11 @@ export interface EuiDataGridStyle {
* If set to true, the footer row will be sticky
*/
stickyFooter?: boolean;
/**
* Optional callback returning the current `gridStyle` config when changes occur from user input (e.g. toolbar display controls).
* Can be used for, e.g. storing user `gridStyle` in a local storage object.
*/
onChange?: (gridStyle: EuiDataGridStyle) => void;
}

export interface EuiDataGridToolBarVisibilityColumnSelectorOptions {
Expand Down Expand Up @@ -767,6 +772,11 @@ export interface EuiDataGridRowHeightsOptions {
* Defines a global lineHeight style to apply to all cells
*/
lineHeight?: string;
/**
* Optional callback returning the current `rowHeightsOptions` when changes occur from user input (e.g. toolbar display controls).
* Can be used for, e.g. storing user `rowHeightsOptions` in a local storage object.
*/
onChange?: (rowHeightsOptions: EuiDataGridRowHeightsOptions) => void;
}

export interface EuiDataGridRowManager {
Expand Down
1 change: 1 addition & 0 deletions src/services/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export * from './useCombinedRefs';
export * from './useUpdateEffect';
export * from './useDependentState';
export * from './useIsWithinBreakpoints';
export * from './useMouseMove';
20 changes: 5 additions & 15 deletions src/services/hooks/useDependentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,18 @@
* Side Public License, v 1.
*/

import { useEffect, useState, useRef } from 'react';
import { useState } from 'react';
import { useUpdateEffect } from './useUpdateEffect';

export function useDependentState<T>(
valueFn: (previousState: undefined | T) => T,
deps: unknown[]
) {
const [state, setState] = useState<T>(valueFn as () => T);

// use ref instead of a state to avoid causing an unnecessary re-render
const hasMounted = useRef<boolean>(false);

useEffect(() => {
// don't call setState on initial mount
if (hasMounted.current === true) {
setState(valueFn);
} else {
hasMounted.current = true;
}

// purposefully omitting `updateCount.current` and `valueFn`
// this means updating only the valueFn has no effect, but allows for more natural feeling hook use
// eslint-disable-next-line react-hooks/exhaustive-deps
// don't call setState on initial mount
useUpdateEffect(() => {
setState(valueFn);
}, deps);

return [state, setState] as const;
Expand Down
58 changes: 58 additions & 0 deletions src/services/hooks/useUpdateEffect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 { mount } from 'enzyme';
import { useUpdateEffect } from './useUpdateEffect';

describe('useUpdateEffect', () => {
const mockEffect = jest.fn();
const mockCleanup = jest.fn();

const MockComponent = ({ test }: { test?: boolean }) => {
useUpdateEffect(() => {
mockEffect();
return () => mockCleanup();
}, [test]);

return null;
};

beforeEach(() => {
jest.clearAllMocks();
});

it('does not invoke the passed effect on initial mount', () => {
mount(<MockComponent />);

expect(mockEffect).not.toHaveBeenCalled();
});

it('invokes the passed effect on each component update/rerender', () => {
const component = mount(<MockComponent />);

component.setProps({ test: true });
expect(mockEffect).toHaveBeenCalledTimes(1);

component.setProps({ test: false });
expect(mockEffect).toHaveBeenCalledTimes(2);

component.setProps({ test: true });
expect(mockEffect).toHaveBeenCalledTimes(3);
});

it('invokes returned cleanup, same as useEffect', () => {
const component = mount(<MockComponent />);

component.setProps({ test: true }); // Trigger first update/call
expect(mockCleanup).not.toHaveBeenCalled();

component.unmount(); // Trigger cleanup
expect(mockCleanup).toHaveBeenCalled();
});
});
Loading