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 c890812e9b..cb58b928bf 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"; } } } + +#{$root} [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/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 758ba241cb..36635bfbe1 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] +### Added + +- We added a "Custom row key" property in the Advanced section to provide stable row identifiers when using view entities, preventing scroll position loss during data refresh. + ## [3.9.0] - 2026-03-23 ### Changed diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 1068ea630f..c807c824fa 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -369,6 +369,13 @@ + + + Custom row key + Stable identifier for rows to maintain scroll position when using view entities. + + + 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/Row.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx index 4d41de7428..ce3809e4be 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx @@ -1,12 +1,12 @@ -import { SelectActionsService } from "@mendix/widget-plugin-grid/main"; import classNames from "classnames"; import { ObjectItem } from "mendix"; import { ReactElement } from "react"; -import { EventsController } from "../typings/CellComponent"; -import { GridColumn } from "../typings/GridColumn"; +import { SelectActionsService } from "@mendix/widget-plugin-grid/main"; import { CheckboxCell } from "./CheckboxCell"; import { DataCell } from "./DataCell"; import { SelectorCell } from "./SelectorCell"; +import { EventsController } from "../typings/CellComponent"; +import { GridColumn } from "../typings/GridColumn"; export interface RowProps { className?: string; @@ -45,7 +45,7 @@ export function Row(props: RowProps): ReactElement { {props.columns.map((column, baseIndex) => { return ( 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/helpers/state/ColumnGroupStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts index e8cf112bd2..66d4ebc49c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts @@ -1,3 +1,4 @@ +import { action, autorun, comparer, computed, makeObservable, observable } from "mobx"; import { reduceArray, restoreArray } from "@mendix/filter-commons/condition-utils"; import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings"; import { ConditionWithMeta } from "@mendix/widget-plugin-filtering/typings/ConditionWithMeta"; @@ -6,20 +7,19 @@ import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { DerivedPropsGate, SetupComponent, SetupComponentHost } from "@mendix/widget-plugin-mobx-kit/main"; -import { action, autorun, computed, makeObservable, observable } from "mobx"; -import { DatagridContainerProps } from "../../../typings/DatagridProps"; -import { ColumnId, GridColumn } from "../../typings/GridColumn"; -import { ColumnFilterSettings, ColumnPersonalizationSettings } from "../../typings/personalization-settings"; -import { SortInstruction } from "../../typings/sorting"; -import { StaticInfo } from "../../typings/static-info"; +import { ColumnFilterStore } from "./column/ColumnFilterStore"; +import { ColumnStore } from "./column/ColumnStore"; import { ColumnsSortingStore, IColumnSortingStore, sortInstructionsToSortRules, sortRulesToSortInstructions } from "./ColumnsSortingStore"; -import { ColumnFilterStore } from "./column/ColumnFilterStore"; -import { ColumnStore } from "./column/ColumnStore"; +import { DatagridContainerProps } from "../../../typings/DatagridProps"; +import { ColumnId, GridColumn } from "../../typings/GridColumn"; +import { ColumnFilterSettings, ColumnPersonalizationSettings } from "../../typings/personalization-settings"; +import { SortInstruction } from "../../typings/sorting"; +import { StaticInfo } from "../../typings/static-info"; export interface IColumnGroupStore { loaded: boolean; @@ -93,7 +93,7 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore, swapColumns: action, setColumnSettings: action, hydrate: action, - sortInstructions: computed({ keepAlive: true }) + sortInstructions: computed({ keepAlive: true, equals: comparer.structural }) }); } diff --git a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts index c03bfc086f..7b9893c7f7 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -1,3 +1,4 @@ +import { Container, injected } from "brandi"; import { WidgetFilterAPI } from "@mendix/widget-plugin-filtering/context"; import { CombinedFilter } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter"; import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; @@ -26,7 +27,6 @@ import { import { SelectionCounterViewModel } from "@mendix/widget-plugin-grid/selection-counter/SelectionCounter.viewModel-atoms"; import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; -import { Container, injected } from "brandi"; import { MainGateProps } from "../../../typings/MainGateProps"; import { WidgetRootViewModel } from "../../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../../features/empty-message/EmptyPlaceholder.viewModel"; @@ -39,7 +39,7 @@ import { GridBasicData } from "../../helpers/state/GridBasicData"; import { GridPersonalizationStore } from "../../helpers/state/GridPersonalizationStore"; import { DatagridConfig } from "../configs/Datagrid.config"; import { gridStyleAtom } from "../models/grid.model"; -import { rowClassProvider } from "../models/rows.model"; +import { rowClassProvider, rowKeyProvider } from "../models/rows.model"; import { DatasourceParamsController } from "../services/DatasourceParamsController"; import { DerivedLoaderController } from "../services/DerivedLoaderController"; import { SelectionGate } from "../services/SelectionGate.service"; @@ -232,6 +232,7 @@ const _07_selectionBindings: BindingGroup = { injected(createSelectionHelper, CORE.setupService, DG.selectionGate, CORE.config.optional); injected(gridStyleAtom, CORE.columnsStore, CORE.config, DG.gridSizeStore); injected(rowClassProvider, CORE.mainGate); + injected(rowKeyProvider, CORE.mainGate); injected( SelectionCounterViewModel, CORE.selection.selectedCount, @@ -244,6 +245,7 @@ const _07_selectionBindings: BindingGroup = { container.bind(DG.selectionHelper).toInstance(createSelectionHelper).inSingletonScope(); container.bind(DG.gridColumnsStyle).toInstance(gridStyleAtom).inTransientScope(); container.bind(DG.rowClass).toInstance(rowClassProvider).inTransientScope(); + container.bind(DG.rowKey).toInstance(rowKeyProvider).inTransientScope(); container.bind(DG.selectionCounterVM).toInstance(SelectionCounterViewModel).inSingletonScope(); }, init(container, { config, props }) { diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts index 2d3cca8eba..277dc9fc29 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -18,6 +18,7 @@ export const [useItemCount] = createInjectionHooks(CORE.atoms.itemCount); export const [useColumn] = createInjectionHooks(CORE.column); export const [useTexts] = createInjectionHooks(CORE.texts); export const [useRowClass] = createInjectionHooks(DG.rowClass); +export const [useRowKey] = createInjectionHooks(DG.rowKey); export const [useDatagridRootVM] = createInjectionHooks(DG.datagridRootVM); export const [useRows] = createInjectionHooks(CORE.rows); export const [useSelectActions] = createInjectionHooks(DG.selectActions); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts deleted file mode 100644 index eb98f0e965..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useBodyScroll.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { UIEventHandler } from "react"; -import { useInfiniteControl } from "./useInfiniteControl"; - -export function useBodyScroll(): { - handleScroll: UIEventHandler | undefined; -} { - const [trackBodyScrolling] = useInfiniteControl(); - - return { - handleScroll: trackBodyScrolling - }; -} diff --git a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx index 50da655ed0..c7165b417c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/useInfiniteControl.tsx @@ -3,29 +3,29 @@ import { useOnScreen } from "@mendix/widget-plugin-hooks/useOnScreen"; import { useGridSizeStore } from "@mendix/datagrid-web/src/model/hooks/injection-hooks"; import { VIRTUAL_SCROLLING_OFFSET } from "../stores/GridSize.store"; -export function useInfiniteControl(): [trackBodyScrolling: ((e: any) => void) | undefined] { +export function useInfiniteControl(): [trackTableScrolling: ((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/__tests__/rows.model.spec.ts b/packages/pluggableWidgets/datagrid-web/src/model/models/__tests__/rows.model.spec.ts index d469574ee5..b3760056bf 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/models/__tests__/rows.model.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/models/__tests__/rows.model.spec.ts @@ -1,7 +1,7 @@ import { listExpression, obj } from "@mendix/widget-plugin-test-utils"; import { configure, isObservable, observable } from "mobx"; import { MainGateProps } from "../../../../typings/MainGateProps"; -import { rowClassProvider } from "../rows.model"; +import { rowClassProvider, rowKeyProvider } from "../rows.model"; describe("rowClassProvider", () => { configure({ @@ -52,3 +52,65 @@ describe("rowClassProvider", () => { expect(isObservable(atom.class)).toBe(false); }); }); + +describe("rowKeyProvider", () => { + configure({ + enforceActions: "never" + }); + + it("returns item.id when customRowKey is not defined", () => { + const gate = observable({ props: {} as MainGateProps }); + const atom = rowKeyProvider(gate); + const item = obj("fixed-id"); + + expect(atom.key.get(item)).toBe(item.id); + }); + + it("returns item.id when customRowKey value is loading (undefined)", () => { + const gate = observable({ + props: { + customRowKey: listExpression(() => undefined as unknown as string) + } as MainGateProps + }); + const atom = rowKeyProvider(gate); + const item = obj("fixed-id"); + + expect(atom.key.get(item)).toBe(item.id); + }); + + it("returns the custom key when customRowKey has a value", () => { + const gate = observable({ + props: { + customRowKey: listExpression(() => "stable-key-123") + } as MainGateProps + }); + const atom = rowKeyProvider(gate); + + expect(atom.key.get(obj())).toBe("stable-key-123"); + }); + + it("updates reactively when customRowKey expression changes", () => { + const gate = observable({ + props: { + customRowKey: listExpression(() => "initial-key") + } as MainGateProps + }); + const atom = rowKeyProvider(gate); + + expect(atom.key.get(obj())).toBe("initial-key"); + + gate.props.customRowKey = listExpression(() => "updated-key"); + expect(atom.key.get(obj())).toBe("updated-key"); + }); + + it("key property is not observable itself", () => { + const gate = observable({ + props: { + customRowKey: listExpression(() => "some-key") + } as MainGateProps + }); + + const atom = rowKeyProvider(gate); + expect(isObservable(atom.key)).toBe(false); + }); +}); 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/models/rows.model.ts b/packages/pluggableWidgets/datagrid-web/src/model/models/rows.model.ts index 2c11fc535c..9e1c8cf796 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/models/rows.model.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/models/rows.model.ts @@ -1,6 +1,6 @@ -import { ComputedAtom, DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; import { ObjectItem } from "mendix"; import { computed, observable } from "mobx"; +import { ComputedAtom, DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; import { MainGateProps } from "../../../typings/MainGateProps"; export interface RowClassProvider { @@ -9,6 +9,12 @@ export interface RowClassProvider { }; } +export interface RowKeyProvider { + key: { + get(item: ObjectItem): string; + }; +} + /** @injectable */ export function rowClassProvider(gate: DerivedPropsGate): RowClassProvider { const atom = { @@ -25,6 +31,24 @@ export function rowClassProvider(gate: DerivedPropsGate): RowClas return observable(atom, { class: computed }); } +/** @injectable */ +export function rowKeyProvider(gate: DerivedPropsGate): RowKeyProvider { + const atom = { + get key() { + return { + get(item: ObjectItem): string { + if (!gate.props.customRowKey) return item.id; + const v = gate.props.customRowKey.get(item); + const customKey = v.value; + return customKey ?? item.id; + } + }; + } + }; + + return observable(atom, { key: computed }); +} + /** @injectable */ export function rowsAtom(gate: DerivedPropsGate): ComputedAtom { return computed(() => { 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/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index cfe55f1015..e2036b7388 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -1,3 +1,6 @@ +import { token } from "brandi"; +import { ListValue, ObjectItem } from "mendix"; +import { CSSProperties, ReactNode } from "react"; import { FilterAPI } from "@mendix/widget-plugin-filtering/context"; import { CombinedFilter, CombinedFilterConfig } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter"; import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; @@ -27,9 +30,6 @@ import { } from "@mendix/widget-plugin-grid/select-all/select-all.model"; import { SelectionCounterViewModel } from "@mendix/widget-plugin-grid/selection-counter/SelectionCounter.viewModel-atoms"; import { ComputedAtom, DerivedPropsGate, Emitter } from "@mendix/widget-plugin-mobx-kit/main"; -import { token } from "brandi"; -import { ListValue, ObjectItem } from "mendix"; -import { CSSProperties, ReactNode } from "react"; import { MainGateProps } from "../../typings/MainGateProps"; import { WidgetRootViewModel } from "../features/base/WidgetRoot.viewModel"; import { EmptyPlaceholderViewModel } from "../features/empty-message/EmptyPlaceholder.viewModel"; @@ -44,7 +44,7 @@ import { GridPersonalizationStore } from "../helpers/state/GridPersonalizationSt import { DatasourceParamsController } from "../model/services/DatasourceParamsController"; import { GridColumn } from "../typings/GridColumn"; import { DatagridConfig } from "./configs/Datagrid.config"; -import { RowClassProvider } from "./models/rows.model"; +import { RowClassProvider, RowKeyProvider } from "./models/rows.model"; import { DatagridSetupService } from "./services/DatagridSetup.service"; import { DerivedLoaderController, DerivedLoaderControllerConfig } from "./services/DerivedLoaderController"; import { TextsService } from "./services/Texts.service"; @@ -141,6 +141,7 @@ export const DG_TOKENS = { gridColumnsStyle: token>("@computed:GridColumnsStyle"), rowClass: token("@store:RowClassProvider"), + rowKey: token("@store:RowKeyProvider"), datagridRootVM: token("WidgetRootViewModel"), diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 6c60d30bb4..c1da56eccf 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -131,6 +131,7 @@ export interface DatagridContainerProps { showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; rowClass?: ListExpressionValue; + customRowKey?: ListExpressionValue; columnsSortable: boolean; columnsResizable: boolean; columnsDraggable: boolean; @@ -198,6 +199,7 @@ export interface DatagridPreviewProps { showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; rowClass: string; + customRowKey: string; columnsSortable: boolean; columnsResizable: boolean; columnsDraggable: boolean; diff --git a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts index 0f907db9a7..57b40fe726 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts @@ -11,6 +11,7 @@ export type MainGateProps = Pick< | "columns" | "configurationAttribute" | "configurationStorageType" + | "customRowKey" | "datasource" | "dynamicPage" | "dynamicPageSize" diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md index c56aa380bd..a00a990284 100644 --- a/packages/pluggableWidgets/gallery-web/CHANGELOG.md +++ b/packages/pluggableWidgets/gallery-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added a "Custom item key" property in the Advanced section to provide stable item identifiers when using view entities, preventing scroll position loss during data refresh. + ## [3.9.0] - 2026-03-23 ### Fixed diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index 62abcb34b8..4dd0ef8322 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -1,7 +1,5 @@ - - + + Gallery Data containers @@ -87,8 +85,7 @@ Keep selection If enabled, selected items will stay selected unless cleared by the user or a Nanoflow. - + Show selection count @@ -99,8 +96,7 @@ - + Loading type @@ -212,6 +208,13 @@ + + + Custom item key + Stable identifier for items to maintain scroll position when using view entities. + + + @@ -223,8 +226,7 @@ Browser local storage - + Attribute Attribute containing the personalized configuration of the capabilities. This configuration is automatically stored and loaded. The attribute requires Unlimited String. @@ -259,8 +261,7 @@ Content description Assistive technology will read this upon reaching gallery. - + Item description Assistive technology will read this upon reaching each gallery item. @@ -293,4 +294,4 @@ - \ No newline at end of file + diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx index b430f9477c..6c6da947df 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx @@ -1,6 +1,12 @@ import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context"; import { observer } from "mobx-react-lite"; -import { useGalleryConfig, useItems, useKeyNavFocus, useTextsService } from "../model/hooks/injection-hooks"; +import { + useGalleryConfig, + useItemKey, + useItems, + useKeyNavFocus, + useTextsService +} from "../model/hooks/injection-hooks"; import { ListBox } from "./ListBox"; import { ListItem } from "./ListItem"; @@ -9,6 +15,7 @@ export const GalleryItems = observer(function GalleryItems() { const config = useGalleryConfig(); const texts = useTextsService(); const focusController = useKeyNavFocus(); + const itemKey = useItemKey(); if (items.length < 1) { return null; @@ -24,7 +31,7 @@ export const GalleryItems = observer(function GalleryItems() { > {items.map((item, index) => ( - + ))} diff --git a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts index 432930099c..fc0a24506c 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/containers/Gallery.container.ts @@ -35,7 +35,7 @@ import { GalleryRootViewModel } from "../../view-models/GalleryRoot.viewModel"; import { GalleryConfig } from "../configs/Gallery.config"; import { galleryPaginationConfig } from "../configs/GalleryPagination.config"; import { settingsConfig } from "../configs/GallerySettings.config"; -import { itemsAtom } from "../models/items.model"; +import { itemsAtom, itemKeyProvider } from "../models/items.model"; import { layoutAtom, numberOfColumnsAtom } from "../models/layout.model"; import { LayoutService } from "../services/Layout.service"; import { LoaderService } from "../services/Loader.service"; @@ -65,12 +65,14 @@ interface BindingGroup { const _01_coreBindings: BindingGroup = { inject() { injected(itemsAtom, CORE.mainGate); + injected(itemKeyProvider, CORE.mainGate); injected(TextsService, CORE.mainGate); }, init(container, { mainGate, config }) { container.bind(CORE.mainGate).toConstant(mainGate); container.bind(CORE.config).toConstant(config); container.bind(CORE.items).toInstance(itemsAtom).inTransientScope(); + container.bind(GY.itemKey).toInstance(itemKeyProvider).inTransientScope(); container.bind(CORE.texts).toInstance(TextsService).inTransientScope(); } }; diff --git a/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts b/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts index fbb866c3b5..2fc2a79492 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/hooks/injection-hooks.ts @@ -23,6 +23,7 @@ export const [useTextsService] = createInjectionHooks(CORE.texts); export const [useKeyNavFocus] = createInjectionHooks(GY.keyNavFocusService); export const [useItemEventsVM] = createInjectionHooks(GY.itemEventsVM); +export const [useItemKey] = createInjectionHooks(GY.itemKey); export const [useLayoutService] = createInjectionHooks(GY.layoutService); export const [useLoaderViewModel] = createInjectionHooks(GY.loader); diff --git a/packages/pluggableWidgets/gallery-web/src/model/models/__tests__/items.model.spec.ts b/packages/pluggableWidgets/gallery-web/src/model/models/__tests__/items.model.spec.ts new file mode 100644 index 0000000000..a69ca66b1c --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/model/models/__tests__/items.model.spec.ts @@ -0,0 +1,66 @@ +import { listExpression, obj } from "@mendix/widget-plugin-test-utils"; +import { configure, isObservable, observable } from "mobx"; +import { GalleryGateProps } from "../../../typings/GalleryGateProps"; +import { itemKeyProvider } from "../items.model"; + +describe("itemKeyProvider", () => { + configure({ + enforceActions: "never" + }); + + it("returns item.id when customItemKey is not defined", () => { + const gate = observable({ props: {} as GalleryGateProps }); + const atom = itemKeyProvider(gate); + const item = obj("fixed-id"); + + expect(atom.key.get(item)).toBe(item.id); + }); + + it("returns item.id when customItemKey value is loading (undefined)", () => { + const gate = observable({ + props: { + customItemKey: listExpression(() => undefined as unknown as string) + } as GalleryGateProps + }); + const atom = itemKeyProvider(gate); + const item = obj("fixed-id"); + + expect(atom.key.get(item)).toBe(item.id); + }); + + it("returns the custom key when customItemKey has a value", () => { + const gate = observable({ + props: { + customItemKey: listExpression(() => "stable-key-123") + } as GalleryGateProps + }); + const atom = itemKeyProvider(gate); + + expect(atom.key.get(obj())).toBe("stable-key-123"); + }); + + it("updates reactively when customItemKey expression changes", () => { + const gate = observable({ + props: { + customItemKey: listExpression(() => "initial-key") + } as GalleryGateProps + }); + const atom = itemKeyProvider(gate); + + expect(atom.key.get(obj())).toBe("initial-key"); + + gate.props.customItemKey = listExpression(() => "updated-key"); + expect(atom.key.get(obj())).toBe("updated-key"); + }); + + it("key property is not observable itself", () => { + const gate = observable({ + props: { + customItemKey: listExpression(() => "some-key") + } as GalleryGateProps + }); + + const atom = itemKeyProvider(gate); + expect(isObservable(atom.key)).toBe(false); + }); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/model/models/items.model.ts b/packages/pluggableWidgets/gallery-web/src/model/models/items.model.ts index f25fbebbf0..41ff1d8395 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/models/items.model.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/models/items.model.ts @@ -1,6 +1,6 @@ import { ComputedAtom, DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; import { ObjectItem } from "mendix"; -import { computed } from "mobx"; +import { computed, observable } from "mobx"; import { GalleryGateProps } from "../../typings/GalleryGateProps"; /** @injectable */ @@ -9,3 +9,27 @@ export function itemsAtom(gate: DerivedPropsGate): ComputedAto return gate.props.datasource?.items ?? []; }); } + +export interface ItemKeyProvider { + key: { + get(item: ObjectItem): string; + }; +} + +/** @injectable */ +export function itemKeyProvider(gate: DerivedPropsGate): ItemKeyProvider { + const atom = { + get key() { + return { + get(item: ObjectItem): string { + if (!gate.props.customItemKey) return item.id; + const v = gate.props.customItemKey.get(item); + const customKey = v.value; + return customKey ?? item.id; + } + }; + } + }; + + return observable(atom, { key: computed }); +} diff --git a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts index d5dcddd98b..b7fc9e0ab6 100644 --- a/packages/pluggableWidgets/gallery-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/gallery-web/src/model/tokens.ts @@ -41,6 +41,7 @@ import { LayoutService } from "./services/Layout.service"; import { LoaderService } from "./services/Loader.service"; import { QueryParamsService } from "./services/QueryParams.service"; import { TextsService } from "./services/Texts.service"; +import { ItemKeyProvider } from "./models/items.model"; const label = (name: string): string => `Gallery[${name}]`; @@ -117,6 +118,7 @@ export const GY_TOKENS = { // keyboard navigation, layout and click clickActionHelper: token(label("@service:clickActionHelper")), itemEventsVM: token>(label("@service:itemEventsVM")), + itemKey: token(label("@store:itemKeyProvider")), keyNavFocusService: token(label("@service:keyNavFocusService")), layoutService: token(label("@service:layoutService")), numberOfColumns: token>(label("@computed:numberOfColumns")), diff --git a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts index 7f0917f4ac..69156ac481 100644 --- a/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts +++ b/packages/pluggableWidgets/gallery-web/src/typings/GalleryGateProps.ts @@ -25,6 +25,7 @@ export type GalleryGateProps = Pick< | "phoneItems" | "ariaLabelListBox" | "itemClass" + | "customItemKey" | "onClick" | "onClickTrigger" | "content" diff --git a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts index 5d85fcaac4..1fc30d2f72 100644 --- a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts +++ b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts @@ -62,6 +62,7 @@ export interface GalleryContainerProps { showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; itemClass?: ListExpressionValue; + customItemKey?: ListExpressionValue; stateStorageType: StateStorageTypeEnum; stateStorageAttr?: EditableValue; storeFilters: boolean; @@ -118,6 +119,7 @@ export interface GalleryPreviewProps { showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; itemClass: string; + customItemKey: string; stateStorageType: StateStorageTypeEnum; stateStorageAttr: string; onConfigurationChange: {} | null; 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`] = `