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,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) {
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
14 changes: 11 additions & 3 deletions packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
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>;
}

export function Grid(props: GridProps): ReactElement {
const { className, style, children, ...rest } = props;
const { className, style, children, isInfinite, containerRef, ...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}
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 @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { GridColumn } from "../typings/GridColumn";
import { ReactNode, useCallback, useEffect, useRef } from "react";

function getColumnSizes(container: HTMLDivElement | null): Map<string, number> {
const sizes = new Map<string, number>();
if (container) {
container.querySelectorAll<HTMLDivElement>("[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<HTMLDivElement | null>(null);
const resizeCallback = useCallback<ResizeObserverCallback>(() => {
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 (
<div className={"grid-mock-header"} aria-hidden ref={headerRef}>
{showCheckboxColumn && <div data-column-id="checkboxes" key={"checkboxes"}></div>}
{visibleColumns.map(c => (
<div
data-column-id={c.columnId}
key={c.columnId}
// we set header ref here instead of the real header
// as this mock header is aligned with CSS grid, so it is more reliable
// the real header is aligned programmatically based on this header
ref={ref => c.setHeaderElementRef(ref)}
></div>
))}
{showColumnSelectorColumn && <div data-column-id="selector" key={"selector"}></div>}
</div>
);
}
50 changes: 38 additions & 12 deletions packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<C extends GridColumn, T extends ObjectItem = ObjectItem> {
CellComponent: CellComponent<C>;
Expand Down Expand Up @@ -151,12 +153,28 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): 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<number[] | undefined>(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 (
<Fragment>
Expand All @@ -165,7 +183,9 @@ const Main = observer(<C extends GridColumn>(props: WidgetProps<C>): ReactElemen
<WidgetContent>
<Grid
aria-multiselectable={selectionEnabled ? selectActionHelper.selectionType === "Multi" : undefined}
style={cssGridStyles}
style={gridRootStyles}
containerRef={gridContainerRef}
isInfinite={isInfinite}
>
<GridHeader
availableColumns={props.availableColumns}
Expand All @@ -182,6 +202,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,10 +213,15 @@ 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}
>
<MockHeader
showCheckboxColumn={selectActionHelper.showCheckboxColumn}
showColumnSelectorColumn={columnsHidable}
visibleColumns={visibleColumns}
updateColumnSizes={setHeaderSizes}
/>
<RowsRenderer
preview={props.preview ?? false}
interactive={basicData.gridInteractive}
Expand Down Expand Up @@ -258,8 +284,8 @@ function gridStyle(columns: GridColumn[], optional: OptionalColumns): CSSPropert
}

return {
gridTemplateColumns: sizes.join(" ")
};
"--mx-grid-template-columns": sizes.join(" ")
} as any;
}

type OptionalColumns = {
Expand Down
Loading