From fc4126f7a92d16bcd7ee7679bc9492a9250daab3 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:57:08 +0200 Subject: [PATCH 1/4] feat(gallery-web): add new pagination settings --- .../gallery-web/src/Gallery.editorConfig.ts | 21 ++++++++++---- .../gallery-web/src/Gallery.editorPreview.tsx | 2 ++ .../gallery-web/src/Gallery.tsx | 17 ++++++----- .../gallery-web/src/Gallery.xml | 29 +++++++++++++------ .../gallery-web/src/components/Gallery.tsx | 16 ++++++---- .../src/components/__tests__/Gallery.spec.tsx | 14 ++++----- .../src/helpers/useGalleryStore.ts | 4 +-- .../gallery-web/src/utils/test-utils.tsx | 10 ++++--- .../gallery-web/typings/GalleryProps.d.ts | 16 +++++----- 9 files changed, 80 insertions(+), 49 deletions(-) diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts index 136ddf7cac..de7475fd0f 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts @@ -10,10 +10,6 @@ import { import { GalleryPreviewProps } from "../typings/GalleryProps"; export function getProperties(values: GalleryPreviewProps, defaultProperties: Properties): Properties { - if (values.pagination !== "buttons") { - hidePropertyIn(defaultProperties, values, "pagingPosition"); - } - if (values.showEmptyPlaceholder === "none") { hidePropertyIn(defaultProperties, values, "emptyPlaceholder"); } @@ -22,8 +18,21 @@ export function getProperties(values: GalleryPreviewProps, defaultProperties: Pr hidePropertiesIn(defaultProperties, values, ["onSelectionChange", "itemSelectionMode"]); } - // Hide scrolling settings for now. - hidePropertiesIn(defaultProperties, values, ["showPagingButtons", "showTotalCount"]); + /** Pagination */ + + if (values.pagination === "buttons") { + hidePropertyIn(defaultProperties, values, "showTotalCount"); + } else { + hidePropertyIn(defaultProperties, values, "showPagingButtons"); + + if (values.showTotalCount === false) { + hidePropertyIn(defaultProperties, values, "pagingPosition"); + } + } + + if (values.pagination !== "loadMore") { + hidePropertyIn(defaultProperties, values, "loadMoreButtonCaption"); + } return defaultProperties; } diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx index dda99fb997..e6471a9e03 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx @@ -95,6 +95,8 @@ function Preview(props: GalleryPreviewProps): ReactElement { pageSize={props.pageSize ?? numberOfItems} paging={props.pagination === "buttons"} paginationPosition={props.pagingPosition} + paginationType={props.pagination} + showPagingButtons={props.showPagingButtons} showEmptyStatePreview={props.showEmptyPlaceholder === "custom"} phoneItems={props.phoneItems!} tabletItems={props.tabletItems!} diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index bd0b6cf89f..396b054b2a 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -1,22 +1,23 @@ -import { observer } from "mobx-react-lite"; import { useOnResetFiltersEvent } from "@mendix/widget-plugin-external-events/hooks"; import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; import { getColumnAndRowBasedOnIndex, useSelectionHelper } from "@mendix/widget-plugin-grid/selection"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { observer } from "mobx-react-lite"; import { ReactElement, ReactNode, createElement, useCallback } from "react"; import { GalleryContainerProps } from "../typings/GalleryProps"; import { Gallery as GalleryComponent } from "./components/Gallery"; +import { HeaderWidgetsHost } from "./components/HeaderWidgetsHost"; import { useItemEventsController } from "./features/item-interaction/ItemEventsController"; import { GridPositionsProps, useGridPositions } from "./features/useGridPositions"; import { useItemHelper } from "./helpers/ItemHelper"; -import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; +import { GalleryContext, GalleryRootScope, useGalleryRootScope } from "./helpers/root-context"; import { useGalleryStore } from "./helpers/useGalleryStore"; -import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; -import { GalleryRootScope, GalleryContext, useGalleryRootScope } from "./helpers/root-context"; -import { HeaderWidgetsHost } from "./components/HeaderWidgetsHost"; +import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; const Container = observer(function GalleryContainer(props: GalleryContainerProps): ReactElement { const { rootStore, itemSelectHelper } = useGalleryRootScope(); + const items = props.datasource.items ?? []; const config: GridPositionsProps = { desktopItems: props.desktopItems, @@ -76,10 +77,12 @@ const Container = observer(function GalleryContainer(props: GalleryContainerProp numberOfItems={props.datasource.totalCount} page={rootStore.paging.currentPage} pageSize={props.pageSize} - paging={props.pagination === "buttons"} + paging={rootStore.paging.showPagination} paginationPosition={props.pagingPosition} - phoneItems={props.phoneItems} + paginationType={props.pagination} setPage={rootStore.paging.setPage} + showPagingButtons={props.showPagingButtons} + phoneItems={props.phoneItems} style={props.style} tabletItems={props.tabletItems} tabIndex={props.tabIndex} diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index 62f0715b3c..2ec8e2118a 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -52,7 +52,7 @@ - + Page size @@ -63,15 +63,12 @@ Paging buttons Virtual scrolling + Load more - - Position of paging buttons + + Show total count - - Below grid - Above grid - Show paging buttons @@ -81,10 +78,24 @@ Auto - - Show total count + + Position of pagination + + Below grid + Above grid + Both + + + + Load more caption + + + Load More + + + Empty message diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index 1d07b4a271..4243e90925 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -5,14 +5,15 @@ import { PositionInGrid, SelectActionHandler } from "@mendix/widget-plugin-grid/ import { ObjectItem } from "mendix"; import { createElement, ReactElement, ReactNode } from "react"; import { GalleryItemHelper } from "../typings/GalleryItem"; -import { ListBox } from "./ListBox"; -import { ListItem } from "./ListItem"; import { GalleryContent } from "./GalleryContent"; import { GalleryFooter } from "./GalleryFooter"; import { GalleryHeader } from "./GalleryHeader"; import { GalleryRoot } from "./GalleryRoot"; import { GalleryTopBar } from "./GalleryTopBar"; +import { ListBox } from "./ListBox"; +import { ListItem } from "./ListItem"; +import { PaginationEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; import { ItemEventsController } from "../typings/ItemEventsController"; export interface GalleryProps { @@ -29,7 +30,9 @@ export interface GalleryProps { paging: boolean; page: number; pageSize: number; - paginationPosition?: "below" | "above"; + paginationPosition?: "top" | "bottom" | "both"; + paginationType: PaginationEnum; + showPagingButtons: ShowPagingButtonsEnum; showEmptyStatePreview?: boolean; phoneItems: number; setPage?: (computePage: (prevPage: number) => number) => void; @@ -60,13 +63,14 @@ export function Gallery(props: GalleryProps): ReactElem page={props.page} pageSize={props.pageSize} previousPage={() => props.setPage && props.setPage(prev => prev - 1)} - pagination={props.paging ? "buttons" : "virtualScrolling"} + pagination={props.paginationType} + showPagingButtons={props.showPagingButtons} /> ) : null; - const showTopBar = props.paging && props.paginationPosition === "above"; - const showFooter = props.paging && props.paginationPosition === "below"; + const showTopBar = props.paging && (props.paginationPosition === "top" || props.paginationPosition === "both"); + const showFooter = props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); return ( { beforeAll(() => { @@ -96,7 +96,7 @@ describe("Gallery", () => { describe("with pagination", () => { it("renders correctly", () => { const { asFragment } = render( - + ); expect(asFragment()).toMatchSnapshot(); @@ -108,7 +108,7 @@ describe("Gallery", () => { new GalleryStore({ gate, ...props, showPagingButtons: "auto", showTotalCount: false }) - ); + const store = useSetup(() => new GalleryStore({ gate, ...props })); return store; } diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx index 4c6690d150..6e4e30e17a 100644 --- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx @@ -1,17 +1,17 @@ -import { createElement } from "react"; import { ClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; -import { SelectActionHandler, getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selection"; +import { PositionController } from "@mendix/widget-plugin-grid/keyboard-navigation/PositionController"; +import { VirtualGridLayout } from "@mendix/widget-plugin-grid/keyboard-navigation/VirtualGridLayout"; +import { getColumnAndRowBasedOnIndex, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; import { listAction, objectItems } from "@mendix/widget-plugin-test-utils"; import { render, RenderResult } from "@testing-library/react"; import userEvent, { UserEvent } from "@testing-library/user-event"; import { ObjectItem } from "mendix"; +import { createElement } from "react"; import { GalleryProps } from "../components/Gallery"; import { ItemEventsController } from "../features/item-interaction/ItemEventsController"; import { ItemHelper } from "../helpers/ItemHelper"; import { ItemHelperBuilder } from "./builders/ItemHelperBuilder"; -import { PositionController } from "@mendix/widget-plugin-grid/keyboard-navigation/PositionController"; -import { VirtualGridLayout } from "@mendix/widget-plugin-grid/keyboard-navigation/VirtualGridLayout"; export function setup(jsx: React.ReactElement): { user: UserEvent } & RenderResult { return { @@ -76,6 +76,8 @@ export function mockProps(params: Helpers & Mocks = {}): GalleryProps; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; itemClass?: ListExpressionValue; @@ -69,9 +70,10 @@ export interface GalleryPreviewProps { phoneItems: number | null; pageSize: number | null; pagination: PaginationEnum; - pagingPosition: PagingPositionEnum; - showPagingButtons: ShowPagingButtonsEnum; showTotalCount: boolean; + showPagingButtons: ShowPagingButtonsEnum; + pagingPosition: PagingPositionEnum; + loadMoreButtonCaption: string; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; itemClass: string; From 9ef25b1d7671fcf1ef7eb015f25879a897227cd4 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:46:57 +0200 Subject: [PATCH 2/4] feat: add "load more" button to gallery --- .../themesource/datawidgets/web/_gallery.scss | 5 ++++ .../gallery-web/src/components/Gallery.tsx | 19 +++++++++++---- .../gallery-web/src/components/LoadMore.tsx | 24 +++++++++++++++++++ .../src/query/DatasourceController.ts | 4 ++++ .../src/query/PaginationController.ts | 4 ++++ .../src/query/query-controller.ts | 4 +++- 6 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss index 743dd90017..94a533499d 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss @@ -88,3 +88,8 @@ $gallery-screen-md: 768px; .widget-gallery-item-button { width: inherit; } + +.widget-gallery-load-more { + display: flex; + justify-content: center; +} diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index 4243e90925..fb711dc56e 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -13,6 +13,7 @@ import { GalleryTopBar } from "./GalleryTopBar"; import { ListBox } from "./ListBox"; import { ListItem } from "./ListItem"; +import { LoadMore, LoadMoreButton as LoadMorePreview } from "src/components/LoadMore"; import { PaginationEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; import { ItemEventsController } from "../typings/ItemEventsController"; @@ -69,8 +70,10 @@ export function Gallery(props: GalleryProps): ReactElem ) : null; - const showTopBar = props.paging && (props.paginationPosition === "top" || props.paginationPosition === "both"); - const showFooter = props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); + const showTopPagination = + props.paging && (props.paginationPosition === "top" || props.paginationPosition === "both"); + const showBottomPagination = + props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); return ( (props: GalleryProps): ReactElem selectable={false} data-focusindex={props.tabIndex || 0} > - {showTopBar && {pagination}} + {showTopPagination && pagination} {props.showHeader && {props.header}} {props.items.length > 0 && ( @@ -115,7 +118,15 @@ export function Gallery(props: GalleryProps): ReactElem
{children}
))} - {showFooter && {pagination}} + + {showBottomPagination && pagination} +
+ {props.preview && props.paginationType === "loadMore" && ( + Load more + )} + {!props.preview && Load more} +
+
); } diff --git a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx new file mode 100644 index 0000000000..c64d6162b7 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx @@ -0,0 +1,24 @@ +import cn from "classnames"; +import { observer } from "mobx-react-lite"; +import { createElement } from "react"; +import { useGalleryRootScope } from "src/helpers/root-context"; + +export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): React.ReactNode { + return ; +} + +export const LoadMore = observer(function LoadMore(props: { children: React.ReactNode }): React.ReactNode { + const { + rootStore: { paging } + } = useGalleryRootScope(); + + if (paging.pagination !== "loadMore") { + return null; + } + + if (!paging.hasMoreItems) { + return null; + } + + return paging.setPage(n => n + 1)}>{props.children}; +}); diff --git a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index fbee1be307..5f5c5b0b1f 100644 --- a/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -90,6 +90,10 @@ export class DatasourceController implements ReactiveController, QueryController return this.datasource.totalCount; } + get hasMoreItems(): boolean { + return this.datasource.hasMoreItems ?? false; + } + /** * Returns computed value that holds controller copy. * Recomputes the copy every time the datasource changes. diff --git a/packages/shared/widget-plugin-grid/src/query/PaginationController.ts b/packages/shared/widget-plugin-grid/src/query/PaginationController.ts index ff68aae944..58fbe1c7e0 100644 --- a/packages/shared/widget-plugin-grid/src/query/PaginationController.ts +++ b/packages/shared/widget-plugin-grid/src/query/PaginationController.ts @@ -67,6 +67,10 @@ export class PaginationController implements ReactiveController { } } + get hasMoreItems(): boolean { + return this._query.hasMoreItems; + } + private _setInitParams(): void { if (this.pagination === "buttons" || this.showTotalCount) { this._query.requestTotalCount(true); diff --git a/packages/shared/widget-plugin-grid/src/query/query-controller.ts b/packages/shared/widget-plugin-grid/src/query/query-controller.ts index a7290cebbd..bc20172a63 100644 --- a/packages/shared/widget-plugin-grid/src/query/query-controller.ts +++ b/packages/shared/widget-plugin-grid/src/query/query-controller.ts @@ -8,11 +8,13 @@ type Members = | "requestTotalCount" | "totalCount" | "limit" - | "offset"; + | "offset" + | "hasMoreItems"; export interface QueryController extends Pick { refresh(): void; setPageSize(size: number): void; + hasMoreItems: boolean; isLoading: boolean; isRefreshing: boolean; isFetchingNextBatch: boolean; From 21376eb0a155486168beeeed8dbfe1657deef626 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:44:09 +0200 Subject: [PATCH 3/4] chore: apply review feedback --- packages/pluggableWidgets/gallery-web/src/Gallery.tsx | 1 + .../pluggableWidgets/gallery-web/src/components/Gallery.tsx | 6 ++++-- .../gallery-web/src/components/LoadMore.tsx | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index 396b054b2a..37e665ed95 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -90,6 +90,7 @@ const Container = observer(function GalleryContainer(props: GalleryContainerProp itemEventsController={itemEventsController} focusController={focusController} getPosition={getPositionCallback} + loadMoreButtonCaption={props.loadMoreButtonCaption?.value} /> ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index fb711dc56e..be5ab3b220 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -50,9 +50,11 @@ export interface GalleryProps { itemHelper: GalleryItemHelper; selectHelper: SelectActionHandler; getPosition: (index: number) => PositionInGrid; + loadMoreButtonCaption?: string; } export function Gallery(props: GalleryProps): ReactElement { + const { loadMoreButtonCaption = "Load more" } = props; const pagination = props.paging ? (
(props: GalleryProps): ReactElem {showBottomPagination && pagination}
{props.preview && props.paginationType === "loadMore" && ( - Load more + {loadMoreButtonCaption} )} - {!props.preview && Load more} + {!props.preview && {loadMoreButtonCaption}}
diff --git a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx index c64d6162b7..150b22cec9 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx @@ -4,7 +4,11 @@ import { createElement } from "react"; import { useGalleryRootScope } from "src/helpers/root-context"; export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): React.ReactNode { - return ; + return ( + + ); } export const LoadMore = observer(function LoadMore(props: { children: React.ReactNode }): React.ReactNode { From 03ce0fa123748083549bd7884f5710d235bf33d7 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:04:23 +0200 Subject: [PATCH 4/4] test: fix unit tests --- .../gallery-web/src/components/Gallery.tsx | 2 +- .../gallery-web/src/components/LoadMore.tsx | 2 +- .../src/components/__tests__/Gallery.spec.tsx | 98 +++++++++++-------- .../__snapshots__/Gallery.spec.tsx.snap | 67 +++++++++++++ .../gallery-web/src/utils/test-utils.tsx | 53 +++++++++- 5 files changed, 176 insertions(+), 46 deletions(-) diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index be5ab3b220..7b01892351 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -13,8 +13,8 @@ import { GalleryTopBar } from "./GalleryTopBar"; import { ListBox } from "./ListBox"; import { ListItem } from "./ListItem"; -import { LoadMore, LoadMoreButton as LoadMorePreview } from "src/components/LoadMore"; import { PaginationEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; +import { LoadMore, LoadMoreButton as LoadMorePreview } from "../components/LoadMore"; import { ItemEventsController } from "../typings/ItemEventsController"; export interface GalleryProps { diff --git a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx index 150b22cec9..3efbf728bc 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx @@ -1,7 +1,7 @@ import cn from "classnames"; import { observer } from "mobx-react-lite"; import { createElement } from "react"; -import { useGalleryRootScope } from "src/helpers/root-context"; +import { useGalleryRootScope } from "../helpers/root-context"; export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): React.ReactNode { return ( diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx index b8f54f49ef..3f6bca60bc 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx @@ -4,7 +4,7 @@ import { render, waitFor } from "@testing-library/react"; import { ObjectItem } from "mendix"; import { createElement } from "react"; import { ItemHelperBuilder } from "../../utils/builders/ItemHelperBuilder"; -import { mockItemHelperWithAction, mockProps, setup } from "../../utils/test-utils"; +import { mockItemHelperWithAction, mockProps, setup, withGalleryContext } from "../../utils/test-utils"; import { Gallery } from "../Gallery"; describe("Gallery", () => { @@ -13,14 +13,14 @@ describe("Gallery", () => { }); describe("DOM Structure", () => { it("renders correctly", () => { - const { asFragment } = render(); + const { asFragment } = render(withGalleryContext()); expect(asFragment()).toMatchSnapshot(); }); it("renders correctly with onclick event", () => { const { asFragment } = render( - + withGalleryContext() ); expect(asFragment()).toMatchSnapshot(); @@ -31,7 +31,7 @@ describe("Gallery", () => { it("runs action on item click", async () => { const execute = jest.fn(); const props = mockProps({ onClick: listAction(mock => ({ ...mock(), execute })) }); - const { user, getAllByRole } = setup(); + const { user, getAllByRole } = setup(withGalleryContext()); const [item] = getAllByRole("listitem"); await user.click(item); @@ -41,7 +41,7 @@ describe("Gallery", () => { it("runs action on Enter|Space press when item is in focus", async () => { const execute = jest.fn(); const props = mockProps({ onClick: listAction(mock => ({ ...mock(), execute })) }); - const { user, getAllByRole } = setup(} />); + const { user, getAllByRole } = setup(withGalleryContext(} />)); const [item] = getAllByRole("listitem"); await user.tab(); @@ -55,19 +55,19 @@ describe("Gallery", () => { describe("with different configurations per platform", () => { it("contains correct classes for desktop", () => { - const { getByRole } = render(); + const { getByRole } = render(withGalleryContext()); const list = getByRole("list"); expect(list).toHaveClass("widget-gallery-lg-12"); }); it("contains correct classes for tablet", () => { - const { getByRole } = render(); + const { getByRole } = render(withGalleryContext()); const list = getByRole("list"); expect(list).toHaveClass("widget-gallery-md-6"); }); it("contains correct classes for phone", () => { - const { getByRole } = render(); + const { getByRole } = render(withGalleryContext()); const list = getByRole("list"); expect(list).toHaveClass("widget-gallery-sm-3"); }); @@ -75,17 +75,19 @@ describe("Gallery", () => { describe("with custom classes", () => { it("contains correct classes in the wrapper", () => { - const { container } = render(); + const { container } = render(withGalleryContext()); expect(container.querySelector(".custom-class")).toBeVisible(); }); it("contains correct classes in the items", () => { const { getAllByRole } = render( - b.withItemClass(listExp(() => "custom-class")))} - /> + withGalleryContext( + b.withItemClass(listExp(() => "custom-class")))} + /> + ) ); const [item] = getAllByRole("listitem"); @@ -96,7 +98,9 @@ describe("Gallery", () => { describe("with pagination", () => { it("renders correctly", () => { const { asFragment } = render( - + withGalleryContext( + + ) ); expect(asFragment()).toMatchSnapshot(); @@ -105,14 +109,16 @@ describe("Gallery", () => { it("triggers correct events on click next button", async () => { const setPage = jest.fn(); const { user, getByLabelText } = setup( - + withGalleryContext( + + ) ); const next = getByLabelText("Go to next page"); @@ -124,11 +130,13 @@ describe("Gallery", () => { describe("with empty option", () => { it("renders correctly", () => { const { asFragment } = render( - renderWrapper(No items found)} - /> + withGalleryContext( + renderWrapper(No items found)} + /> + ) ); expect(asFragment()).toMatchSnapshot(); @@ -138,13 +146,15 @@ describe("Gallery", () => { describe("with accessibility properties", () => { it("renders correctly without items", () => { const { asFragment } = render( - renderWrapper(No items found)} - /> + withGalleryContext( + renderWrapper(No items found)} + /> + ) ); expect(asFragment()).toMatchSnapshot(); @@ -152,14 +162,16 @@ describe("Gallery", () => { it("renders correctly with items", () => { const { asFragment } = render( - `title for '${item.id}'`} - headerTitle="filter title" - emptyMessageTitle="empty message" - emptyPlaceholderRenderer={renderWrapper => renderWrapper(No items found)} - /> + withGalleryContext( + `title for '${item.id}'`} + headerTitle="filter title" + emptyMessageTitle="empty message" + emptyPlaceholderRenderer={renderWrapper => renderWrapper(No items found)} + /> + ) ); expect(asFragment()).toMatchSnapshot(); @@ -169,7 +181,7 @@ describe("Gallery", () => { describe("without filters", () => { it("renders structure without header container", () => { const props = { ...mockProps(), showHeader: false, header: undefined }; - const { asFragment } = render(); + const { asFragment } = render(withGalleryContext()); expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap b/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap index 45f1262440..94c8ad9476 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap +++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap @@ -6,6 +6,9 @@ exports[`Gallery DOM Structure renders correctly 1`] = ` class="widget-gallery my-gallery" data-focusindex="0" > +
+ `; @@ -56,6 +66,9 @@ exports[`Gallery DOM Structure renders correctly with onclick event 1`] = ` class="widget-gallery my-gallery" data-focusindex="0" > + + `; @@ -121,6 +141,9 @@ exports[`Gallery with accessibility properties renders correctly with items 1`] class="widget-gallery my-gallery" data-focusindex="0" > + + `; @@ -174,6 +204,9 @@ exports[`Gallery with accessibility properties renders correctly without items 1 class="widget-gallery my-gallery" data-focusindex="0" > + + `; @@ -205,6 +245,9 @@ exports[`Gallery with empty option renders correctly 1`] = ` class="widget-gallery my-gallery" data-focusindex="0" > + + `; @@ -397,6 +447,13 @@ exports[`Gallery with pagination renders correctly 1`] = ` + `; @@ -407,6 +464,9 @@ exports[`Gallery without filters renders structure without header container 1`] class="widget-gallery my-gallery" data-focusindex="0" > + + `; diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx index 6e4e30e17a..7bac996fb3 100644 --- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx @@ -3,14 +3,18 @@ import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navig import { PositionController } from "@mendix/widget-plugin-grid/keyboard-navigation/PositionController"; import { VirtualGridLayout } from "@mendix/widget-plugin-grid/keyboard-navigation/VirtualGridLayout"; import { getColumnAndRowBasedOnIndex, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; -import { listAction, objectItems } from "@mendix/widget-plugin-test-utils"; +import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; +import { list, listAction, objectItems } from "@mendix/widget-plugin-test-utils"; import { render, RenderResult } from "@testing-library/react"; import userEvent, { UserEvent } from "@testing-library/user-event"; import { ObjectItem } from "mendix"; import { createElement } from "react"; +import { GalleryContainerProps } from "../../typings/GalleryProps"; import { GalleryProps } from "../components/Gallery"; import { ItemEventsController } from "../features/item-interaction/ItemEventsController"; import { ItemHelper } from "../helpers/ItemHelper"; +import { GalleryContext, GalleryRootScope } from "../helpers/root-context"; +import { GalleryStore } from "../stores/GalleryStore"; import { ItemHelperBuilder } from "./builders/ItemHelperBuilder"; export function setup(jsx: React.ReactElement): { user: UserEvent } & RenderResult { @@ -32,6 +36,53 @@ export function mockItemHelperWithAction(execute: () => void): ItemHelper { ); } +export function createMockGalleryContext(): GalleryRootScope { + // Create minimal GalleryContainerProps for the store + const mockContainerProps: GalleryContainerProps = { + name: "gallery-test", + class: "gallery-test-class", + datasource: list(3), + itemSelectionMode: "clear", + desktopItems: 4, + tabletItems: 3, + phoneItems: 2, + pageSize: 10, + pagination: "buttons", + showTotalCount: false, + showPagingButtons: "always", + pagingPosition: "bottom", + showEmptyPlaceholder: "none", + onClickTrigger: "single" + }; + + // Create a proper gate provider and gate + const gateProvider = new GateProvider(mockContainerProps); + const gate = gateProvider.gate; + + // Create real GalleryStore instance + const mockStore = new GalleryStore({ + gate, + name: "gallery-test", + pagination: "buttons", + showPagingButtons: "always", + showTotalCount: false, + pageSize: 10 + }); + + const mockSelectHelper = new SelectActionHandler("None", undefined); + + return { + rootStore: mockStore, + selectionHelper: undefined, + itemSelectHelper: mockSelectHelper + }; +} + +export function withGalleryContext(component: React.ReactElement, context?: GalleryRootScope): React.ReactElement { + const contextValue = context || createMockGalleryContext(); + return createElement(GalleryContext.Provider, { value: contextValue }, component); +} + type Helpers = { selectHelper?: SelectActionHandler; actionHelper?: ClickActionHelper;