Skip to content
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 src-docs/src/views/datagrid/_snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ inMemory={{ level: 'sorting' }}`,
4: 200, // row at index 4 will adjust the height to 200px
6: 'auto', // row at index 6 will automatically adjust the height
},
scrollAnchorRow: 'start', // compensate for layout shift when auto-sized rows are scrolled into view
}}`,
ref: `// Optional. For advanced control of internal data grid state, passes back an object of imperative API methods
ref={dataGridRef}`,
Expand Down
4 changes: 4 additions & 0 deletions src-docs/src/views/datagrid/basics/virtualization.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ export default () => {
onChangeItemsPerPage: onChangeItemsPerPage,
onChangePage: onChangePage,
}}
rowHeightsOptions={{
defaultHeight: 'auto',
scrollAnchorRow: 'start',
}}
/>
</DataContext.Provider>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,42 @@ export const dataGridRowHeightOptionsExample = {
</li>
</ul>
</li>
<li>
<EuiCode>scrollAnchorRow</EuiCode>
<ul>
<li>
Optional indicator of the row that should be used as an
anchor for vertical layout shift compensation.
</li>
<li>
Can be set to the default <EuiCode>undefined</EuiCode>,
<EuiCode>&quot;start&quot;</EuiCode>, or
<EuiCode>&quot;center&quot;</EuiCode>.
</li>
<li>
If set to <EuiCode>&quot;start&quot;</EuiCode>, the topmost
visible row will monitor for unexpected changes to its
vertical position and try to compensate for these by
scrolling the grid scroll container such that the topmost
row position remains stable.
</li>
<li>
If set to <EuiCode>&quot;center&quot;</EuiCode>, the middle
visible row will monitor for unexpected changes to its
vertical position and try to compensate for these by
scrolling the grid scroll container such that the middle row
position remains stable.
</li>
<li>
This is particularly useful when the grid contains
<EuiCode>auto</EuiCode> sized rows. Since these rows are
measured as they appear in the overscan, they can cause
surprising shifts of the vertical position of all following
rows when their measured height is different from the
estimated height.
</li>
</ul>
</li>
</ul>
</EuiText>
<EuiSpacer />
Expand Down
8 changes: 7 additions & 1 deletion src/components/datagrid/body/data_grid_body.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ describe('EuiDataGridBody', () => {
resetAfterRowIndex: jest.fn(),
} as any,
};
const outerGridElementRef = { current: null };
const gridItemsRendered = { current: null };
const rerenderGridBodyRef = { current: null };
const rowHeightUtils = new RowHeightUtils(gridRef, rerenderGridBodyRef);
const rowHeightUtils = new RowHeightUtils(
gridRef,
outerGridElementRef,
gridItemsRendered,
rerenderGridBodyRef
);

const requiredProps = {
headerIsInteractive: true,
Expand Down
2 changes: 2 additions & 0 deletions src/components/datagrid/body/data_grid_body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ export const EuiDataGridBody: FunctionComponent<EuiDataGridBodyProps> = (
*/
const rowHeightUtils = useRowHeightUtils({
gridRef,
outerGridElementRef: outerGridRef,
gridItemsRenderedRef: gridItemsRendered,
gridStyles,
columns,
rowHeightsOptions,
Expand Down
54 changes: 54 additions & 0 deletions src/components/datagrid/body/data_grid_cell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { EuiDataGridCell } from './data_grid_cell';

describe('EuiDataGridCell', () => {
const mockRowHeightUtils = new RowHeightUtils(
{ current: null },
{ current: null },
{ current: null },
{ current: null }
);
Expand Down Expand Up @@ -235,6 +237,58 @@ describe('EuiDataGridCell', () => {
);
expect(popoverContent).toMatchSnapshot();
});

describe('rowHeightsOptions.scrollAnchorRow', () => {
let component: ReactWrapper;

beforeEach(() => {
component = mount(
<EuiDataGridCell
{...requiredProps}
rowHeightsOptions={{
defaultHeight: 'auto',
scrollAnchorRow: 'start',
}}
style={{ top: '30px' }}
/>
);
});

it('compensates for layout shifts', () => {
component.setProps({ style: { top: '60px' } });
expect(
mockRowHeightUtils.compensateForLayoutShift
).toHaveBeenCalledWith(0, 30, 'start');
});

describe('does not compensate for layout shifts when', () => {
afterEach(() => {
expect(
mockRowHeightUtils.compensateForLayoutShift
).not.toHaveBeenCalled();
});

test('the rowIndex is changing', () => {
component.setProps({ style: '60px', rowIndex: 3 });
});

test('the columnId is changing', () => {
component.setProps({ style: '60px', columnId: 'someOtherColumn' });
});

test('scrollAnchorRow is undefined', () => {
component.setProps({ rowHeightsOptions: { defaultHeight: 20 } });
});

test('the cell is not the first cell in the row', () => {
component.setProps({ colIndex: 1 });
});

test('the cell top position is not changing', () => {
component.setProps({ style: { top: '30px' } });
});
});
});
});

describe('componentDidMount', () => {
Expand Down
16 changes: 16 additions & 0 deletions src/components/datagrid/body/data_grid_cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,22 @@ export class EuiDataGridCell extends Component<
this.recalculateLineHeight();
}

if (
this.props.rowHeightsOptions?.scrollAnchorRow &&
this.props.colIndex === 0 && // once per row
this.props.columnId === prevProps.columnId && // if this is still the same column
this.props.rowIndex === prevProps.rowIndex && // if this is still the same row
this.props.style?.top !== prevProps.style?.top // if the top position has changed
) {
const previousTop = parseFloat(prevProps.style?.top as string);
const currentTop = parseFloat(this.props.style?.top as string);
this.props.rowHeightUtils?.compensateForLayoutShift(
this.props.rowIndex,
currentTop - previousTop,
this.props.rowHeightsOptions?.scrollAnchorRow
);
}

if (
this.props.popoverContext.popoverIsOpen !==
prevProps.popoverContext.popoverIsOpen ||
Expand Down
9 changes: 9 additions & 0 deletions src/components/datagrid/data_grid_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,8 @@ export type EuiDataGridOnColumnResizeHandler = (
data: EuiDataGridOnColumnResizeData
) => void;

export type EuiDataGridScrollAnchorRow = 'start' | 'center' | undefined;

export type EuiDataGridRowHeightOption =
| number
| 'auto'
Expand All @@ -916,6 +918,13 @@ export interface EuiDataGridRowHeightsOptions {
* Can be used for, e.g. storing user `rowHeightsOptions` in a local storage object.
*/
onChange?: (rowHeightsOptions: EuiDataGridRowHeightsOptions) => void;
/**
* Optional indicator of the row that should be used as an anchor for vertical layout shift compensation.
* When set to 'start' or 'center', the topmost or middle visible row will try
* to compensate for changes in their top offsets by adjusting the grid's scroll
* position.
*/
scrollAnchorRow?: EuiDataGridScrollAnchorRow;
}

export interface EuiDataGridRowManager {
Expand Down
1 change: 1 addition & 0 deletions src/components/datagrid/utils/__mocks__/row_heights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const RowHeightUtils = jest
isRowHeightOverride: jest.fn(rowHeightUtils.isRowHeightOverride),
resetRow: jest.fn(),
resetGrid: jest.fn(),
compensateForLayoutShift: jest.fn(),
};

return (rowHeightUtilsMock as any) as ActualRowHeightUtils;
Expand Down
121 changes: 120 additions & 1 deletion src/components/datagrid/utils/row_heights.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ describe('RowHeightUtils', () => {
scrollToItem: jest.fn(),
},
};
const outerGridElementRef = { current: null };
const gridItemsRenderedRef = { current: null };
const rerenderGridBodyRef = { current: jest.fn() };
const rowHeightUtils = new RowHeightUtils(gridRef, rerenderGridBodyRef);
const rowHeightUtils = new RowHeightUtils(
gridRef,
outerGridElementRef,
gridItemsRenderedRef,
rerenderGridBodyRef
);

beforeEach(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -422,6 +429,118 @@ describe('RowHeightUtils', () => {
});
});
});

describe('layout shift compensation', () => {
it('can compensate vertical shifts of the start anchor row', () => {
const rowHeightUtils = new RowHeightUtils(
gridRef,
{
current: {
scrollTop: 100,
} as any,
},
{
current: {
overscanRowStartIndex: 1,
overscanRowStopIndex: 12,
overscanColumnStartIndex: 0,
overscanColumnStopIndex: 1,
visibleRowStartIndex: 2,
visibleRowStopIndex: 11,
visibleColumnStartIndex: 0,
visibleColumnStopIndex: 1,
},
},
rerenderGridBodyRef
);

// the center row shifted by 10 pixels
rowHeightUtils.compensateForLayoutShift(4, 10, 'start');

// no scrolling should have taken place
expect(gridRef.current?.scrollTo).toHaveBeenCalledTimes(0);

// the anchor row shifted by 23 pixels
rowHeightUtils.compensateForLayoutShift(2, 23, 'start');

// the grid should have scrolled accordingly
expect(gridRef.current?.scrollTo).toHaveBeenCalledWith(
expect.objectContaining({
scrollTop: 123,
})
);
});

it('can compensate vertical shifts of the center anchor row', () => {
const rowHeightUtils = new RowHeightUtils(
gridRef,
{
current: {
scrollTop: 100,
} as any,
},
{
current: {
overscanRowStartIndex: 1,
overscanRowStopIndex: 12,
overscanColumnStartIndex: 0,
overscanColumnStopIndex: 1,
visibleRowStartIndex: 2,
visibleRowStopIndex: 11,
visibleColumnStartIndex: 0,
visibleColumnStopIndex: 1,
},
},
rerenderGridBodyRef
);

// the topmost visible row shifted by 10 pixels
rowHeightUtils.compensateForLayoutShift(2, 10, 'center');

// no scrolling should have taken place
expect(gridRef.current?.scrollTo).toHaveBeenCalledTimes(0);

// the anchor row shifted by 23 pixels
rowHeightUtils.compensateForLayoutShift(4, 23, 'center');

// the grid should have scrolled accordingly
expect(gridRef.current?.scrollTo).toHaveBeenCalledWith(
expect.objectContaining({
scrollTop: 123,
})
);
});

it("doesn't compensate vertical shifts when no anchor row is specified", () => {
const rowHeightUtils = new RowHeightUtils(
gridRef,
{
current: {
scrollTop: 100,
} as any,
},
{
current: {
overscanRowStartIndex: 1,
overscanRowStopIndex: 12,
overscanColumnStartIndex: 0,
overscanColumnStopIndex: 1,
visibleRowStartIndex: 2,
visibleRowStopIndex: 11,
visibleColumnStartIndex: 0,
visibleColumnStopIndex: 1,
},
},
rerenderGridBodyRef
);

// the topmost visible row shifted by 23 pixels, but no anchor has been specified
rowHeightUtils.compensateForLayoutShift(2, 23, undefined);

// no scrolling should have taken place
expect(gridRef.current?.scrollTo).toHaveBeenCalledTimes(0);
});
});
});
});

Expand Down
Loading