diff --git a/CHANGELOG.md b/CHANGELOG.md
index 336a3fb016b..59d3f902213 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/src-docs/src/views/resize_observer/resize_observer_example.js b/src-docs/src/views/resize_observer/resize_observer_example.js
index 4ffe9a4b894..d0810ba74a7 100644
--- a/src-docs/src/views/resize_observer/resize_observer_example.js
+++ b/src-docs/src/views/resize_observer/resize_observer_example.js
@@ -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: [
@@ -49,16 +49,37 @@ export const ResizeObserverExample = {
callback which you must put on the element you wish to observe.
- Due to limited browser support (currently supported in Chrome and
- Opera), EuiResizeObserver will fallback to using
- the MutationObserver API with a default set of
+ Due to limited browser support (currently not in Safari and IE11),{' '}
+ EuiResizeObserver will fallback to using the{' '}
+ MutationObserver API with a default set of
parameters that approximate the results of{' '}
MutationObserver.
),
- components: { EuiResizeObserver },
demo: ,
},
+ {
+ title: 'useResizeObserver hook',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: resizeObserverHookSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: resizeObserverHookHtml,
+ },
+ ],
+ text: (
+
+
+ There is also a React hook, useResizeObserver,
+ which provides the same observation functionality.
+
+
+ ),
+ demo: ,
+ },
],
};
diff --git a/src-docs/src/views/resize_observer/resize_observer_hook.js b/src-docs/src/views/resize_observer/resize_observer_hook.js
new file mode 100644
index 00000000000..da94ad3b4d0
--- /dev/null
+++ b/src-docs/src/views/resize_observer/resize_observer_hook.js
@@ -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 (
+
+
+ {hasResizeObserver ? (
+
+ Browser
+ supports ResizeObserver API.
+
+ ) : (
+
+ Browser does
+ not support ResizeObserver API. Using MutationObserver.
+
+ )}
+
+ {`height: ${dimensions.height}; width: ${
+ dimensions.width
+ }`}
+
+
+
+
+
+
+ Toggle container padding
+
+
+
+
+
+
+
+ {items.map(item => (
+ - {item}
+ ))}
+
+
+ add item
+
+
+
+ );
+};
diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx
index cfa59333bb9..3dcf3d117f4 100644
--- a/src/components/datagrid/data_grid.tsx
+++ b/src/components/datagrid/data_grid.tsx
@@ -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;
@@ -230,40 +231,40 @@ function useDefaultColumnWidth(
trailingControlColumns: EuiDataGridProps['leadingControlColumns'] = [],
columns: EuiDataGridProps['columns']
): number | null {
+ const containerSize = useResizeObserver(container);
+
const [defaultColumnWidth, setDefaultColumnWidth] = useState(
null
);
useEffect(() => {
- if (container != null) {
- const gridWidth = container.clientWidth;
-
- const controlColumnWidths = [
- ...leadingControlColumns,
- ...trailingControlColumns,
- ].reduce(
- (claimedWidth, controlColumn: EuiDataGridControlColumn) =>
- claimedWidth + controlColumn.width,
- 0
- );
+ const gridWidth = containerSize.width;
+
+ const controlColumnWidths = [
+ ...leadingControlColumns,
+ ...trailingControlColumns,
+ ].reduce(
+ (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;
}
diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx
index a36b4e4a4d2..a25db3c3259 100644
--- a/src/components/datagrid/data_grid_cell.tsx
+++ b/src/components/datagrid/data_grid_cell.tsx
@@ -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;
if (nextProps.width !== this.props.width) return true;
if (nextProps.renderCellValue !== this.props.renderCellValue) return true;
if (nextProps.onCellFocus !== this.props.onCellFocus) return true;
diff --git a/src/components/index.js b/src/components/index.js
index 78214b35586..fab472fa1f1 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -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';
diff --git a/src/components/observer/observer.ts b/src/components/observer/observer.ts
index 07e3caa7784..ae8c9b64e53 100644
--- a/src/components/observer/observer.ts
+++ b/src/components/observer/observer.ts
@@ -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;
}
diff --git a/src/components/observer/resize_observer/index.ts b/src/components/observer/resize_observer/index.ts
index 13179582c9a..fcc6d0459c9 100644
--- a/src/components/observer/resize_observer/index.ts
+++ b/src/components/observer/resize_observer/index.ts
@@ -1 +1 @@
-export { EuiResizeObserver } from './resize_observer';
+export { EuiResizeObserver, useResizeObserver } from './resize_observer';
diff --git a/src/components/observer/resize_observer/resize_observer.test.tsx b/src/components/observer/resize_observer/resize_observer.test.tsx
index 5866b7bd41f..f1c68b5d8ac 100644
--- a/src/components/observer/resize_observer/resize_observer.test.tsx
+++ b/src/components/observer/resize_observer/resize_observer.test.tsx
@@ -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
@@ -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 }) => {
@@ -25,6 +26,10 @@ describe('EuiResizeObserver', () => {
const component = mount(Hello World} />);
+ // Resize observer is expected to fire once on mount
+ await waitforResizeObserver();
+ expect(onResize).toHaveBeenCalledTimes(1);
+
component.setProps({
children: (
@@ -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
{children}
;
+ });
+
+ const component = mount(
Hello World } />);
+
+ // 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: (
+
+
Hello World
+
Hello Again
+
+ ),
+ });
+
+ await waitforResizeObserver();
+
+ // Expect two more calls because children changed (re-render) & resize observer reacted
+ expect(Wrapper).toHaveBeenCalledTimes(5);
+ });
+});
diff --git a/src/components/observer/resize_observer/resize_observer.tsx b/src/components/observer/resize_observer/resize_observer.tsx
index 73aca10c58a..ddaba07adcc 100644
--- a/src/components/observer/resize_observer/resize_observer.tsx
+++ b/src/components/observer/resize_observer/resize_observer.tsx
@@ -1,18 +1,26 @@
-import { ReactNode } from 'react';
-
-import { EuiObserver } from '../observer';
+import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
+import { EuiObserver, Observer } from '../observer';
interface Props {
children: (ref: (e: HTMLElement | null) => void) => ReactNode;
onResize: (dimensions: { height: number; width: number }) => void;
}
+// IE11 and Safari don't support the `ResizeObserver` API at the time of writing
+const hasResizeObserver =
+ typeof window !== 'undefined' && typeof window.ResizeObserver !== 'undefined';
+
+const mutationObserverOptions = {
+ // [MutationObserverInit](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit)
+ attributes: true, // Account for style changes from `className` or `style`
+ characterData: true, // Account for text content size differences
+ childList: true, // Account for adding/removing child nodes
+ subtree: true, // Account for deep child nodes
+};
+
export class EuiResizeObserver extends EuiObserver {
name = 'EuiResizeObserver';
- // Only Chrome and Opera support the `ResizeObserver` API at the time of writing
- hasResizeObserver = typeof window.ResizeObserver !== 'undefined';
-
onResize = () => {
if (this.childNode != null) {
// Eventually use `clientRect` on the `entries[]` returned natively
@@ -25,23 +33,65 @@ export class EuiResizeObserver extends EuiObserver {
};
beginObserve = () => {
- let observerOptions;
- if (this.hasResizeObserver) {
- this.observer = new window.ResizeObserver(this.onResize);
- } else {
- // MutationObserver fallback
- observerOptions = {
- // [MutationObserverInit](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit)
- attributes: true, // Account for style changes from `className` or `style`
- characterData: true, // Account for text content size differences
- childList: true, // Account for adding/removing child nodes
- subtree: true, // Account for deep child nodes
- };
- this.observer = new MutationObserver(this.onResize);
- requestAnimationFrame(this.onResize); // Mimic ResizeObserver behavior of triggering a resize event on init
- }
// The superclass checks that childNode is not null before invoking
// beginObserve()
- this.observer.observe(this.childNode!, observerOptions);
+ const childNode = this.childNode!;
+ this.observer = makeResizeObserver(childNode, this.onResize);
};
}
+
+const makeResizeObserver = (node: Element, callback: () => void) => {
+ let observer: Observer | undefined;
+ if (hasResizeObserver) {
+ observer = new window.ResizeObserver(callback);
+ observer.observe(node);
+ } else {
+ observer = new MutationObserver(callback);
+ observer.observe(node, mutationObserverOptions);
+ requestAnimationFrame(callback); // Mimic ResizeObserver behavior of triggering a resize event on init
+ }
+ return observer;
+};
+
+export const useResizeObserver = (container: Element | null) => {
+ const [size, _setSize] = useState({ width: 0, height: 0 });
+
+ // _currentDimensions and _setSize are used to only store the
+ // new state (and trigger a re-render) when the new dimensions actually differ
+ const _currentDimensions = useRef(size);
+ const setSize = useCallback(dimensions => {
+ if (
+ _currentDimensions.current.width !== dimensions.width ||
+ _currentDimensions.current.height !== dimensions.height
+ ) {
+ _currentDimensions.current = dimensions;
+ _setSize(dimensions);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (container != null) {
+ // ResizeObserver's first call to the observation callback is scheduled in the future
+ // so find the container's initial dimensions now
+ const boundingRect = container.getBoundingClientRect();
+ setSize({
+ width: boundingRect.width,
+ height: boundingRect.height,
+ });
+
+ const observer = makeResizeObserver(container, () => {
+ const boundingRect = container.getBoundingClientRect();
+ setSize({
+ width: boundingRect.width,
+ height: boundingRect.height,
+ });
+ });
+
+ return () => observer.disconnect();
+ } else {
+ setSize({ width: 0, height: 0 });
+ }
+ }, [container, setSize]);
+
+ return size;
+};