From e12ff2cef5fc13923ffb8a4ba6417e0cfa879418 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Wed, 22 Oct 2025 11:57:12 +0200 Subject: [PATCH] chore: improve virtual scrolling --- .../datawidgets/web/_datagrid.scss | 45 +++++--- .../datagrid-web/CHANGELOG.md | 4 + .../datagrid-web/src/components/Grid.tsx | 25 ++++- .../datagrid-web/src/components/GridBody.tsx | 30 ++---- .../src/components/GridHeader.tsx | 8 +- .../datagrid-web/src/components/Widget.tsx | 20 +++- .../src/components/InfiniteBody.tsx | 100 +++++++++++------- 7 files changed, 144 insertions(+), 88 deletions(-) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index e6ecaea017..737591eada 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -394,21 +394,14 @@ $root: ".widget-datagrid"; display: grid !important; min-width: fit-content; margin-bottom: 0; + &.infinite-loading { + // in order to restrict the scroll to row area + // we need to prevent table itself to expanding beyond available position + min-width: 0; + } } } - &-content { - overflow-x: auto; - } - - &-grid-head { - display: contents; - } - - &-grid-body { - display: contents; - } - &.widget-datagrid-selection-method-click { .tr.tr-selected .td { background-color: $grid-selected-row-background; @@ -520,10 +513,30 @@ $root: ".widget-datagrid"; margin: 0 auto; } -.infinite-loading.widget-datagrid-grid-body { - // when virtual scroll is enabled we make area that holds rows scrollable - // (while the area that holds column headers still stays in place) - overflow-y: auto; +.infinite-loading { + .widget-datagrid-grid-head { + width: calc(var(--mx-grid-width) - var(--mx-grid-scrollbar-size)); + overflow-x: hidden; + } + .widget-datagrid-grid-head[data-scrolled-y="true"] { + box-shadow: 0 5px 5px -5px gray; + } + + .widget-datagrid-grid-body { + width: var(--mx-grid-width); + overflow-y: auto; + max-height: var(--mx-grid-body-height); + } + + .widget-datagrid-grid-head[data-scrolled-x="true"]:after { + content: ""; + position: absolute; + left: 0px; + width: 10px; + box-shadow: inset 5px 0 5px -5px gray; + top: 0; + bottom: 0; + } } .widget-datagrid-grid-head, diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index b9bea86823..074d8e6e66 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We improved virtual scrolling behavior when horizontal scrolling is present due to grid size. + ### Added - We fixed an issue where missing consistency checks for the captions were causing runtime errors instead of in Studio Pro diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx index 4a4e61d9ec..cbdad7495e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx @@ -1,17 +1,36 @@ import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { JSX, ReactElement, RefObject } from "react"; type P = Omit; export interface GridProps extends P { className?: string; + isInfinite: boolean; + containerRef: RefObject; + bodyHeight?: string; + gridWidth?: string; + scrollBarSize?: string; } export function Grid(props: GridProps): ReactElement { - const { className, style, children, ...rest } = props; + const { className, style, children, isInfinite, bodyHeight, gridWidth, containerRef, scrollBarSize, ...rest } = + props; return ( -
+
{children}
); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index b9c3e76bb1..4cd31d2537 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -1,9 +1,8 @@ import classNames from "classnames"; -import { Fragment, ReactElement, ReactNode } from "react"; -import { LoadingTypeEnum, PaginationEnum } from "../../typings/DatagridProps"; +import { Fragment, ReactElement, ReactNode, RefObject } from "react"; +import { LoadingTypeEnum } from "../../typings/DatagridProps"; import { SpinnerLoader } from "./loader/SpinnerLoader"; import { RowSkeletonLoader } from "./loader/RowSkeletonLoader"; -import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; interface Props { className?: string; @@ -15,20 +14,12 @@ interface Props { columnsSize: number; rowsSize: number; pageSize: number; - pagination: PaginationEnum; - hasMoreItems: boolean; - setPage?: (update: (page: number) => number) => void; + trackScrolling?: (e: any) => void; + bodyRef: RefObject; } export function GridBody(props: Props): ReactElement { - const { children, pagination, hasMoreItems, setPage } = props; - - const isInfinite = pagination === "virtualScrolling"; - const [trackScrolling, bodySize, containerRef] = useInfiniteControl({ - hasMoreItems, - isInfinite, - setPage - }); + const { children, bodyRef, trackScrolling } = props; const content = (): ReactElement => { if (props.isFirstLoad) { @@ -44,15 +35,10 @@ export function GridBody(props: Props): ReactElement { return (
0 ? { maxHeight: `${bodySize}px` } : {}} + className={classNames("widget-datagrid-grid-body table-content", props.className)} role="rowgroup" - ref={containerRef} - onScroll={isInfinite ? trackScrolling : undefined} + ref={bodyRef} + onScroll={trackScrolling} > {content()}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx index 5ef8785352..9565a39b59 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx @@ -1,4 +1,4 @@ -import { ReactElement, ReactNode, useCallback, useState } from "react"; +import { ReactElement, ReactNode, RefObject, useCallback, useState } from "react"; import { ColumnId, GridColumn } from "../typings/GridColumn"; import { CheckboxColumnHeader } from "./CheckboxColumnHeader"; import { ColumnResizer } from "./ColumnResizer"; @@ -21,6 +21,7 @@ type GridHeaderProps = { id: string; isLoading: boolean; preview?: boolean; + headerRef: RefObject; }; export function GridHeader({ @@ -37,7 +38,8 @@ export function GridHeader({ headerWrapperRenderer, id, isLoading, - preview + preview, + headerRef }: GridHeaderProps): ReactElement { const [dragOver, setDragOver] = useState<[ColumnId, "before" | "after"] | undefined>(undefined); const [isDragging, setIsDragging] = useState<[ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined>(); @@ -56,7 +58,7 @@ export function GridHeader({ } return ( -
+
{columns.map((column, index) => diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 70b7ae1aed..d4e55fed24 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -25,6 +25,7 @@ import { WidgetFooter } from "./WidgetFooter"; import { WidgetHeader } from "./WidgetHeader"; import { WidgetRoot } from "./WidgetRoot"; import { WidgetTopBar } from "./WidgetTopBar"; +import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; export interface WidgetProps { CellComponent: CellComponent; @@ -157,6 +158,14 @@ const Main = observer((props: WidgetProps): ReactElemen }); const selectionEnabled = selectActionHelper.selectionType !== "None"; + const isInfinite = paginationType === "virtualScrolling"; + + const [trackBodyScrolling, bodyHeight, gridWidth, scrollBarSize, gridBodyRef, gridContainerRef, gridHeaderRef] = + useInfiniteControl({ + setPage, + isInfinite, + hasMoreItems + }); return ( @@ -166,6 +175,11 @@ const Main = observer((props: WidgetProps): ReactElemen (props: WidgetProps): ReactElemen id={props.id} isLoading={props.columnsLoading} preview={props.preview} + headerRef={gridHeaderRef} /> {showRefreshIndicator ? : null} (props: WidgetProps): ReactElemen columnsSize={visibleColumns.length} rowsSize={rows.length} pageSize={pageSize} - pagination={props.paginationType} - hasMoreItems={hasMoreItems} - setPage={setPage} + trackScrolling={trackBodyScrolling} + bodyRef={gridBodyRef} > number) => void; - style?: CSSProperties; } const offsetBottom = 30; export function useInfiniteControl( props: PropsWithChildren -): [trackScrolling: (e: any) => void, bodySize: number, containerRef: RefObject] { +): [ + trackBodyScrolling: ((e: any) => void) | undefined, + bodyHeight: string | undefined, + gridWidth: string | undefined, + scrollBarSize: string | undefined, + gridBodyRef: RefObject, + gridContainerRef: RefObject, + gridHeaderRef: RefObject +] { const { setPage, hasMoreItems, isInfinite } = props; - const [bodySize, setBodySize] = useState(0); - const containerRef = useRef(null); - const isVisible = useOnScreen(containerRef as RefObject); + const [gridWidth, setGridWidth] = useState(); + const [bodyHeight, setBodyHeight] = useState(); + const [scrollBarSize, setScrollBarSize] = useState(); + const gridContainerRef = useRef(null); + const gridBodyRef = useRef(null); + const gridHeaderRef = useRef(null); + const isVisible = useOnScreen(gridBodyRef as RefObject); - const trackScrolling = useCallback( + const trackBodyScrolling = useCallback( (e: any) => { + const head = gridHeaderRef.current; + if (head) { + head.scrollTo({ left: e.target.scrollLeft }); + head.dataset.scrolledY = e.target.scrollTop > 0 ? "true" : "false"; + head.dataset.scrolledX = e.target.scrollLeft > 0 ? "true" : "false"; + } + + const scrollBarSize = e.target.offsetWidth - e.target.clientWidth; + setScrollBarSize(`${scrollBarSize}px`); + /** * In Windows OS the result of first expression returns a non integer and result in never loading more, require floor to solve. * note: Math floor sometimes result in incorrect integer value, @@ -48,31 +57,40 @@ export function useInfiniteControl( [hasMoreItems, setPage] ); - const calculateBodyHeight = useCallback((): void => { - if (isVisible && isInfinite && hasMoreItems && bodySize <= 0 && containerRef.current) { - setBodySize(containerRef.current.clientHeight - offsetBottom); + const lockGridBodyHeight = useCallback((): void => { + if (isVisible && isInfinite && hasMoreItems && bodyHeight === undefined && gridBodyRef.current) { + setBodyHeight(`${gridBodyRef.current.clientHeight - offsetBottom}px`); } - }, [isInfinite, hasMoreItems, bodySize, isVisible]); + }, [isInfinite, hasMoreItems, bodyHeight, isVisible]); useLayoutEffect(() => { - setTimeout(() => calculateBodyHeight(), 100); - }, [calculateBodyHeight]); + setTimeout(() => lockGridBodyHeight(), 100); + }, [lockGridBodyHeight]); - return [trackScrolling, bodySize, containerRef]; -} + useLayoutEffect(() => { + const observeTarget = gridContainerRef.current; + if (!isInfinite || !observeTarget) return; -export function InfiniteBody(props: PropsWithChildren): ReactElement { - const { className, isInfinite } = props; - const [trackScrolling, bodySize, containerRef] = useInfiniteControl(props); - return ( -
0 ? { ...props.style, maxHeight: bodySize } : props.style} - > - {props.children} -
- ); + const resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + setGridWidth(entry.target.clientWidth ? `${entry.target.clientWidth}px` : undefined); + } + }); + + resizeObserver.observe(observeTarget); + + return () => { + resizeObserver.unobserve(observeTarget); + }; + }, [isInfinite]); + + return [ + isInfinite ? trackBodyScrolling : undefined, + bodyHeight, + gridWidth, + scrollBarSize, + gridBodyRef, + gridContainerRef, + gridHeaderRef + ]; }