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 bbc77135b4..d817a34753 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -346,8 +346,13 @@ $root: ".widget-datagrid"; position: relative; &-grid { + &-head, + &-body { + display: contents; + } &.table { display: grid !important; + grid-template-columns: var(--widgets-grid-template-columns); min-width: fit-content; margin-bottom: 0; &.infinite-loading { @@ -487,106 +492,53 @@ $root: ".widget-datagrid"; } .infinite-loading { - .widget-datagrid-grid-head { - // lock header width - // and prevent it from having own scrolling - // as scrolling is synchronized in JS - width: calc(var(--widgets-grid-width) - var(--widgets-grid-scrollbar-size)); - overflow-x: hidden; - } + // infinite loading grid limits height to initiate scrolling + max-height: var(--widgets-grid-table-height); + overflow: auto; .widget-datagrid-grid-head { - &[data-scrolled-x="true"], - &[data-scrolled-y="true"] { - z-index: 1; + .th { + // make header sticky when grid is scrolled. + position: sticky; + top: 0; + z-index: 10; + &:has([data-overlay-content]) { + // when there is popup open inside of the header cell, it should + // overlay other header elements, including ones from other grids. + z-index: 20; + } } } - .widget-datagrid-grid-head[data-scrolled-y="true"] { + &[data-scrolled-y] .widget-datagrid-grid-head .th:after { // add shadow under the header // implying that grid is scrolled vertically (there are rows hidden under header) - // the data attribute added in JS - box-shadow: 0 5px 5px -5px gray; - } - - .widget-datagrid-grid-body { - // lock the size of the body - // and enable it to have own scrolling - // body is the leading element - // header scroll will be synced to match it - width: var(--widgets-grid-width); - overflow-y: auto; - max-height: var(--widgets-grid-body-height); - } - - .widget-datagrid-grid-head[data-scrolled-x="true"]:after { - // add inner shadow to the left side of the grid - // implying that the grid is scrolled horizontally (there are rows hidden on the left) - // the data attribute added in JS content: ""; position: absolute; + bottom: -5px; left: 0; - width: 10px; - box-shadow: inset 5px 0 5px -5px gray; - top: 0; - bottom: 0; + right: 0; + height: 5px; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), transparent); } } -// styles for browsers that don't support subgrid -// if the browser doesn't support subgrid -// fall back header an body the contents and apply grid template to the table -.widget-datagrid-grid.table { - grid-template-columns: var(--widgets-grid-template-columns); -} -.widget-datagrid-grid-body, -.widget-datagrid-grid-head { - display: contents; -} - -// styles for modern browsers -@supports (grid-template-rows: subgrid) { - .widget-datagrid-grid.table:not([data-has-scroll-x="true"]) { - grid-template-columns: var(--widgets-grid-template-columns); - .widget-datagrid-grid-body, - .widget-datagrid-grid-head { - display: grid; - min-width: 0; - - // this property makes sure we align our own grid columns - // to the columns defined in the global grid - grid-template-columns: subgrid; - - // ensure that we cover all columns of original top level grid - // so our own columns get aligned with the parent - grid-column: 1 / -1; - } - } - - .widget-datagrid-grid.table[data-has-scroll-x="true"] { - // reset the columns defined on table level - // header and body will define their own instead - // this is needed to make body horizontally scrollable - grid-template-columns: initial; - .widget-datagrid-grid-head { - display: grid; - min-width: 0; - - grid-template-columns: var(--widgets-grid-template-columns-head, var(--widgets-grid-template-columns)); - } - - .widget-datagrid-grid-body { - display: grid; - - grid-template-columns: var(--widgets-grid-template-columns); - } +// add shadows at the left to imply that grid is scrolled horizontally +.widget-datagrid-content:has(.widget-datagrid-grid[data-scrolled-x]) { + position: relative; // needed to keep :after element positioned inside + &:after { + content: ""; + position: absolute; + left: 0; + z-index: 10; + bottom: 0; + top: 0; + width: 5px; + background: linear-gradient(to right, rgba(0, 0, 0, 0.1), transparent); + pointer-events: none; } } -.grid-mock-header { - display: contents; -} - :where(#{$root}-paging-bottom, #{$root}-paging-top) { display: flex; flex-flow: row nowrap; @@ -681,3 +633,7 @@ $root: ".widget-datagrid"; } } } + +[data-overlay-content] { + z-index: 15; +} diff --git a/packages/pluggableWidgets/combobox-web/src/components/ComboboxMenuWrapper.tsx b/packages/pluggableWidgets/combobox-web/src/components/ComboboxMenuWrapper.tsx index cda1155f9a..51f414c827 100644 --- a/packages/pluggableWidgets/combobox-web/src/components/ComboboxMenuWrapper.tsx +++ b/packages/pluggableWidgets/combobox-web/src/components/ComboboxMenuWrapper.tsx @@ -61,6 +61,7 @@ export function ComboboxMenuWrapper(props: ComboboxMenuWrapperProps): ReactEleme } : style } + data-overlay-content={isOpen || undefined} > {menuHeaderContent && (
(props.expanded ?
{children}
: null)} allowSameDay={false} ariaLabelledBy={`${props.id}-label`} autoFocus={false} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnSelector.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnSelector.tsx index caac97e39c..9ec931e44b 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnSelector.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnSelector.tsx @@ -125,6 +125,7 @@ export function ColumnSelector(props: ColumnSelectorProps): ReactElement { id={`${props.id}-column-selectors`} className={`column-selectors`} data-focusindex={0} + data-overlay-content role="menu" style={{ ...correctedFloatingStyles, maxHeight }} {...getFloatingProps()} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx index 9c3135bc33..b95527665d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx @@ -2,10 +2,12 @@ import classNames from "classnames"; import { observer } from "mobx-react-lite"; import { PropsWithChildren, ReactElement } from "react"; import { useDatagridConfig, useGridSizeStore, useGridStyle } from "../model/hooks/injection-hooks"; +import { useInfiniteControl } from "../model/hooks/useInfiniteControl"; export const Grid = observer(function Grid(props: PropsWithChildren): ReactElement { const config = useDatagridConfig(); const gridSizeStore = useGridSizeStore(); + const [handleScroll] = useInfiniteControl(); const style = useGridStyle().get(); return ( @@ -17,6 +19,7 @@ export const Grid = observer(function Grid(props: PropsWithChildren): ReactEleme role="grid" style={style} ref={gridSizeStore.gridContainerRef} + onScroll={handleScroll} > {props.children}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index 6c27f43ca2..e4280c7a3a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -8,22 +8,15 @@ import { usePaginationVM, useVisibleColumnsCount } from "../model/hooks/injection-hooks"; -import { useBodyScroll } from "../model/hooks/useBodyScroll"; import { RowSkeletonLoader } from "./loader/RowSkeletonLoader"; import { SpinnerLoader } from "./loader/SpinnerLoader"; export const GridBody = observer(function GridBody(props: PropsWithChildren): ReactElement { const { children } = props; const gridSizeStore = useGridSizeStore(); - const { handleScroll } = useBodyScroll(); return ( -
+
{children}
); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx index 1c396b3b75..e2265daa1a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx @@ -64,6 +64,7 @@ export function Header(props: HeaderProps): ReactElement { onDrop={draggableProps.onDrop} onDragEnter={draggableProps.onDragEnter} onDragOver={draggableProps.onDragOver} + ref={ref => column.setHeaderElementRef(ref)} >
( - entries => { - const container = entries[0].target.parentElement!; - - const gridContainer = container.closest(".table"); - const gridBody = container.closest(".table-content"); - if (gridContainer && gridBody) { - if (gridContainer.dataset.hasScrollX === "true") { - if (gridBody.scrollWidth <= gridBody.clientWidth) { - delete gridContainer.dataset.hasScrollX; - } - } else { - if (gridContainer.scrollWidth > gridContainer.clientWidth) { - gridContainer.dataset.hasScrollX = "true"; - } - } - } - - const sizes = new Map(); - 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.getBoundingClientRect().width); - }); - gridSizeStore.updateColumnSizes(sizes.values().toArray()); - }, - [gridSizeStore] - ); - - useEffect(() => { - const observer = new ResizeObserver(resizeCallback); - - columnsStore.visibleColumns.forEach(c => { - if (c.headerElementRef) observer.observe(c.headerElementRef); - }); - - return () => { - observer.disconnect(); - }; - }, [resizeCallback, columnsStore.visibleColumns]); - - return ( -
- {config.checkboxColumnEnabled &&
} - {columnsStore.visibleColumns.map(c => ( -
c.setHeaderElementRef(ref)} - >
- ))} - {config.selectorColumnEnabled &&
} -
- ); -} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 8a3809b2b6..bce68e5755 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -13,7 +13,6 @@ import { WidgetFooter } from "./WidgetFooter"; import { WidgetHeader } from "./WidgetHeader"; import { WidgetRoot } from "./WidgetRoot"; import { WidgetTopBar } from "./WidgetTopBar"; -import { MockHeader } from "./MockHeader"; export function Widget(props: { onExportCancel?: () => void }): ReactElement { return ( @@ -27,7 +26,6 @@ export function Widget(props: { onExportCancel?: () => void }): ReactElement { - diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx index fd42081e81..d95e0206df 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx @@ -1,9 +1,12 @@ import { render } from "@testing-library/react"; import { ContainerProvider } from "brandi-react"; +import { setupIntersectionObserverStub } from "@mendix/widget-plugin-test-utils"; import { createDatagridContainer } from "../../model/containers/createDatagridContainer"; import { mockContainerProps } from "../../utils/test-utils"; import { Grid } from "../Grid"; +setupIntersectionObserverStub(); + describe("Grid", () => { it("renders without crashing", () => { const props = mockContainerProps(); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx index 50da655ed0..e1bed0d181 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx @@ -6,26 +6,26 @@ import { VIRTUAL_SCROLLING_OFFSET } from "../stores/GridSize.store"; export function useInfiniteControl(): [trackBodyScrolling: ((e: any) => void) | undefined] { const gridSizeStore = useGridSizeStore(); - const isVisible = useOnScreen(gridSizeStore.gridBodyRef as RefObject); + const isVisible = useOnScreen(gridSizeStore.gridContainerRef as RefObject); - const trackBodyScrolling = useCallback( + const trackTableScrolling = useCallback( (e: UIEvent) => { const target = e.target as HTMLElement; - const head = gridSizeStore.gridHeaderRef.current; - if (head) { - // synchronize header position to the body as they are decoupled - // we don't use state to optimize speed as we - // don't want a re-render. - head.scrollTo({ left: target.scrollLeft }); - + const container = gridSizeStore.gridContainerRef.current; + if (container) { // this is cosmetic, needed to provide nice shadows when body is scrolled - head.dataset.scrolledY = target.scrollTop > 0 ? "true" : "false"; - head.dataset.scrolledX = target.scrollLeft > 0 ? "true" : "false"; + if (target.scrollTop > 0) { + container.dataset.scrolledY = "true"; + } else { + delete container.dataset.scrolledY; + } + if (target.scrollLeft > 0) { + container.dataset.scrolledX = "true"; + } else { + delete container.dataset.scrolledX; + } } - // we need to determine scrollbar width to calculate header size correctly in css - gridSizeStore.setScrollBarSize(target.offsetWidth - target.clientWidth); - /** * 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, @@ -42,26 +42,9 @@ export function useInfiniteControl(): [trackBodyScrolling: ((e: any) => void) | ); useEffect(() => { - const timer = setTimeout(() => isVisible && gridSizeStore.lockGridBodyHeight(), 100); + const timer = setTimeout(() => isVisible && gridSizeStore.lockGridContainerHeight(), 100); return () => clearTimeout(timer); }); - useEffect(() => { - const observeTarget = gridSizeStore.gridContainerRef.current; - if (!gridSizeStore.hasVirtualScrolling || !observeTarget) return; - - const resizeObserver = new ResizeObserver(entries => { - for (const entry of entries) { - gridSizeStore.setGridWidth(entry.contentRect.width); - } - }); - - resizeObserver.observe(observeTarget); - - return () => { - resizeObserver.unobserve(observeTarget); - }; - }, [gridSizeStore]); - - return [gridSizeStore.hasVirtualScrolling ? trackBodyScrolling : undefined]; + return [gridSizeStore.hasVirtualScrolling ? trackTableScrolling : undefined]; } diff --git a/packages/pluggableWidgets/datagrid-web/src/model/models/grid.model.ts b/packages/pluggableWidgets/datagrid-web/src/model/models/grid.model.ts index e8c99f06a3..90144c34f6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/models/grid.model.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/models/grid.model.ts @@ -18,10 +18,7 @@ export function gridStyleAtom( checkboxColumn: config.checkboxColumnEnabled, selectorColumn: config.selectorColumnEnabled }), - "--widgets-grid-template-columns-head": gridSizeStore.templateColumnsHead, - "--widgets-grid-body-height": asPx(gridSizeStore.gridBodyHeight), - "--widgets-grid-width": asPx(gridSizeStore.gridWidth), - "--widgets-grid-scrollbar-size": asPx(gridSizeStore.scrollBarSize) + "--widgets-grid-table-height": asPx(gridSizeStore.gridContainerHeight) }) as CSSProperties ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/model/stores/GridSize.store.ts b/packages/pluggableWidgets/datagrid-web/src/model/stores/GridSize.store.ts index b3a11b6688..77c9e08d89 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/stores/GridSize.store.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/stores/GridSize.store.ts @@ -1,6 +1,6 @@ import { SetPageAction } from "@mendix/widget-plugin-grid/pagination/main"; import { ComputedAtom } from "@mendix/widget-plugin-mobx-kit/main"; -import { action, computed, makeAutoObservable, observable } from "mobx"; +import { action, makeAutoObservable, observable } from "mobx"; import { createRef } from "react"; import { PaginationConfig } from "../../features/pagination/pagination.config"; @@ -11,10 +11,7 @@ export class GridSizeStore { gridBodyRef = createRef(); gridHeaderRef = createRef(); - scrollBarSize?: number; - gridWidth?: number; - gridBodyHeight?: number; - columnSizes?: number[]; + gridContainerHeight?: number; private lockedAtPageSize?: number; @@ -30,19 +27,8 @@ export class GridSizeStore { gridHeaderRef: false, lockedAtPageSize: false, - scrollBarSize: observable, - setScrollBarSize: action, - - gridWidth: observable, - setGridWidth: action, - - gridBodyHeight: observable, - lockGridBodyHeight: action, - - columnSizes: observable, - updateColumnSizes: action, - - templateColumnsHead: computed + gridContainerHeight: observable, + lockGridContainerHeight: action }); } @@ -54,34 +40,12 @@ export class GridSizeStore { return this.paginationConfig.pagination === "virtualScrolling"; } - get templateColumnsHead(): string | undefined { - return this.columnSizes - ?.map(size => { - const str = size.toString(); - const dotIndex = str.indexOf("."); - return `${dotIndex === -1 ? str : str.slice(0, dotIndex + 4)}px`; - }) - .join(" "); - } - bumpPage(): void { if (this.hasMoreItems) { return this.setPageAction(page => page + 1); } } - setScrollBarSize(size: number): void { - this.scrollBarSize = size; - } - - setGridWidth(size: number | undefined): void { - this.gridWidth = size; - } - - updateColumnSizes(sizes: number[]): void { - this.columnSizes = sizes; - } - /** * Computes the total viewport height of visible rows based on the current page size. * @returns {number} Total height in pixels of visible rows, or 0 if no rows present. @@ -101,7 +65,17 @@ export class GridSizeStore { return totalHeight; } - lockGridBodyHeight(): void { + computeHeaderViewport(): number { + const firstTh = this.gridHeaderRef.current?.querySelector(".th"); + + if (!firstTh) { + return 0; + } + + return firstTh.offsetHeight; + } + + lockGridContainerHeight(): void { if (!this.hasVirtualScrolling || !this.hasMoreItems) { return; } @@ -109,31 +83,34 @@ export class GridSizeStore { // Reset the locked height when page size changes so layout is recomputed // for the new number of rows (e.g. switching from 10 → 5 rows). const currentPageSize = this.pageSizeAtom.get(); - if (this.gridBodyHeight !== undefined && this.lockedAtPageSize !== currentPageSize) { - this.gridBodyHeight = undefined; + if (this.gridContainerHeight !== undefined && this.lockedAtPageSize !== currentPageSize) { + this.gridContainerHeight = undefined; this.lockedAtPageSize = undefined; } - const gridBody = this.gridBodyRef.current; - if (!gridBody || this.gridBodyHeight !== undefined) { + const gridContainer = this.gridContainerRef.current; + if (!gridContainer || this.gridContainerHeight !== undefined) { return; } - const viewportHeight = this.computeBodyViewport(); + const bodyViewportHeight = this.computeBodyViewport(); + const headerViewportHeight = this.computeHeaderViewport(); // Don't lock height before the grid body has rendered content. // clientHeight is 0 when the element has no layout yet, which would // produce a negative height and break scrolling. - if (viewportHeight <= 0) { + if (bodyViewportHeight <= 0) { return; } + const fullHeight = bodyViewportHeight + headerViewportHeight; + // If content already overflows the container (fixed-height grid), do not subtract the // pre-fetch offset — that would hide the last rows and trigger the next page too early. // Only subtract the offset when the grid does not yet overflow (auto-height grid) so // that we create a small synthetic overflow that makes the body scrollable. - const overflows = gridBody.scrollHeight > viewportHeight; - this.gridBodyHeight = viewportHeight - (overflows ? 0 : VIRTUAL_SCROLLING_OFFSET); + const overflows = gridContainer.scrollHeight > fullHeight; + this.gridContainerHeight = fullHeight - (overflows ? 0 : VIRTUAL_SCROLLING_OFFSET); this.lockedAtPageSize = currentPageSize; } } diff --git a/packages/pluggableWidgets/popup-menu-web/src/__tests__/__snapshots__/Menu.spec.tsx.snap b/packages/pluggableWidgets/popup-menu-web/src/__tests__/__snapshots__/Menu.spec.tsx.snap index d2eb16172f..cecd9fde46 100644 --- a/packages/pluggableWidgets/popup-menu-web/src/__tests__/__snapshots__/Menu.spec.tsx.snap +++ b/packages/pluggableWidgets/popup-menu-web/src/__tests__/__snapshots__/Menu.spec.tsx.snap @@ -17,6 +17,7 @@ exports[`Menu renders menu 1`] = ` diff --git a/packages/pluggableWidgets/tooltip-web/src/components/Tooltip.tsx b/packages/pluggableWidgets/tooltip-web/src/components/Tooltip.tsx index 13637494c6..231885c1c3 100644 --- a/packages/pluggableWidgets/tooltip-web/src/components/Tooltip.tsx +++ b/packages/pluggableWidgets/tooltip-web/src/components/Tooltip.tsx @@ -48,6 +48,7 @@ export const Tooltip = (props: TooltipProps): ReactElement => { ref={refs?.setFloating} style={floatingStyles} {...getFloatingProps?.()} + data-overlay-content > {renderMethod === "text" ? textMessage : htmlMessage}
diff --git a/packages/pluggableWidgets/tooltip-web/src/components/__tests__/__snapshots__/Tooltip.spec.tsx.snap b/packages/pluggableWidgets/tooltip-web/src/components/__tests__/__snapshots__/Tooltip.spec.tsx.snap index 21d786034a..0c48903965 100644 --- a/packages/pluggableWidgets/tooltip-web/src/components/__tests__/__snapshots__/Tooltip.spec.tsx.snap +++ b/packages/pluggableWidgets/tooltip-web/src/components/__tests__/__snapshots__/Tooltip.spec.tsx.snap @@ -19,6 +19,7 @@ exports[`Tooltip render DOM structure 1`] = `