Skip to content

Commit

Permalink
Dynamically detect header row height
Browse files Browse the repository at this point in the history
  • Loading branch information
chandlerprall committed Dec 18, 2020
1 parent 00ed696 commit 8b4ed63
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 56 deletions.
74 changes: 39 additions & 35 deletions src/components/datagrid/data_grid_body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ import {
EuiDataGridHeaderRow,
EuiDataGridHeaderRowProps,
} from './data_grid_header_row';
import { EuiMutationObserver } from '../observer/mutation_observer';
import { useMutationObserver } from '../observer/mutation_observer';
import { EuiText } from '../text';
import { DataGridSortingContext } from './data_grid_context';
import { useResizeObserver } from '../observer/resize_observer';

export interface EuiDataGridBodyProps {
gridHeight?: number;
Expand Down Expand Up @@ -111,7 +112,6 @@ const providedPopoverContents: EuiDataGridPopoverContents = {
},
};

const HEADER_ROW_HEIGHT = 34;
const FOOTER_ROW_HEIGHT = 34;

const DefaultColumnFormatter: EuiDataGridPopoverContent = ({ children }) => {
Expand All @@ -137,6 +137,7 @@ const Cell: FunctionComponent<GridChildComponentProps> = ({
renderCellValue,
interactiveCellId,
setRowHeight,
headerRowDimensions,
} = data;

_rowIndex += rowOffset;
Expand Down Expand Up @@ -186,7 +187,9 @@ const Cell: FunctionComponent<GridChildComponentProps> = ({
setRowHeight={setRowHeight}
style={{
...style,
top: `${parseFloat(style.top as string) + HEADER_ROW_HEIGHT}px`,
top: `${
parseFloat(style.top as string) + headerRowDimensions.height
}px`,
}}
/>
);
Expand All @@ -210,7 +213,9 @@ const Cell: FunctionComponent<GridChildComponentProps> = ({
className={classes}
style={{
...style,
top: `${parseFloat(style.top as string) + HEADER_ROW_HEIGHT}px`,
top: `${
parseFloat(style.top as string) + headerRowDimensions.height
}px`,
}}
/>
);
Expand Down Expand Up @@ -247,7 +252,9 @@ const Cell: FunctionComponent<GridChildComponentProps> = ({
className={classes}
style={{
...style,
top: `${parseFloat(style.top as string) + HEADER_ROW_HEIGHT}px`,
top: `${
parseFloat(style.top as string) + headerRowDimensions.height
}px`,
}}
/>
);
Expand Down Expand Up @@ -287,6 +294,14 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
switchColumnPos,
} = props;

const [headerRowRef, setHeaderRowRef] = useState<HTMLDivElement | null>(null);

useMutationObserver(headerRowRef, handleHeaderMutation, {
subtree: true,
childList: true,
});
const headerRowDimensions = useResizeObserver(headerRowRef, 'height');

const startRow = pagination ? pagination.pageIndex * pagination.pageSize : 0;
let endRow = pagination
? (pagination.pageIndex + 1) * pagination.pageSize
Expand Down Expand Up @@ -356,7 +371,7 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
}

return rowMap;
}, [sorting, inMemory, inMemoryValues, schema, schemaDetectors]);
}, [sorting, inMemoryValues, schema, schemaDetectors]);

const mergedPopoverContents = useMemo(
() => ({
Expand All @@ -368,34 +383,22 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (

const headerRow = useMemo(() => {
return (
<>
<EuiMutationObserver
observerOptions={{
subtree: true,
childList: true,
}}
onMutation={handleHeaderMutation}>
{(ref) => (
<EuiDataGridHeaderRow
ref={ref}
switchColumnPos={switchColumnPos}
setVisibleColumns={setVisibleColumns}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
columns={columns}
columnWidths={columnWidths}
defaultColumnWidth={defaultColumnWidth}
setColumnWidth={setColumnWidth}
schema={schema}
schemaDetectors={schemaDetectors}
headerIsInteractive={headerIsInteractive}
/>
)}
</EuiMutationObserver>
</>
<EuiDataGridHeaderRow
ref={setHeaderRowRef}
switchColumnPos={switchColumnPos}
setVisibleColumns={setVisibleColumns}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
columns={columns}
columnWidths={columnWidths}
defaultColumnWidth={defaultColumnWidth}
setColumnWidth={setColumnWidth}
schema={schema}
schemaDetectors={schemaDetectors}
headerIsInteractive={headerIsInteractive}
/>
);
}, [
handleHeaderMutation,
switchColumnPos,
setVisibleColumns,
leadingControlColumns,
Expand Down Expand Up @@ -450,7 +453,7 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
ref={ref}
style={{
...style,
height: style.height + HEADER_ROW_HEIGHT,
height: style.height + headerRowDimensions.height,
}}
{...rest}>
{headerRow}
Expand All @@ -461,7 +464,7 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
);
}
),
[headerRow, footerRow]
[headerRowDimensions.height, headerRow, footerRow]
);

const gridRef = useRef<Grid>(null);
Expand Down Expand Up @@ -519,7 +522,7 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
gridHeight ||
rowHeight * visibleRowIndices.length +
SCROLLBAR_HEIGHT +
HEADER_ROW_HEIGHT +
headerRowDimensions.height +
(footerRow ? FOOTER_ROW_HEIGHT : 0)
}
rowHeight={getRowHeight}
Expand All @@ -536,6 +539,7 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
defaultColumnWidth,
renderCellValue,
interactiveCellId,
headerRowDimensions,
}}
rowCount={visibleRowIndices.length}>
{Cell}
Expand Down
2 changes: 1 addition & 1 deletion src/components/observer/mutation_observer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
* under the License.
*/

