Skip to content
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
## [`master`](https://github.com/elastic/eui/tree/master)

- Added `prepend` and `append` ability to `EuiComboBox` single selection only ([#3003](https://github.com/elastic/eui/pull/3003))
- Added `prepend` and `append` ability to `EuiComboBox` single selection only ([#3003](https://github.com/elastic/eui/pull/3003))
- 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 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
33 changes: 27 additions & 6 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 @@ -57,8 +57,29 @@ export const ResizeObserverExample = {
</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 @@ -55,6 +55,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 @@ -225,40 +226,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
Copy Markdown
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';
43 changes: 40 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,35 @@ describe('EuiResizeObserver', () => {
expect(onResize).toHaveBeenCalledTimes(2);
});
});

describe('useResizeObserver', () => {
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 rected
Comment thread
chandlerprall marked this conversation as resolved.
Outdated
expect(Wrapper).toHaveBeenCalledTimes(5);
});
});
Loading