Skip to content

Commit 735ffa8

Browse files
committed
feat: better horizontal scrolling
1 parent 219ca0f commit 735ffa8

File tree

15 files changed

+264
-94
lines changed

15 files changed

+264
-94
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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
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+
// TODO: add check custom css styling is applied
710
const style = useGridStyle().get();
811
return (
912
<div
1013
aria-multiselectable={config.multiselectable}
11-
className={"widget-datagrid-grid table"}
14+
className={classNames("widget-datagrid-grid table", { "infinite-loading": gridSizeStore.isInfinite })}
1215
role="grid"
1316
style={style}
17+
ref={gridSizeStore.gridContainerRef}
1418
>
1519
{props.children}
1620
</div>

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import classNames from "classnames";
21
import { observer } from "mobx-react-lite";
32
import { Fragment, PropsWithChildren, ReactElement, ReactNode } from "react";
43
import {
@@ -14,12 +13,11 @@ import { SpinnerLoader } from "./loader/SpinnerLoader";
1413

1514
export const GridBody = observer(function GridBody(props: PropsWithChildren): ReactElement {
1615
const { children } = props;
17-
const { bodySize, containerRef, isInfinite, handleScroll } = useBodyScroll();
16+
const { containerRef, handleScroll } = useBodyScroll();
1817

1918
return (
2019
<div
21-
className={classNames("widget-datagrid-grid-body table-content", { "infinite-loading": isInfinite })}
22-
style={isInfinite && bodySize > 0 ? { maxHeight: `${bodySize}px` } : {}}
20+
className={"widget-datagrid-grid-body table-content"}
2321
role="rowgroup"
2422
ref={containerRef}
2523
onScroll={handleScroll}

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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { GridColumn } from "../typings/GridColumn";
2+
import { ReactNode, useCallback, useEffect, useRef } from "react";
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+
interface MockHeaderProps {
21+
visibleColumns: GridColumn[];
22+
showCheckboxColumn: boolean;
23+
showColumnSelectorColumn: boolean;
24+
updateColumnSizes: (sizes: number[]) => void;
25+
}
26+
27+
export function MockHeader({
28+
visibleColumns,
29+
showCheckboxColumn,
30+
showColumnSelectorColumn,
31+
updateColumnSizes
32+
}: MockHeaderProps): ReactNode {
33+
const headerRef = useRef<HTMLDivElement | null>(null);
34+
const resizeCallback = useCallback<ResizeObserverCallback>(() => {
35+
updateColumnSizes(getColumnSizes(headerRef.current).values().toArray());
36+
}, [headerRef, updateColumnSizes]);
37+
38+
useEffect(() => {
39+
const observer = new ResizeObserver(resizeCallback);
40+
41+
if (headerRef.current) {
42+
observer.observe(headerRef.current);
43+
}
44+
return () => {
45+
observer.disconnect();
46+
};
47+
}, [resizeCallback, headerRef]);
48+
49+
return (
50+
<div className={"grid-mock-header"} aria-hidden ref={headerRef}>
51+
{showCheckboxColumn && <div data-column-id="checkboxes" key={"checkboxes"}></div>}
52+
{visibleColumns.map(c => (
53+
<div
54+
data-column-id={c.columnId}
55+
key={c.columnId}
56+
// we set header ref here instead of the real header
57+
// as this mock header is aligned with CSS grid, so it is more reliable
58+
// the real header is aligned programmatically based on this header
59+
ref={ref => c.setHeaderElementRef(ref)}
60+
></div>
61+
))}
62+
{showColumnSelectorColumn && <div data-column-id="selector" key={"selector"}></div>}
63+
</div>
64+
);
65+
}

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: 6 additions & 0 deletions
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);
@@ -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
}

packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const [useExportProgressService] = createInjectionHooks(DG.exportProgress
99
export const [useLoaderViewModel] = createInjectionHooks(DG.loaderVM);
1010
export const [useMainGate] = createInjectionHooks(CORE.mainGate);
1111
export const [usePaginationService] = createInjectionHooks(DG.paginationService);
12+
export const [useGridSizeStore] = createInjectionHooks(DG.gridSizeStore);
1213
export const [useSelectionHelper] = createInjectionHooks(DG.selectionHelper);
1314
export const [useGridStyle] = createInjectionHooks(DG.gridColumnsStyle);
1415
export const [useQueryService] = createInjectionHooks(DG.query);

0 commit comments

Comments
 (0)