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

Allow EuiDataGrid's default column width to update on resize #2991

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
- Added `onColumnResize` prop to `EuiDataGrid` of type `EuiDataGridOnColumnResizeHandler` that gets called when column changes it's size ([#2963](https://github.com/elastic/eui/pull/2963))
- Added RGB format support to `EuiColorPicker` and `EuiColorStops` ([#2850](https://github.com/elastic/eui/pull/2850))
- Added alpha channel (opacity) support to `EuiColorPicker` and `EuiColorStops` ([#2850](https://github.com/elastic/eui/pull/2850))
- Added `useResizeObserver` hook ([#2991](https://github.com/elastic/eui/pull/2991))

**Bug Fixes**

- Fixed `EuiFieldNumber` so values of type `number` are now allowed ([#3020](https://github.com/elastic/eui/pull/3020))
- Fixed SASS `contrastRatio()` function in dark mode by fixing the `pow()` math function ([#3013], (https://github.com/elastic/eui/pull/3013))
- Fixed bug preventing `EuiDataGrid` from re-evaluating the default column width on resize ([#2991](https://github.com/elastic/eui/pull/2991))

## [`21.0.0`](https://github.com/elastic/eui/tree/v21.0.0)

Expand Down
39 changes: 30 additions & 9 deletions src-docs/src/views/resize_observer/resize_observer_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { renderToHtml } from '../../services';

import { GuideSectionTypes } from '../../components';

import {
EuiCode,
EuiLink,
EuiResizeObserver,
} from '../../../../src/components';
import { EuiCode, EuiLink } from '../../../../src/components';

import { ResizeObserverExample as ResizeObserver } from './resize_observer';
const resizeObserverSource = require('!!raw-loader!./resize_observer');
const resizeObserverHtml = renderToHtml(ResizeObserver);

import { ResizeObserverHookExample as ResizeObserverHook } from './resize_observer_hook';
const resizeObserverHookSource = require('!!raw-loader!./resize_observer_hook');
const resizeObserverHookHtml = renderToHtml(ResizeObserverHook);

export const ResizeObserverExample = {
title: 'ResizeObserver',
sections: [
Expand Down Expand Up @@ -49,16 +49,37 @@ export const ResizeObserverExample = {
callback which you must put on the element you wish to observe.
</p>
<p>
Due to limited browser support (currently supported in Chrome and
Opera), <EuiCode>EuiResizeObserver</EuiCode> will fallback to using
the <EuiCode>MutationObserver</EuiCode> API with a default set of
Due to limited browser support (currently not in Safari and IE11),{' '}
<EuiCode>EuiResizeObserver</EuiCode> will fallback to using the{' '}
<EuiCode>MutationObserver</EuiCode> API with a default set of
parameters that approximate the results of{' '}
<EuiCode>MutationObserver</EuiCode>.
</p>
</React.Fragment>
),
components: { EuiResizeObserver },
demo: <ResizeObserver />,
},
{
title: 'useResizeObserver hook',
source: [
{
type: GuideSectionTypes.JS,
code: resizeObserverHookSource,
},
{
type: GuideSectionTypes.HTML,
code: resizeObserverHookHtml,
},
],
text: (
<React.Fragment>
<p>
There is also a React hook, <EuiCode>useResizeObserver</EuiCode>,
which provides the same observation functionality.
</p>
</React.Fragment>
),
demo: <ResizeObserverHook />,
},
],
};
72 changes: 72 additions & 0 deletions src-docs/src/views/resize_observer/resize_observer_hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useRef, useState } from 'react';

import {
EuiButton,
EuiButtonEmpty,
EuiCode,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiText,
useResizeObserver,
} from '../../../../src/components';

export const ResizeObserverHookExample = () => {
const hasResizeObserver = typeof ResizeObserver !== 'undefined';
const [paddingSize, setPaddingSize] = useState('s');
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);

const togglePaddingSize = () => {
setPaddingSize(paddingSize => (paddingSize === 's' ? 'l' : 's'));
};

const addItem = () => {
setItems(items => [...items, `Item ${items.length + 1}`]);
};

const resizeRef = useRef();
const dimensions = useResizeObserver(resizeRef.current);

return (
<div>
<EuiText>
{hasResizeObserver ? (
<p>
<EuiIcon type="checkInCircleFilled" color="secondary" /> Browser
supports ResizeObserver API.
</p>
) : (
<p>
<EuiIcon type="crossInACircleFilled" color="danger" /> Browser does
not support ResizeObserver API. Using MutationObserver.
</p>
)}
<p>
<EuiCode>{`height: ${dimensions.height}; width: ${
dimensions.width
}`}</EuiCode>
</p>
</EuiText>

<EuiSpacer />

<EuiButton fill={true} onClick={togglePaddingSize}>
Toggle container padding
</EuiButton>

<EuiSpacer />

<div className="eui-displayInlineBlock" ref={resizeRef}>
<EuiPanel className="eui-displayInlineBlock" paddingSize={paddingSize}>
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
<EuiSpacer size="s" />
<EuiButtonEmpty onClick={addItem}>add item</EuiButtonEmpty>
</EuiPanel>
</div>
</div>
);
};
51 changes: 26 additions & 25 deletions src/components/datagrid/data_grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
import { useColumnSorting } from './column_sorting';
import { EuiMutationObserver } from '../observer/mutation_observer';
import { DataGridContext } from './data_grid_context';
import { useResizeObserver } from '../observer/resize_observer/resize_observer';

// When below this number the grid only shows the full screen button
const MINIMUM_WIDTH_FOR_GRID_CONTROLS = 479;
Expand Down Expand Up @@ -230,40 +231,40 @@ function useDefaultColumnWidth(
trailingControlColumns: EuiDataGridProps['leadingControlColumns'] = [],
columns: EuiDataGridProps['columns']
): number | null {
const containerSize = useResizeObserver(container);

const [defaultColumnWidth, setDefaultColumnWidth] = useState<number | null>(
null
);

useEffect(() => {
if (container != null) {
const gridWidth = container.clientWidth;

const controlColumnWidths = [
...leadingControlColumns,
...trailingControlColumns,
].reduce<number>(
(claimedWidth, controlColumn: EuiDataGridControlColumn) =>
claimedWidth + controlColumn.width,
0
);
const gridWidth = containerSize.width;

const controlColumnWidths = [
...leadingControlColumns,
...trailingControlColumns,
].reduce<number>(
(claimedWidth, controlColumn: EuiDataGridControlColumn) =>
claimedWidth + controlColumn.width,
0
);

const columnsWithWidths = columns.filter<
EuiDataGridColumn & { initialWidth: number }
>(doesColumnHaveAnInitialWidth);
const columnsWithWidths = columns.filter<
EuiDataGridColumn & { initialWidth: number }
>(doesColumnHaveAnInitialWidth);

const definedColumnsWidth = columnsWithWidths.reduce(
(claimedWidth, column) => claimedWidth + column.initialWidth,
0
);
const definedColumnsWidth = columnsWithWidths.reduce(
(claimedWidth, column) => claimedWidth + column.initialWidth,
0
);

const claimedWidth = controlColumnWidths + definedColumnsWidth;
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, leadingControlColumns, trailingControlColumns]);
const widthToFill = gridWidth - claimedWidth;
const unsizedColumnCount = columns.length - columnsWithWidths.length;
const columnWidth = Math.max(widthToFill / unsizedColumnCount, 100);
setDefaultColumnWidth(columnWidth);
}, [containerSize, columns, leadingControlColumns, trailingControlColumns]);

return defaultColumnWidth;
}
Expand Down
1 change: 1 addition & 0 deletions src/components/datagrid/data_grid_cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export class EuiDataGridCell extends Component<
if (nextProps.visibleRowIndex !== this.props.visibleRowIndex) return true;
if (nextProps.colIndex !== this.props.colIndex) return true;
if (nextProps.columnId !== this.props.columnId) return true;
if (nextProps.columnType !== this.props.columnType) return true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only React had a way to specify dependencies that also included an ESLint rule to tell you when you're missing one. Oh wait ... 😅

Maybe someday this'll be a function component with hooks.

if (nextProps.width !== this.props.width) return true;
if (nextProps.renderCellValue !== this.props.renderCellValue) return true;
if (nextProps.onCellFocus !== this.props.onCellFocus) return true;
Expand Down
5 changes: 4 additions & 1 deletion src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,10 @@ export { EuiProgress } from './progress';

export { EuiTreeView } from './tree_view';

export { EuiResizeObserver } from './observer/resize_observer';
export {
EuiResizeObserver,
useResizeObserver,
} from './observer/resize_observer';

export { EuiSearchBar, Query, Ast } from './search_bar';

Expand Down
2 changes: 1 addition & 1 deletion src/components/observer/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ interface BaseProps {
children: (ref: any) => ReactNode;
}

interface Observer {
export interface Observer {
disconnect: () => void;
observe: (element: Element, options?: { [key: string]: any }) => void;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/observer/resize_observer/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { EuiResizeObserver } from './resize_observer';
export { EuiResizeObserver, useResizeObserver } from './resize_observer';
60 changes: 57 additions & 3 deletions src/components/observer/resize_observer/resize_observer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { mount } from 'enzyme';
import { EuiResizeObserver } from './resize_observer';
import { EuiResizeObserver, useResizeObserver } from './resize_observer';
import { sleep } from '../../../test';
import { act } from 'react-dom/test-utils';

export async function waitforResizeObserver(period = 30) {
// `period` defaults to 30 because its the delay used by the ResizeObserver polyfill
Expand All @@ -10,7 +11,7 @@ export async function waitforResizeObserver(period = 30) {

describe('EuiResizeObserver', () => {
it('watches for a resize', async () => {
expect.assertions(1);
expect.assertions(2);
const onResize = jest.fn();

const Wrapper: FunctionComponent<{}> = ({ children }) => {
Expand All @@ -25,6 +26,10 @@ describe('EuiResizeObserver', () => {

const component = mount(<Wrapper children={<div>Hello World</div>} />);

// Resize observer is expected to fire once on mount
await waitforResizeObserver();
expect(onResize).toHaveBeenCalledTimes(1);

component.setProps({
children: (
<div>
Expand All @@ -40,3 +45,52 @@ describe('EuiResizeObserver', () => {
expect(onResize).toHaveBeenCalledTimes(2);
});
});

type GetBoundingClientRect = typeof HTMLElement['prototype']['getBoundingClientRect'];
describe('useResizeObserver', () => {
let _originalgetBoundingClientRect: undefined | GetBoundingClientRect;
beforeAll(() => {
_originalgetBoundingClientRect =
HTMLElement.prototype.getBoundingClientRect;
HTMLElement.prototype.getBoundingClientRect = function() {
// use the length of the element's HTML to represent its height
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
return { width: 100, height: this.innerHTML.length } as ReturnType<
GetBoundingClientRect
>;
};
});
afterAll(() => {
HTMLElement.prototype.getBoundingClientRect = _originalgetBoundingClientRect!;
});

it('watches for a resize', async () => {
expect.assertions(2);

const Wrapper: FunctionComponent<{}> = jest.fn(({ children }) => {
const [ref, setRef] = useState();
useResizeObserver(ref);
return <div ref={setRef}>{children}</div>;
});

const component = mount(<Wrapper children={<div>Hello World</div>} />);

// Expect the initial render, re-render when the ref is created, and a 3rd for the onresize callback
await act(() => waitforResizeObserver());
expect(Wrapper).toHaveBeenCalledTimes(3);

component.setProps({
children: (
<div>
<div>Hello World</div>
<div>Hello Again</div>
</div>
),
});

await waitforResizeObserver();

// Expect two more calls because children changed (re-render) & resize observer reacted
expect(Wrapper).toHaveBeenCalledTimes(5);
});
});
Loading