Skip to content

Commit 02cf2af

Browse files
committed
feat: better horizontal virtual scrolling
1 parent 219ca0f commit 02cf2af

File tree

16 files changed

+326
-53
lines changed

16 files changed

+326
-53
lines changed

packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,11 @@ $root: ".widget-datagrid";
395395
display: grid !important;
396396
min-width: fit-content;
397397
margin-bottom: 0;
398+
&.infinite-loading {
399+
// in order to restrict the scroll to row area
400+
// we need to prevent table itself to expanding beyond available position
401+
min-width: 0;
402+
}
398403
}
399404
}
400405

@@ -530,24 +535,57 @@ $root: ".widget-datagrid";
530535
margin: var(--spacing-small, 8px) 0;
531536
}
532537

533-
.infinite-loading.widget-datagrid-grid-body {
534-
// when virtual scroll is enabled we make area that holds rows scrollable
535-
// (while the area that holds column headers still stays in place)
536-
overflow-y: auto;
538+
.infinite-loading {
539+
.widget-datagrid-grid-head {
540+
width: calc(var(--mx-grid-width) - var(--mx-grid-scrollbar-size));
541+
overflow-x: hidden;
542+
}
543+
.widget-datagrid-grid-head[data-scrolled-y="true"] {
544+
box-shadow: 0 5px 5px -5px gray;
545+
}
546+
547+
.widget-datagrid-grid-body {
548+
width: var(--mx-grid-width);
549+
overflow-y: auto;
550+
max-height: var(--mx-grid-body-height);
551+
}
552+
553+
.widget-datagrid-grid-head[data-scrolled-x="true"]:after {
554+
content: "";
555+
position: absolute;
556+
left: 0px;
557+
width: 10px;
558+
box-shadow: inset 5px 0 5px -5px gray;
559+
top: 0;
560+
bottom: 0;
561+
}
537562
}
538563

539-
.widget-datagrid-grid-head,
564+
.widget-datagrid-grid-head {
565+
display: grid;
566+
567+
// this head is not part of the grid, so it has dedicated column template --mx-grid-template-columns-head
568+
// but it might not be available at the initial render, so we use template from the grid --mx-grid-template-columns
569+
// using template from the grid might to misalignment from the grid itself,
570+
// but in practice:
571+
// - grid has no data at that moment, so misalignment is not visible.
572+
// - as soon as the grid itself gets rendered --mx-grid-template-columns-head gets calculated
573+
// and everything looks like it should.
574+
grid-template-columns: var(--mx-grid-template-columns-head, var(--mx-grid-template-columns));
575+
}
540576
.widget-datagrid-grid-body {
541577
// this element has to position their children (columns or headers)
542578
// as grid and have those aligned with the parent grid
543579
display: grid;
544580
// this property makes sure we align our own grid columns
545581
// to the columns defined in the global grid
546-
grid-template-columns: subgrid;
582+
grid-template-columns: var(--mx-grid-template-columns);
583+
}
547584

548-
// ensure that we cover all columns of original top level grid
549-
// so our own columns get aligned with the parent
585+
.grid-mock-header {
586+
grid-template-columns: subgrid;
550587
grid-column: 1 / -1;
588+
display: grid;
551589
}
552590

553591
:where(#{$root}-paging-bottom, #{$root}-padding-top) {

packages/pluggableWidgets/datagrid-web/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- We improved virtual scrolling behavior when horizontal scrolling is present due to grid size.
12+
913
### Added
1014

1115
- We added a new property for export to excel. The new property allows to set the cell export type and also the format for type number and date.

packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
import classNames from "classnames";
12
import { observer } from "mobx-react-lite";
23
import { PropsWithChildren, ReactElement } from "react";
3-
import { useDatagridConfig, useGridStyle } from "../model/hooks/injection-hooks";
4+
import { useDatagridConfig, useGridSizeStore, useGridStyle } from "../model/hooks/injection-hooks";
45

56
export const Grid = observer(function Grid(props: PropsWithChildren): ReactElement {
67
const config = useDatagridConfig();
8+
const gridSizeStore = useGridSizeStore();
9+
10+
// TODO: add check custom css styling is applie
711
const style = useGridStyle().get();
812
return (
913
<div
1014
aria-multiselectable={config.multiselectable}
11-
className={"widget-datagrid-grid table"}
15+
className={classNames("widget-datagrid-grid table", { "infinite-loading": gridSizeStore.isInfinite })}
1216
role="grid"
1317
style={style}
18+
ref={gridSizeStore.gridContainerRef}
1419
>
1520
{props.children}
1621
</div>

packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import classNames from "classnames";
21
import { observer } from "mobx-react-lite";
32
import { Fragment, PropsWithChildren, ReactElement, ReactNode } from "react";
43
import {
54
useDatagridConfig,
5+
useGridSizeStore,
66
useItemCount,
77
useLoaderViewModel,
88
usePaginationService,
@@ -14,14 +14,14 @@ import { SpinnerLoader } from "./loader/SpinnerLoader";
1414

1515
export const GridBody = observer(function GridBody(props: PropsWithChildren): ReactElement {
1616
const { children } = props;
17-
const { bodySize, containerRef, isInfinite, handleScroll } = useBodyScroll();
17+
const gridSizeStore = useGridSizeStore();
18+
const { handleScroll } = useBodyScroll();
1819

1920
return (
2021
<div
21-
className={classNames("widget-datagrid-grid-body table-content", { "infinite-loading": isInfinite })}
22-
style={isInfinite && bodySize > 0 ? { maxHeight: `${bodySize}px` } : {}}
22+
className={"widget-datagrid-grid-body table-content"}
2323
role="rowgroup"
24-
ref={containerRef}
24+
ref={gridSizeStore.gridBodyRef}
2525
onScroll={handleScroll}
2626
>
2727
<ContentGuard>{children}</ContentGuard>

packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReactElement, useState } from "react";
2-
import { useColumnsStore, useDatagridConfig } from "../model/hooks/injection-hooks";
2+
import { useColumnsStore, useDatagridConfig, useGridSizeStore } from "../model/hooks/injection-hooks";
33
import { ColumnId } from "../typings/GridColumn";
44
import { CheckboxColumnHeader } from "./CheckboxColumnHeader";
55
import { ColumnProvider } from "./ColumnProvider";
@@ -11,6 +11,7 @@ import { HeaderSkeletonLoader } from "./loader/HeaderSkeletonLoader";
1111
export function GridHeader(): ReactElement {
1212
const { columnsHidable, id: gridId } = useDatagridConfig();
1313
const columnsStore = useColumnsStore();
14+
const gridSizeStore = useGridSizeStore();
1415
const columns = columnsStore.visibleColumns;
1516
const [dragOver, setDragOver] = useState<[ColumnId, "before" | "after"] | undefined>(undefined);
1617
const [isDragging, setIsDragging] = useState<[ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined>();
@@ -20,7 +21,7 @@ export function GridHeader(): ReactElement {
2021
}
2122

2223
return (
23-
<div className="widget-datagrid-grid-head" role="rowgroup">
24+
<div className="widget-datagrid-grid-head" role="rowgroup" ref={gridSizeStore.gridHeaderRef}>
2425
<div key="headers_row" className="tr" role="row">
2526
<CheckboxColumnHeader key="headers_column_select_all" />
2627
{columns.map(column => (

packages/pluggableWidgets/datagrid-web/src/components/Header.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ export function Header(props: HeaderProps): ReactElement {
6060
role="columnheader"
6161
style={!canSort ? { cursor: "unset" } : undefined}
6262
title={caption}
63-
ref={ref => column.setHeaderElementRef(ref)}
6463
data-column-id={column.columnId}
6564
onDrop={draggableProps.onDrop}
6665
onDragEnter={draggableProps.onDragEnter}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ReactNode, useCallback, useEffect, useRef } from "react";
2+
import { useColumnsStore, useDatagridConfig, useGridSizeStore } from "../model/hooks/injection-hooks";
3+
4+
function getColumnSizes(container: HTMLDivElement | null): Map<string, number> {
5+
const sizes = new Map<string, number>();
6+
if (container) {
7+
container.querySelectorAll<HTMLDivElement>("[data-column-id]").forEach(c => {
8+
const columnId = c.dataset.columnId;
9+
if (!columnId) {
10+
console.debug("getColumnSizes: can't find id on:", c);
11+
return;
12+
}
13+
sizes.set(columnId, c.offsetWidth);
14+
});
15+
}
16+
17+
return sizes;
18+
}
19+
20+
export function MockHeader(): ReactNode {
21+
const columnsStore = useColumnsStore();
22+
const config = useDatagridConfig();
23+
const gridSizeStore = useGridSizeStore();
24+
const headerRef = useRef<HTMLDivElement | null>(null);
25+
const resizeCallback = useCallback<ResizeObserverCallback>(() => {
26+
gridSizeStore.updateColumnSizes(getColumnSizes(headerRef.current).values().toArray());
27+
}, [headerRef, gridSizeStore]);
28+
29+
useEffect(() => {
30+
const observer = new ResizeObserver(resizeCallback);
31+
32+
if (headerRef.current) {
33+
observer.observe(headerRef.current);
34+
}
35+
return () => {
36+
observer.disconnect();
37+
};
38+
}, [resizeCallback, headerRef]);
39+
40+
return (
41+
<div className={"grid-mock-header"} aria-hidden ref={headerRef}>
42+
{config.checkboxColumnEnabled && <div data-column-id="checkboxes" key={"checkboxes"}></div>}
43+
{columnsStore.visibleColumns.map(c => (
44+
<div
45+
data-column-id={c.columnId}
46+
key={c.columnId}
47+
// we set header ref here instead of the real header
48+
// as this mock header is aligned with CSS grid, so it is more reliable
49+
// the real header is aligned programmatically based on this header
50+
ref={ref => c.setHeaderElementRef(ref)}
51+
></div>
52+
))}
53+
{config.selectorColumnEnabled && <div data-column-id="selector" key={"selector"}></div>}
54+
</div>
55+
);
56+
}

packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { WidgetFooter } from "./WidgetFooter";
1313
import { WidgetHeader } from "./WidgetHeader";
1414
import { WidgetRoot } from "./WidgetRoot";
1515
import { WidgetTopBar } from "./WidgetTopBar";
16+
import { MockHeader } from "./MockHeader";
1617

1718
export function Widget(props: { onExportCancel?: () => void }): ReactElement {
1819
return (
@@ -25,6 +26,7 @@ export function Widget(props: { onExportCancel?: () => void }): ReactElement {
2526
<SelectAllBar />
2627
<RefreshStatus />
2728
<GridBody>
29+
<MockHeader />
2830
<RowsRenderer />
2931
<EmptyPlaceholder />
3032
</GridBody>

packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnStore.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export class ColumnStore implements GridColumn {
2828
private baseInfo: BaseColumnInfo;
2929
private parentStore: IColumnParentStore;
3030

31+
// this holds size of the column that it had prior to resizing started
32+
// this is needed to prevent personalization being saved while resizing
3133
private frozenSize: number | undefined;
3234

3335
// dynamic props from PW API

packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { DerivedLoaderController } from "../services/DerivedLoaderController";
3232
import { PaginationController } from "../services/PaginationController";
3333
import { SelectionGate } from "../services/SelectionGate.service";
3434
import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens";
35+
import { GridSizeStore } from "../stores/GridSize.store";
3536

3637
// base
3738
injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.filterHost);
@@ -40,6 +41,7 @@ injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.
4041
injected(PaginationController, CORE.setupService, DG.paginationConfig, DG.query);
4142
injected(GridBasicData, CORE.mainGate);
4243
injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM);
44+
injected(GridSizeStore, DG.paginationService);
4345

4446
// loader
4547
injected(DerivedLoaderController, DG.query, DG.exportProgressService, CORE.columnsStore, DG.loaderConfig);
@@ -58,7 +60,7 @@ injected(GridPersonalizationStore, CORE.setupService, CORE.mainGate, CORE.column
5860
// selection
5961
injected(SelectionGate, CORE.mainGate);
6062
injected(createSelectionHelper, CORE.setupService, DG.selectionGate, CORE.config.optional);
61-
injected(gridStyleAtom, CORE.columnsStore, CORE.config);
63+
injected(gridStyleAtom, CORE.columnsStore, CORE.config, DG.gridSizeStore);
6264
injected(rowClassProvider, CORE.mainGate);
6365

6466
// row-interaction
@@ -98,6 +100,8 @@ export class DatagridContainer extends Container {
98100
this.bind(DG.query).toInstance(DatasourceService).inSingletonScope();
99101
// Pagination service
100102
this.bind(DG.paginationService).toInstance(PaginationController).inSingletonScope();
103+
// Grid sizing and scrolling store
104+
this.bind(DG.gridSizeStore).toInstance(GridSizeStore).inSingletonScope();
101105
// Datasource params service
102106
this.bind(DG.paramsService).toInstance(DatasourceParamsController).inSingletonScope();
103107
// FilterAPI
@@ -229,5 +233,7 @@ export class DatagridContainer extends Container {
229233

230234
// Hydrate filters from props
231235
this.get(DG.combinedFilter).hydrate(props.datasource.filter);
236+
237+
this.get(DG.gridSizeStore);
232238
}
233239
}

0 commit comments

Comments
 (0)