export { EuiMutationObserver } from './mutation_observer';
export { EuiMutationObserver, useMutationObserver } from './mutation_observer';
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
* under the License.
*/

import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { mount } from 'enzyme';
import { EuiMutationObserver } from './mutation_observer';
import { EuiMutationObserver, useMutationObserver } from './mutation_observer';
import { sleep } from '../../../test';

export async function waitforMutationObserver(period = 30) {
Expand Down Expand Up @@ -55,3 +55,33 @@ describe('EuiMutationObserver', () => {
expect(onMutation).toHaveBeenCalledTimes(1);
});
});

describe('useMutationObserver', () => {
it('watches changing content', async () => {
expect.assertions(2);

const mutationCallback = jest.fn();
const Wrapper: FunctionComponent<{}> = jest.fn(({ children }) => {
const [ref, setRef] = useState<Element | null>(null);
useMutationObserver(ref, mutationCallback, { childList: true, subtree: true });
return <div ref={setRef}>{children}</div>;
});

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

await waitforMutationObserver();
expect(mutationCallback).toHaveBeenCalledTimes(0);

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

await waitforMutationObserver();
expect(mutationCallback).toHaveBeenCalledTimes(1);
});
});
71 changes: 53 additions & 18 deletions src/components/observer/mutation_observer/mutation_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { ReactNode } from 'react';
import { ReactNode, useEffect } from 'react';

import { EuiObserver } from '../observer';

Expand All @@ -40,22 +40,57 @@ export class EuiMutationObserver extends EuiObserver<Props> {
};

beginObserve = () => {
// IE11 and the MutationObserver polyfill used in Kibana (for Jest) implement
// an older spec in which specifying `attributeOldValue` or `attributeFilter`
// without specifying `attributes` results in a `SyntaxError`.
// The following logic patches the newer spec in which `attributes: true` can be
// implied when appropriate (`attributeOldValue` or `attributeFilter` is specified).
const observerOptions: MutationObserverInit = {
...this.props.observerOptions,
};
const needsAttributes =
observerOptions.hasOwnProperty('attributeOldValue') ||
observerOptions.hasOwnProperty('attributeFilter');
if (needsAttributes && !observerOptions.hasOwnProperty('attributes')) {
observerOptions.attributes = true;
}

this.observer = new MutationObserver(this.onMutation);
this.observer.observe(this.childNode!, observerOptions);
const childNode = this.childNode!;
this.observer = makeMutationObserver(
childNode,
this.props.observerOptions,
this.onMutation
);
};
}

const makeMutationObserver = (
node: Element,
_observerOptions: MutationObserverInit | undefined,
callback: MutationCallback
) => {
// IE11 and the MutationObserver polyfill used in Kibana (for Jest) implement
// an older spec in which specifying `attributeOldValue` or `attributeFilter`
// without specifying `attributes` results in a `SyntaxError`.
// The following logic patches the newer spec in which `attributes: true` can be
// implied when appropriate (`attributeOldValue` or `attributeFilter` is specified).
const observerOptions: MutationObserverInit = {
..._observerOptions,
};
const needsAttributes =
observerOptions.hasOwnProperty('attributeOldValue') ||
observerOptions.hasOwnProperty('attributeFilter');
if (needsAttributes && !observerOptions.hasOwnProperty('attributes')) {
observerOptions.attributes = true;
}

const observer = new MutationObserver(callback);
observer.observe(node, observerOptions);

return observer;
};

export const useMutationObserver = (
container: Element | null,
callback: MutationCallback,
observerOptions?: MutationObserverInit
) => {
useEffect(
() => {
if (container != null) {
const observer = makeMutationObserver(container, observerOptions, () =>
console.log('SOMETHING CHANGED')
);
return () => observer.disconnect();
}
},
// ignore changing observerOptions
// eslint-disable-next-line
[container, callback]
);
};

0 comments on commit 8b4ed63

Please sign in to comment.