Skip to content

Commit b14e00f

Browse files
committed
chore: use position sticky for virtual scrolling
1 parent c99ddf8 commit b14e00f

File tree

9 files changed

+88
-249
lines changed

9 files changed

+88
-249
lines changed

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

Lines changed: 40 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,13 @@ $root: ".widget-datagrid";
346346
position: relative;
347347

348348
&-grid {
349+
&-head,
350+
&-body {
351+
display: contents;
352+
}
349353
&.table {
350354
display: grid !important;
355+
grid-template-columns: var(--widgets-grid-template-columns);
351356
min-width: fit-content;
352357
margin-bottom: 0;
353358
&.infinite-loading {
@@ -487,106 +492,53 @@ $root: ".widget-datagrid";
487492
}
488493

489494
.infinite-loading {
490-
.widget-datagrid-grid-head {
491-
// lock header width
492-
// and prevent it from having own scrolling
493-
// as scrolling is synchronized in JS
494-
width: calc(var(--widgets-grid-width) - var(--widgets-grid-scrollbar-size));
495-
overflow-x: hidden;
496-
}
495+
// infinite loading grid limits height to initiate scrolling
496+
max-height: var(--widgets-grid-table-height);
497+
overflow: auto;
497498

498499
.widget-datagrid-grid-head {
499-
&[data-scrolled-x="true"],
500-
&[data-scrolled-y="true"] {
501-
z-index: 1;
500+
.th {
501+
// make header sticky when grid is scrolled.
502+
position: sticky;
503+
top: 0;
504+
z-index: 10;
505+
&:has([data-overlay-content]) {
506+
// when there is popup open inside of the header cell, it should
507+
// overlay other header elements, including ones from other grids.
508+
z-index: 20;
509+
}
502510
}
503511
}
504512

505-
.widget-datagrid-grid-head[data-scrolled-y="true"] {
513+
&[data-scrolled-y] .widget-datagrid-grid-head .th:after {
506514
// add shadow under the header
507515
// implying that grid is scrolled vertically (there are rows hidden under header)
508-
// the data attribute added in JS
509-
box-shadow: 0 5px 5px -5px gray;
510-
}
511-
512-
.widget-datagrid-grid-body {
513-
// lock the size of the body
514-
// and enable it to have own scrolling
515-
// body is the leading element
516-
// header scroll will be synced to match it
517-
width: var(--widgets-grid-width);
518-
overflow-y: auto;
519-
max-height: var(--widgets-grid-body-height);
520-
}
521-
522-
.widget-datagrid-grid-head[data-scrolled-x="true"]:after {
523-
// add inner shadow to the left side of the grid
524-
// implying that the grid is scrolled horizontally (there are rows hidden on the left)
525-
// the data attribute added in JS
526516
content: "";
527517
position: absolute;
518+
bottom: -5px;
528519
left: 0;
529-
width: 10px;
530-
box-shadow: inset 5px 0 5px -5px gray;
531-
top: 0;
532-
bottom: 0;
520+
right: 0;
521+
height: 5px;
522+
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), transparent);
533523
}
534524
}
535525

536-
// styles for browsers that don't support subgrid
537-
// if the browser doesn't support subgrid
538-
// fall back header an body the contents and apply grid template to the table
539-
.widget-datagrid-grid.table {
540-
grid-template-columns: var(--widgets-grid-template-columns);
541-
}
542-
.widget-datagrid-grid-body,
543-
.widget-datagrid-grid-head {
544-
display: contents;
545-
}
546-
547-
// styles for modern browsers
548-
@supports (grid-template-rows: subgrid) {
549-
.widget-datagrid-grid.table:not([data-has-scroll-x="true"]) {
550-
grid-template-columns: var(--widgets-grid-template-columns);
551-
.widget-datagrid-grid-body,
552-
.widget-datagrid-grid-head {
553-
display: grid;
554-
min-width: 0;
555-
556-
// this property makes sure we align our own grid columns
557-
// to the columns defined in the global grid
558-
grid-template-columns: subgrid;
559-
560-
// ensure that we cover all columns of original top level grid
561-
// so our own columns get aligned with the parent
562-
grid-column: 1 / -1;
563-
}
564-
}
565-
566-
.widget-datagrid-grid.table[data-has-scroll-x="true"] {
567-
// reset the columns defined on table level
568-
// header and body will define their own instead
569-
// this is needed to make body horizontally scrollable
570-
grid-template-columns: initial;
571-
.widget-datagrid-grid-head {
572-
display: grid;
573-
min-width: 0;
574-
575-
grid-template-columns: var(--widgets-grid-template-columns-head, var(--widgets-grid-template-columns));
576-
}
577-
578-
.widget-datagrid-grid-body {
579-
display: grid;
580-
581-
grid-template-columns: var(--widgets-grid-template-columns);
582-
}
526+
// add shadows at the left to imply that grid is scrolled horizontally
527+
.widget-datagrid-content:has(.widget-datagrid-grid[data-scrolled-x]) {
528+
position: relative; // needed to keep :after element positioned inside
529+
&:after {
530+
content: "";
531+
position: absolute;
532+
left: 0;
533+
z-index: 10;
534+
bottom: 0;
535+
top: 0;
536+
width: 5px;
537+
background: linear-gradient(to right, rgba(0, 0, 0, 0.1), transparent);
538+
pointer-events: none;
583539
}
584540
}
585541

586-
.grid-mock-header {
587-
display: contents;
588-
}
589-
590542
:where(#{$root}-paging-bottom, #{$root}-paging-top) {
591543
display: flex;
592544
flex-flow: row nowrap;
@@ -681,3 +633,7 @@ $root: ".widget-datagrid";
681633
}
682634
}
683635
}
636+
637+
[data-overlay-content] {
638+
z-index: 15;
639+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import classNames from "classnames";
22
import { observer } from "mobx-react-lite";
33
import { PropsWithChildren, ReactElement } from "react";
44
import { useDatagridConfig, useGridSizeStore, useGridStyle } from "../model/hooks/injection-hooks";
5+
import { useInfiniteControl } from "../model/hooks/useInfiniteControl";
56

67
export const Grid = observer(function Grid(props: PropsWithChildren): ReactElement {
78
const config = useDatagridConfig();
89
const gridSizeStore = useGridSizeStore();
10+
const [handleScroll] = useInfiniteControl();
911

1012
const style = useGridStyle().get();
1113
return (
@@ -17,6 +19,7 @@ export const Grid = observer(function Grid(props: PropsWithChildren): ReactEleme
1719
role="grid"
1820
style={style}
1921
ref={gridSizeStore.gridContainerRef}
22+
onScroll={handleScroll}
2023
>
2124
{props.children}
2225
</div>

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,15 @@ import {
88
usePaginationVM,
99
useVisibleColumnsCount
1010
} from "../model/hooks/injection-hooks";
11-
import { useBodyScroll } from "../model/hooks/useBodyScroll";
1211
import { RowSkeletonLoader } from "./loader/RowSkeletonLoader";
1312
import { SpinnerLoader } from "./loader/SpinnerLoader";
1413

1514
export const GridBody = observer(function GridBody(props: PropsWithChildren): ReactElement {
1615
const { children } = props;
1716
const gridSizeStore = useGridSizeStore();
18-
const { handleScroll } = useBodyScroll();
1917

2018
return (
21-
<div
22-
className={"widget-datagrid-grid-body table-content"}
23-
role="rowgroup"
24-
ref={gridSizeStore.gridBodyRef}
25-
onScroll={handleScroll}
26-
>
19+
<div className={"widget-datagrid-grid-body table-content"} role="rowgroup" ref={gridSizeStore.gridBodyRef}>
2720
<ContentGuard>{children}</ContentGuard>
2821
</div>
2922
);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function Header(props: HeaderProps): ReactElement {
6464
onDrop={draggableProps.onDrop}
6565
onDragEnter={draggableProps.onDragEnter}
6666
onDragOver={draggableProps.onDragOver}
67+
ref={ref => column.setHeaderElementRef(ref)}
6768
>
6869
<div
6970
className={classNames("column-container")}

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

Lines changed: 0 additions & 69 deletions
This file was deleted.

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

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

1817
export function Widget(props: { onExportCancel?: () => void }): ReactElement {
1918
return (
@@ -27,7 +26,6 @@ export function Widget(props: { onExportCancel?: () => void }): ReactElement {
2726
<RefreshStatus />
2827
<GridBody>
2928
<RowsRenderer />
30-
<MockHeader />
3129
<EmptyPlaceholder />
3230
</GridBody>
3331
</Grid>

packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,26 @@ import { VIRTUAL_SCROLLING_OFFSET } from "../stores/GridSize.store";
66
export function useInfiniteControl(): [trackBodyScrolling: ((e: any) => void) | undefined] {
77
const gridSizeStore = useGridSizeStore();
88

9-
const isVisible = useOnScreen(gridSizeStore.gridBodyRef as RefObject<HTMLElement>);
9+
const isVisible = useOnScreen(gridSizeStore.gridContainerRef as RefObject<HTMLElement>);
1010

11-
const trackBodyScrolling = useCallback(
11+
const trackTableScrolling = useCallback(
1212
(e: UIEvent<HTMLDivElement>) => {
1313
const target = e.target as HTMLElement;
14-
const head = gridSizeStore.gridHeaderRef.current;
15-
if (head) {
16-
// synchronize header position to the body as they are decoupled
17-
// we don't use state to optimize speed as we
18-
// don't want a re-render.
19-
head.scrollTo({ left: target.scrollLeft });
20-
14+
const container = gridSizeStore.gridContainerRef.current;
15+
if (container) {
2116
// this is cosmetic, needed to provide nice shadows when body is scrolled
22-
head.dataset.scrolledY = target.scrollTop > 0 ? "true" : "false";
23-
head.dataset.scrolledX = target.scrollLeft > 0 ? "true" : "false";
17+
if (target.scrollTop > 0) {
18+
container.dataset.scrolledY = "true";
19+
} else {
20+
delete container.dataset.scrolledY;
21+
}
22+
if (target.scrollLeft > 0) {
23+
container.dataset.scrolledX = "true";
24+
} else {
25+
delete container.dataset.scrolledX;
26+
}
2427
}
2528

26-
// we need to determine scrollbar width to calculate header size correctly in css
27-
gridSizeStore.setScrollBarSize(target.offsetWidth - target.clientWidth);
28-
2929
/**
3030
* In Windows OS the result of first expression returns a non integer and result in never loading more, require floor to solve.
3131
* note: Math floor sometimes result in incorrect integer value,
@@ -42,26 +42,9 @@ export function useInfiniteControl(): [trackBodyScrolling: ((e: any) => void) |
4242
);
4343

4444
useEffect(() => {
45-
const timer = setTimeout(() => isVisible && gridSizeStore.lockGridBodyHeight(), 100);
45+
const timer = setTimeout(() => isVisible && gridSizeStore.lockGridContainerHeight(), 100);
4646
return () => clearTimeout(timer);
4747
});
4848

49-
useEffect(() => {
50-
const observeTarget = gridSizeStore.gridContainerRef.current;
51-
if (!gridSizeStore.hasVirtualScrolling || !observeTarget) return;
52-
53-
const resizeObserver = new ResizeObserver(entries => {
54-
for (const entry of entries) {
55-
gridSizeStore.setGridWidth(entry.contentRect.width);
56-
}
57-
});
58-
59-
resizeObserver.observe(observeTarget);
60-
61-
return () => {
62-
resizeObserver.unobserve(observeTarget);
63-
};
64-
}, [gridSizeStore]);
65-
66-
return [gridSizeStore.hasVirtualScrolling ? trackBodyScrolling : undefined];
49+
return [gridSizeStore.hasVirtualScrolling ? trackTableScrolling : undefined];
6750
}

packages/pluggableWidgets/datagrid-web/src/model/models/grid.model.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ export function gridStyleAtom(
1818
checkboxColumn: config.checkboxColumnEnabled,
1919
selectorColumn: config.selectorColumnEnabled
2020
}),
21-
"--widgets-grid-template-columns-head": gridSizeStore.templateColumnsHead,
22-
"--widgets-grid-body-height": asPx(gridSizeStore.gridBodyHeight),
23-
"--widgets-grid-width": asPx(gridSizeStore.gridWidth),
24-
"--widgets-grid-scrollbar-size": asPx(gridSizeStore.scrollBarSize)
21+
"--widgets-grid-table-height": asPx(gridSizeStore.gridContainerHeight)
2522
}) as CSSProperties
2623
);
2724
}

0 commit comments

Comments
 (0)