Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/datagrid-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 22 additions & 3 deletions packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import classNames from "classnames";
import { JSX, ReactElement } from "react";
import { JSX, ReactElement, RefObject } from "react";

type P = Omit<JSX.IntrinsicElements["div"], "role" | "ref">;

export interface GridProps extends P {
className?: string;
isInfinite: boolean;
containerRef: RefObject<HTMLDivElement | null>;
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 (
<div className={classNames("widget-datagrid-grid table", className)} role="grid" style={style} {...rest}>
<div
className={classNames("widget-datagrid-grid table", { "infinite-loading": isInfinite }, className)}
role="grid"
style={{
...style,
...({
"--mx-grid-width": gridWidth,
"--mx-grid-body-height": bodyHeight,
"--mx-grid-scrollbar-size": scrollBarSize
} as any)
}}
ref={containerRef}
{...rest}
>
{children}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<HTMLDivElement | null>;
}

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) {
Expand All @@ -44,15 +35,10 @@ export function GridBody(props: Props): ReactElement {

return (
<div
className={classNames(
"widget-datagrid-grid-body table-content",
{ "infinite-loading": isInfinite },
props.className
)}
style={isInfinite && bodySize > 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()}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -21,6 +21,7 @@ type GridHeaderProps = {
id: string;
isLoading: boolean;
preview?: boolean;
headerRef: RefObject<HTMLDivElement | null>;
};

export function GridHeader({
Expand All @@ -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>();
Expand All @@ -56,7 +58,7 @@ export function GridHeader({
}

return (
<div className="widget-datagrid-grid-head" role="rowgroup">
<div className="widget-datagrid-grid-head" role="rowgroup" ref={headerRef}>
<div key="headers_row" className="tr" role="row">
<CheckboxColumnHeader key="headers_column_select_all" />
{columns.map((column, index) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<C extends GridColumn, T extends ObjectItem = ObjectItem> {
CellComponent: CellComponent<C>;
Expand Down Expand Up @@ -157,6 +158,14 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
});

const selectionEnabled = selectActionHelper.selectionType !== "None";
const isInfinite = paginationType === "virtualScrolling";

const [trackBodyScrolling, bodyHeight, gridWidth, scrollBarSize, gridBodyRef, gridContainerRef, gridHeaderRef] =
useInfiniteControl({
setPage,
isInfinite,
hasMoreItems
});

return (
<Fragment>
Expand All @@ -166,6 +175,11 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
<Grid
aria-multiselectable={selectionEnabled ? selectActionHelper.selectionType === "Multi" : undefined}
style={cssGridStyles}
containerRef={gridContainerRef}
isInfinite={isInfinite}
bodyHeight={bodyHeight}
gridWidth={gridWidth}
scrollBarSize={scrollBarSize}
>
<GridHeader
availableColumns={props.availableColumns}
Expand All @@ -182,6 +196,7 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
id={props.id}
isLoading={props.columnsLoading}
preview={props.preview}
headerRef={gridHeaderRef}
/>
{showRefreshIndicator ? <RefreshIndicator /> : null}
<GridBody
Expand All @@ -192,9 +207,8 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
columnsSize={visibleColumns.length}
rowsSize={rows.length}
pageSize={pageSize}
pagination={props.paginationType}
hasMoreItems={hasMoreItems}
setPage={setPage}
trackScrolling={trackBodyScrolling}
bodyRef={gridBodyRef}
>
<RowsRenderer
preview={props.preview ?? false}
Expand Down
100 changes: 59 additions & 41 deletions packages/shared/widget-plugin-grid/src/components/InfiniteBody.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this file to useInfiniteControl

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And probably move it to hooks or something

Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
import {
CSSProperties,
PropsWithChildren,
ReactElement,
RefObject,
useCallback,
useLayoutEffect,
useRef,
useState
} from "react";
import classNames from "classnames";
import { PropsWithChildren, RefObject, useCallback, useLayoutEffect, useRef, useState } from "react";
import { useOnScreen } from "@mendix/widget-plugin-hooks/useOnScreen";

export interface InfiniteBodyProps {
className?: string;
hasMoreItems: boolean;
isInfinite: boolean;
role?: string;
setPage?: (computePage: (prevPage: number) => number) => void;
style?: CSSProperties;
}
const offsetBottom = 30;

export function useInfiniteControl(
props: PropsWithChildren<InfiniteBodyProps>
): [trackScrolling: (e: any) => void, bodySize: number, containerRef: RefObject<HTMLDivElement | null>] {
): [
trackBodyScrolling: ((e: any) => void) | undefined,
bodyHeight: string | undefined,
gridWidth: string | undefined,
scrollBarSize: string | undefined,
gridBodyRef: RefObject<HTMLDivElement | null>,
gridContainerRef: RefObject<HTMLDivElement | null>,
gridHeaderRef: RefObject<HTMLDivElement | null>
] {
const { setPage, hasMoreItems, isInfinite } = props;
const [bodySize, setBodySize] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(containerRef as RefObject<HTMLElement>);
const [gridWidth, setGridWidth] = useState<string | undefined>();
const [bodyHeight, setBodyHeight] = useState<string | undefined>();
const [scrollBarSize, setScrollBarSize] = useState<string | undefined>();
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const gridBodyRef = useRef<HTMLDivElement>(null);
const gridHeaderRef = useRef<HTMLDivElement | null>(null);
const isVisible = useOnScreen(gridBodyRef as RefObject<HTMLElement>);

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,
Expand All @@ -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<InfiniteBodyProps>): ReactElement {
const { className, isInfinite } = props;
const [trackScrolling, bodySize, containerRef] = useInfiniteControl(props);
return (
<div
ref={containerRef}
className={classNames(className, isInfinite ? "infinite-loading" : "")}
onScroll={isInfinite ? trackScrolling : undefined}
role={props.role}
style={isInfinite && bodySize > 0 ? { ...props.style, maxHeight: bodySize } : props.style}
>
{props.children}
</div>
);
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
];
}
Loading