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..b963d60549 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,24 +513,57 @@ $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, +.widget-datagrid-grid-head { + display: grid; + + // this head is not part of the grid, so it has dedicated column template --mx-grid-template-columns-head + // but it might not be available at the initial render, so we use template from the grid --mx-grid-template-columns + // using template from the grid might to misalignment from the grid itself, + // but in practice: + // - grid has no data at that moment, so misalignment is not visible. + // - as soon as the grid itself gets rendered --mx-grid-template-columns-head gets calculated + // and everything looks like it should. + grid-template-columns: var(--mx-grid-template-columns-head, var(--mx-grid-template-columns)); +} .widget-datagrid-grid-body { // this element has to position their children (columns or headers) // as grid and have those aligned with the parent grid display: grid; // this property makes sure we align our own grid columns // to the columns defined in the global grid - grid-template-columns: subgrid; + grid-template-columns: var(--mx-grid-template-columns); +} - // ensure that we cover all columns of original top level grid - // so our own columns get aligned with the parent +.grid-mock-header { + grid-template-columns: subgrid; grid-column: 1 / -1; + display: grid; } :where(#{$root}-paging-bottom) { 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..0ac6a69f8f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx @@ -1,17 +1,25 @@ 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; } export function Grid(props: GridProps): ReactElement { - const { className, style, children, ...rest } = props; + const { className, style, children, isInfinite, containerRef, ...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/Header.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx index 54c35f8c6a..d669ea1a8f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx @@ -70,7 +70,6 @@ export function Header(props: HeaderProps): ReactElement { role="columnheader" style={!canSort ? { cursor: "unset" } : undefined} title={caption} - ref={ref => props.column.setHeaderElementRef(ref)} data-column-id={props.column.columnId} onDrop={draggableProps.onDrop} onDragEnter={draggableProps.onDragEnter} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/MockHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/MockHeader.tsx new file mode 100644 index 0000000000..fb07278ca5 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/MockHeader.tsx @@ -0,0 +1,65 @@ +import { GridColumn } from "../typings/GridColumn"; +import { ReactNode, useCallback, useEffect, useRef } from "react"; + +function getColumnSizes(container: HTMLDivElement | null): Map { + const sizes = new Map(); + if (container) { + container.querySelectorAll("[data-column-id]").forEach(c => { + const columnId = c.dataset.columnId; + if (!columnId) { + console.debug("getColumnSizes: can't find id on:", c); + return; + } + sizes.set(columnId, c.offsetWidth); + }); + } + + return sizes; +} + +interface MockHeaderProps { + visibleColumns: GridColumn[]; + showCheckboxColumn: boolean; + showColumnSelectorColumn: boolean; + updateColumnSizes: (sizes: number[]) => void; +} + +export function MockHeader({ + visibleColumns, + showCheckboxColumn, + showColumnSelectorColumn, + updateColumnSizes +}: MockHeaderProps): ReactNode { + const headerRef = useRef(null); + const resizeCallback = useCallback(() => { + updateColumnSizes(getColumnSizes(headerRef.current).values().toArray()); + }, [headerRef, updateColumnSizes]); + + useEffect(() => { + const observer = new ResizeObserver(resizeCallback); + + if (headerRef.current) { + observer.observe(headerRef.current); + } + return () => { + observer.disconnect(); + }; + }, [resizeCallback, headerRef]); + + return ( +
+ {showCheckboxColumn &&
} + {visibleColumns.map(c => ( +
c.setHeaderElementRef(ref)} + >
+ ))} + {showColumnSelectorColumn &&
} +
+ ); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 70b7ae1aed..9f5d4675b3 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -4,7 +4,7 @@ import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navig import classNames from "classnames"; import { ListActionValue, ObjectItem } from "mendix"; import { observer } from "mobx-react-lite"; -import { CSSProperties, Fragment, ReactElement, ReactNode } from "react"; +import { CSSProperties, Fragment, ReactElement, ReactNode, useState } from "react"; import { LoadingTypeEnum, PaginationEnum, @@ -25,6 +25,8 @@ 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"; +import { MockHeader } from "./MockHeader"; export interface WidgetProps { CellComponent: CellComponent; @@ -151,12 +153,28 @@ const Main = observer((props: WidgetProps): ReactElemen /> ) : null; - const cssGridStyles = gridStyle(visibleColumns, { - selectItemColumn: selectActionHelper.showCheckboxColumn, - visibilitySelectorColumn: columnsHidable - }); - const selectionEnabled = selectActionHelper.selectionType !== "None"; + const isInfinite = paginationType === "virtualScrolling"; + + const [trackBodyScrolling, bodyHeight, gridWidth, scrollBarSize, gridBodyRef, gridContainerRef, gridHeaderRef] = + useInfiniteControl({ + setPage, + isInfinite, + hasMoreItems + }); + + const [headerSizes, setHeaderSizes] = useState(undefined); + + const gridRootStyles = { + ...gridStyle(visibleColumns, { + selectItemColumn: selectActionHelper.showCheckboxColumn, + visibilitySelectorColumn: columnsHidable + }), + "--mx-grid-template-columns-header": headerSizes?.map(v => `${v}px`).join(" "), + "--mx-grid-width": gridWidth, + "--mx-grid-body-height": bodyHeight, + "--mx-grid-scrollbar-size": scrollBarSize + }; return ( @@ -165,7 +183,9 @@ 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} > + ({ takeRecords: jest.fn() })); +window.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn() +})); + function withCtx( widgetProps: WidgetProps, contextOverrides: Partial = {} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap index cdc8ba6713..6753b42539 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap @@ -11,7 +11,7 @@ exports[`Table renders the structure correctly 1`] = `
+