diff --git a/package.json b/package.json index 5d72d8091f..80033981ae 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.38.1", "@mendix/pluggable-widgets-tools": "10.21.2", + "@prettier/plugin-xml": ">=3.4.1", "@testing-library/react": ">=15.0.6", "@types/big.js": "^6.2.2", "@types/node": "~22.14.0", @@ -75,7 +76,6 @@ "mime-types": "patches/mime-types.patch", "mobx-react-lite@4.0.7": "patches/mobx-react-lite@4.0.7.patch", "mobx@6.12.3": "patches/mobx@6.12.3.patch", - "rc-trigger": "patches/rc-trigger.patch", "react-big-calendar@0.19.2": "patches/react-big-calendar@0.19.2.patch", "react-dropzone": "patches/react-dropzone.patch" }, diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx index d005ac1201..df477acb54 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import { PropsWithChildren, ReactElement } from "react"; import { useDatagridRootVM } from "../model/hooks/injection-hooks"; +/** @remark vm source `WidgetRoot.viewModel.ts` */ export const WidgetRoot = observer(function WidgetRoot({ children }: PropsWithChildren): ReactElement { const vm = useDatagridRootVM(); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts b/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts deleted file mode 100644 index 7d19cf3143..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/features/pagination/pagination.model.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { boundPageSize } from "@mendix/widget-plugin-grid/main"; -import { ComputedAtom, DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; -import { computed } from "mobx"; -import { ReactNode } from "react"; - -/** Atom for the dynamic page index provided by the widget's props. */ -export function dynamicPageAtom( - gate: DerivedPropsGate<{ dynamicPage?: { value?: Big } }>, - config: { isLimitBased: boolean } -): ComputedAtom { - return computed(() => { - const page = gate.props.dynamicPage?.value?.toNumber() ?? -1; - if (config.isLimitBased) { - return Math.max(page, -1); - } - // Switch to zero-based index for offset-based pagination - return Math.max(page - 1, -1); - }); -} - -/** Atom for the dynamic page size. */ -export function dynamicPageSizeAtom( - gate: DerivedPropsGate<{ dynamicPageSize?: { value?: Big } }> -): ComputedAtom { - return boundPageSize(() => gate.props.dynamicPageSize?.value?.toNumber() ?? -1); -} - -export function customPaginationAtom( - gate: DerivedPropsGate<{ customPagination?: ReactNode }> -): ComputedAtom { - return computed(() => gate.props.customPagination); -} 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 2493dfe82c..733fd39f9e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -6,15 +6,23 @@ import { createClickActionHelper, createFocusController, createSelectionHelper, - createSetPageAction, - createSetPageSizeAction, - currentPageAtom, DatasourceService, layoutAtom, - pageSizeAtom, SelectActionsProvider, TaskProgressService } from "@mendix/widget-plugin-grid/main"; +import { + createSetPageAction, + createSetPageSizeAction, + currentPageAtom, + customPaginationAtom, + dynamicPageAtom, + dynamicPageSizeAtom, + DynamicPaginationFeature, + PageControlService, + pageSizeAtom, + PaginationViewModel +} from "@mendix/widget-plugin-grid/pagination/main"; 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"; @@ -22,11 +30,7 @@ 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"; -import { DynamicPaginationFeature } from "../../features/pagination/DynamicPagination.feature"; -import { PageControlService } from "../../features/pagination/PageControl.service"; import { paginationConfig } from "../../features/pagination/pagination.config"; -import { customPaginationAtom, dynamicPageAtom, dynamicPageSizeAtom } from "../../features/pagination/pagination.model"; -import { PaginationViewModel } from "../../features/pagination/Pagination.viewModel"; import { createCellEventsController } from "../../features/row-interaction/CellEventsController"; import { creteCheckboxEventsController } from "../../features/row-interaction/CheckboxEventsController"; import { SelectAllModule } from "../../features/select-all/SelectAllModule.container"; @@ -39,8 +43,8 @@ import { rowClassProvider } from "../models/rows.model"; import { DatasourceParamsController } from "../services/DatasourceParamsController"; import { DerivedLoaderController } from "../services/DerivedLoaderController"; import { SelectionGate } from "../services/SelectionGate.service"; -import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; import { GridSizeStore } from "../stores/GridSize.store"; +import { CORE_TOKENS as CORE, DG_TOKENS as DG, SA_TOKENS } from "../tokens"; // base injected(ColumnGroupStore, CORE.setupService, CORE.mainGate, CORE.config, DG.filterHost); @@ -48,7 +52,7 @@ injected(DatasourceParamsController, CORE.setupService, DG.query, DG.combinedFil injected(DatasourceService, CORE.setupService, DG.queryGate, DG.refreshInterval.optional); injected(GridBasicData, CORE.mainGate); injected(WidgetRootViewModel, CORE.mainGate, CORE.config, DG.exportProgressService, SA_TOKENS.selectionDialogVM); -injected(GridSizeStore, CORE.atoms.hasMoreItems, DG.paginationConfig, DG.setPageAction); +injected(GridSizeStore, CORE.atoms.hasMoreItems, DG.paginationConfig, DG.setPageAction.optional); /** Pagination **/ injected(createSetPageAction, DG.query, DG.paginationConfig, DG.currentPage, DG.pageSize); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts index cdcf6d5911..cfe55f1015 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -10,9 +10,15 @@ import { SelectAllService, SelectionDynamicProps, SelectionHelperService, - SetPageAction, TaskProgressService } from "@mendix/widget-plugin-grid/main"; +import { + DynamicPaginationFeature, + GridPageControl, + PageSizeStore, + PaginationViewModel, + SetPageAction +} from "@mendix/widget-plugin-grid/pagination/main"; import { SelectAllFeature } from "@mendix/widget-plugin-grid/select-all/select-all.feature"; import { BarStore, @@ -27,9 +33,6 @@ 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"; -import { DynamicPaginationFeature } from "../features/pagination/DynamicPagination.feature"; -import { GridPageControl } from "../features/pagination/GridPageControl"; -import { PaginationViewModel } from "../features/pagination/Pagination.viewModel"; import { PaginationConfig } from "../features/pagination/pagination.config"; import { CellEventsController } from "../features/row-interaction/CellEventsController"; import { CheckboxEventsController } from "../features/row-interaction/CheckboxEventsController"; @@ -45,7 +48,6 @@ import { RowClassProvider } from "./models/rows.model"; import { DatagridSetupService } from "./services/DatagridSetup.service"; import { DerivedLoaderController, DerivedLoaderControllerConfig } from "./services/DerivedLoaderController"; import { TextsService } from "./services/Texts.service"; -import { PageSizeStore } from "./stores/PageSize.store"; import { GridSizeStore } from "./stores/GridSize.store"; /** Tokens to resolve dependencies from the container. */ diff --git a/packages/pluggableWidgets/gallery-web/package.json b/packages/pluggableWidgets/gallery-web/package.json index 81d9012097..16a68cea99 100644 --- a/packages/pluggableWidgets/gallery-web/package.json +++ b/packages/pluggableWidgets/gallery-web/package.json @@ -46,6 +46,8 @@ "@mendix/widget-plugin-filtering": "workspace:*", "@mendix/widget-plugin-mobx-kit": "workspace:^", "@mendix/widget-plugin-sorting": "workspace:*", + "brandi": "^5.0.0", + "brandi-react": "^5.0.0", "classnames": "^2.5.1", "mobx": "6.12.3", "mobx-react-lite": "4.0.7" diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx index e1994c0b37..ce1859a112 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx @@ -1,112 +1,19 @@ import { enableStaticRendering } from "mobx-react-lite"; enableStaticRendering(true); -import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; -import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; -import { getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selection"; -import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context"; -import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; -import { GUID, ObjectItem } from "mendix"; -import { createElement, ReactElement, ReactNode, RefObject, useCallback, useMemo } from "react"; +import { createElement, ReactElement } from "react"; import { GalleryPreviewProps } from "../typings/GalleryProps"; -import { Gallery as GalleryComponent } from "./components/Gallery"; -import { useItemEventsController } from "./features/item-interaction/ItemEventsController"; -import { useGridPositionsPreview } from "./features/useGridPositionsPreview"; -import { useItemPreviewHelper } from "./helpers/ItemPreviewHelper"; -import { useItemSelectHelper } from "./helpers/useItemSelectHelper"; import "./ui/GalleryPreview.scss"; -const SortAPI = getGlobalSortContext(); - function Preview(props: GalleryPreviewProps): ReactElement { - const { emptyPlaceholder } = props; - const { numberOfColumns, numberOfRows, containerRef, numberOfItems } = useGridPositionsPreview({ - phoneItems: props.phoneItems ?? 1, - tabletItems: props.tabletItems ?? 1, - desktopItems: props.desktopItems ?? 1, - totalItems: props.pageSize ?? 3 - }); - - const items: ObjectItem[] = Array.from({ length: numberOfItems }).map((_, index) => ({ - id: String(index) as GUID - })); - - const selectHelper = useItemSelectHelper(props.itemSelection, undefined); - - const getPositionCallback = useCallback( - (index: number) => getColumnAndRowBasedOnIndex(numberOfColumns, items.length, index), - [numberOfColumns, items.length] - ); - - const focusController = useFocusTargetController({ - rows: numberOfRows, - columns: numberOfColumns, - pageSize: props.pageSize ?? 0 - }); - - const clickActionHelper = useClickActionHelper({ onClick: props.onClick, onClickTrigger: "none" }); - - const itemEventsController = useItemEventsController( - selectHelper, - clickActionHelper, - focusController, - numberOfColumns, - props.itemSelectionMode - ); - - const sortAPI = useMemo( - () => - ({ - version: 1, - host: new SortStoreHost() - }) as const, - [] - ); - return ( -
}> - ReactElement) => ( - - {renderWrapper(null)} - - ), - [emptyPlaceholder] - )} - header={ - - -
- - - } - showHeader - hasMoreItems={false} - items={items} - itemHelper={useItemPreviewHelper({ - contentValue: props.content, - hasOnClick: props.onClick !== null - })} - numberOfItems={props.pageSize!} - page={0} - pageSize={props.pageSize!} - paging={props.pagination === "buttons"} - paginationPosition={props.pagingPosition} - paginationType={props.pagination} - showPagingButtons={props.showPagingButtons} - showEmptyStatePreview={props.showEmptyPlaceholder === "custom"} - phoneItems={props.phoneItems!} - tabletItems={props.tabletItems!} - selectHelper={selectHelper} - itemEventsController={itemEventsController} - focusController={focusController} - getPosition={getPositionCallback} - showRefreshIndicator={false} - preview - /> +
+ FIX ME: Preview for gallery +
+ +
+ +
); } diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx index 4e2262c42a..1e98553069 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx @@ -1,126 +1,15 @@ -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, useCallback } from "react"; +import { ContainerProvider } from "brandi-react"; +import { ReactElement } 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 { GalleryContext, GalleryRootScope, useGalleryRootScope } from "./helpers/root-context"; -import { useGalleryJSActions } from "./helpers/useGalleryJSActions"; -import { useGalleryStore } from "./helpers/useGalleryStore"; -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, - phoneItems: props.phoneItems, - tabletItems: props.tabletItems, - totalItems: items.length - }; - const { numberOfColumns, numberOfRows } = useGridPositions(config); - const getPositionCallback = useCallback( - (index: number) => getColumnAndRowBasedOnIndex(numberOfColumns, items.length, index), - [numberOfColumns, items.length] - ); - - const focusController = useFocusTargetController({ - rows: numberOfRows, - columns: numberOfColumns, - pageSize: props.pageSize - }); - - const clickActionHelper = useClickActionHelper({ onClick: props.onClick, onClickTrigger: props.onClickTrigger }); - - const itemEventsController = useItemEventsController( - itemSelectHelper, - clickActionHelper, - focusController, - numberOfColumns, - props.itemSelectionMode - ); - - const itemHelper = useItemHelper({ - classValue: props.itemClass, - contentValue: props.content, - clickValue: props.onClick - }); - - useGalleryJSActions(rootStore, itemSelectHelper); - - const header = {props.filtersPlaceholder}; - - return ( - ReactElement) => - props.showEmptyPlaceholder === "custom" ? renderWrapper(props.emptyPlaceholder) :
, - [props.emptyPlaceholder, props.showEmptyPlaceholder] - )} - emptyMessageTitle={props.emptyMessageTitle?.value} - header={header} - headerTitle={props.filterSectionTitle?.value} - ariaLabelListBox={props.ariaLabelListBox?.value} - showHeader={!!props.filtersPlaceholder} - hasMoreItems={props.datasource.hasMoreItems ?? false} - items={items} - itemHelper={itemHelper} - numberOfItems={props.datasource.totalCount} - page={rootStore.paging.currentPage} - pageSize={props.pageSize} - paging={rootStore.paging.showPagination} - paginationPosition={props.pagingPosition} - paginationType={props.pagination} - setPage={rootStore.paging.setPage} - showPagingButtons={props.showPagingButtons} - phoneItems={props.phoneItems} - style={props.style} - tabletItems={props.tabletItems} - tabIndex={props.tabIndex} - selectHelper={itemSelectHelper} - itemEventsController={itemEventsController} - focusController={focusController} - getPosition={getPositionCallback} - loadMoreButtonCaption={props.loadMoreButtonCaption?.value} - showRefreshIndicator={rootStore.loaderCtrl.showRefreshIndicator} - selectionCountPosition={props.selectionCountPosition} - /> - ); -}); - -function useCreateGalleryScope(props: GalleryContainerProps): GalleryRootScope { - const rootStore = useGalleryStore(props); - const selectionHelper = useSelectionHelper( - props.itemSelection, - props.datasource, - props.onSelectionChange, - props.keepSelection ? "always keep" : "always clear" - ); - const itemSelectHelper = useItemSelectHelper(props.itemSelection, selectionHelper); - - return useConst({ - rootStore, - selectionHelper, - itemSelectHelper, - selectionCountStore: rootStore.selectionCountStore - }); -} +import { GalleryWidget } from "./components/GalleryWidget"; +import { useGalleryContainer } from "./model/hooks/useGalleryContainer"; export function Gallery(props: GalleryContainerProps): ReactElement { - const scope = useCreateGalleryScope(props); + const container = useGalleryContainer(props); return ( - - - + + + ); } diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index f786af638b..2023361b60 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -12,6 +12,10 @@ Filters placeholder + + Content placeholder + + Data source @@ -20,6 +24,42 @@ Refresh time (in seconds) + + + + Desktop columns + + + + Tablet columns + + + + Phone columns + + + + + + On click trigger + + + Single click + Double click + + + + On click action + + + + On selection change + + + + + + Selection @@ -29,14 +69,34 @@ + + Selection method + + + Checkbox + Row click + + + + Auto select first row + Automatically select the first row + - Item click toggles selection + Toggle on click Defines item selection behavior. Yes No + + "Select all" checkbox + Displays a checkbox in the grid header that allows selecting or deselecting all rows on the current page. + + + "Select all" across pages + Shows a banner with the option to select all rows across all pages when all rows on the current page are selected. + Keep selection If enabled, selected items will stay selected unless cleared by the user or a Nanoflow. @@ -50,37 +110,21 @@ Off - - Clear selection label - Customize the label of the 'Clear section' button - - Clear selection - Selectie wissen - - - - Content placeholder + + + + Loading type + + Spinner + Skeleton + Show refresh indicator Show a refresh indicator when the data is being loaded. - - - Desktop columns - - - - Tablet columns - - - - Phone columns - - - Page size @@ -95,8 +139,12 @@ Load more - - Show total count + + Custom pagination + + + + Custom pagination @@ -107,6 +155,10 @@ Auto + + Show total count + + Position of pagination @@ -124,8 +176,29 @@ Laad meer + + Page size attribute + Attribute to set the page size dynamically. + + + + + + Page attribute + Attribute to set the page dynamically. + + + + + + Total count + Attribute to store current total count + + + + - + Empty message @@ -144,24 +217,6 @@ - - - On click trigger - - - Single click - Double click - - - - On click action - - - - On selection change - - - @@ -194,38 +249,74 @@ - + - - Filter section - Assistive technology will read this upon reaching a filtering or sorting section. - - - Empty message - Assistive technology will read this upon reaching an empty message section. - - - Content description - Assistive technology will read this upon reaching gallery. - - - Item description - Assistive technology will read this upon reaching each gallery item. - + + + Filter section + Assistive technology will read this upon reaching a filtering or sorting section. + + + Empty message + Assistive technology will read this upon reaching an empty message section. + + + Content description + Assistive technology will read this upon reaching gallery. + + + Item description + Assistive technology will read this upon reaching each gallery item. + + + + - Item count singular + Row count singular Must include '%d' to denote number position - %d item selected - %d item geselecteerd + %d row selected + %d rij geselecteerd - Item count plural + Row count plural Must include '%d' to denote number position - %d items selected - %d items geselecteerd + %d rows selected + %d rijen geselecteerd + + + + Select all text + + + Select all rows in the data source + Selecteer alle rijen in de gegevensbron + + + + Select all template + This caption used when total count is available. + + Select all %d rows in the data source + Selecteer alle %d rijen in de gegevensbron + + + + Select status template + + + All %d rows selected. + Alle %d rijen geselecteerd. + + + + Clear selection label + Customize the label of the 'Clear section' button + + Clear selection + Selectie wissen diff --git a/packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx b/packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx new file mode 100644 index 0000000000..e01fdcc597 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/EmptyPlaceholder.tsx @@ -0,0 +1,15 @@ +import { observer } from "mobx-react-lite"; +import { ReactNode } from "react"; +import { useEmptyPlaceholderVM } from "../model/hooks/injection-hooks"; + +export const EmptyPlaceholder = observer(function EmptyPlaceholder(): ReactNode { + const vm = useEmptyPlaceholderVM(); + + if (!vm.content) return null; + + return ( +
+
{vm.content}
+
+ ); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx deleted file mode 100644 index 56d2b458b1..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator"; -import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination"; -import { KeyNavProvider } from "@mendix/widget-plugin-grid/keyboard-navigation/context"; -import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; -import { PositionInGrid, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; -import { ObjectItem } from "mendix"; -import { CSSProperties, ReactElement, ReactNode } from "react"; -import { GalleryItemHelper } from "../typings/GalleryItem"; -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, SelectionCountPositionEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; -import { LoadMore, LoadMoreButton as LoadMorePreview } from "../components/LoadMore"; -import { ItemEventsController } from "../typings/ItemEventsController"; -import { SelectionCounter } from "./SelectionCounter"; - -export interface GalleryProps { - className?: string; - desktopItems: number; - emptyPlaceholderRenderer?: (renderWrapper: (children: ReactNode) => ReactElement) => ReactElement; - emptyMessageTitle?: string; - header?: ReactNode; - headerTitle?: string; - showHeader: boolean; - hasMoreItems: boolean; - items: T[]; - numberOfItems?: number; - paging: boolean; - page: number; - pageSize: number; - paginationPosition?: "top" | "bottom" | "both"; - paginationType: PaginationEnum; - showPagingButtons: ShowPagingButtonsEnum; - showEmptyStatePreview?: boolean; - phoneItems: number; - setPage?: (computePage: (prevPage: number) => number) => void; - style?: CSSProperties; - tabletItems: number; - tabIndex?: number; - ariaLabelListBox?: string; - ariaLabelItem?: (item: T) => string | undefined; - preview?: boolean; - selectionCountPosition?: SelectionCountPositionEnum; - - // Helpers - focusController: FocusTargetController; - itemEventsController: ItemEventsController; - itemHelper: GalleryItemHelper; - selectHelper: SelectActionHandler; - getPosition: (index: number) => PositionInGrid; - loadMoreButtonCaption?: string; - showRefreshIndicator: boolean; -} - -export function Gallery(props: GalleryProps): ReactElement { - const { loadMoreButtonCaption = "Load more" } = props; - const pagination = props.paging ? ( - props.setPage && props.setPage(() => page)} - nextPage={() => props.setPage && props.setPage(prev => prev + 1)} - numberOfItems={props.numberOfItems} - page={props.page} - pageSize={props.pageSize} - previousPage={() => props.setPage && props.setPage(prev => prev - 1)} - pagination={props.paginationType} - showPagingButtons={props.showPagingButtons} - /> - ) : null; - - const showTopPagination = - props.paging && (props.paginationPosition === "top" || props.paginationPosition === "both"); - const showBottomPagination = - props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); - - const selectionCounter = - !props.preview && props.selectionCountPosition !== "off" && props.selectHelper.selectionType === "Multi" ? ( - - ) : null; - - const showTopSelectionCounter = selectionCounter && props.selectionCountPosition === "top"; - const showBottomSelectionCounter = selectionCounter && props.selectionCountPosition === "bottom"; - - const showLoadMore = props.paginationType === "loadMore"; - const showFooter = showBottomSelectionCounter || showBottomPagination || showLoadMore; - - return ( - - -
- {showTopSelectionCounter && selectionCounter} - {showTopPagination &&
{pagination}
} -
-
- {props.showHeader && {props.header}} - {props.showRefreshIndicator ? : null} - - {props.items.length > 0 && ( - - - {props.items.map((item, index) => ( - - ))} - - - )} - - {(props.items.length === 0 || props.showEmptyStatePreview) && - props.emptyPlaceholderRenderer && - props.emptyPlaceholderRenderer(children => ( -
-
{children}
-
- ))} - {showFooter && ( - -
- {showBottomSelectionCounter && ( -
{selectionCounter}
- )} - -
- {showBottomPagination && pagination} - {showLoadMore && - (props.preview ? ( - {loadMoreButtonCaption} - ) : ( - {loadMoreButtonCaption} - ))} -
-
-
- )} -
- ); -} diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx index 45b0da9952..fe7c5c838a 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryContent.tsx @@ -1,30 +1,21 @@ -import { InfiniteBodyProps, useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; +import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; import classNames from "classnames"; -import { ReactElement, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; +import { PropsWithChildren, ReactElement } from "react"; +import { usePaginationConfig, usePaginationVM } from "../model/hooks/injection-hooks"; -type PickProps = "hasMoreItems" | "setPage" | "isInfinite"; - -export type GalleryContentProps = { - className?: string; - children?: ReactNode; -} & Pick; - -export function GalleryContent({ - children, - className, - hasMoreItems, - isInfinite, - setPage -}: GalleryContentProps): ReactElement { +export const GalleryContent = observer(function GalleryContent({ children }: PropsWithChildren): ReactElement { + const paginationVM = usePaginationVM(); + const isInfinite = usePaginationConfig().isLimitBased; const [trackScrolling, bodySize, containerRef] = useInfiniteControl({ - hasMoreItems, + hasMoreItems: paginationVM.hasMoreItems, isInfinite, - setPage + setPage: paginationVM.setPage.bind(paginationVM) }); return (
0 ? { maxHeight: bodySize } : undefined} @@ -32,4 +23,4 @@ export function GalleryContent({ {children}
); -} +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx index e9d9b88bfa..033b31f45e 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooter.tsx @@ -1,12 +1,6 @@ import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { PropsWithChildren, ReactElement } from "react"; -type GalleryFooterProps = Omit; - -export function GalleryFooter({ children, className, ...rest }: GalleryFooterProps): ReactElement { - return ( -
- {children} -
- ); +export function GalleryFooter({ children, className }: PropsWithChildren<{ className?: string }>): ReactElement { + return
{children}
; } diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx new file mode 100644 index 0000000000..508a17780b --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx @@ -0,0 +1,35 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; +import { observer } from "mobx-react-lite"; +import { ReactElement } from "react"; +import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; +import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; +import { usePaginationConfig } from "../model/hooks/injection-hooks"; +import { LoadMore } from "./LoadMore"; +import { Pagination } from "./Pagination"; + +export const GalleryFooterControls = observer(function GalleryFooterControls(): ReactElement { + const counterVM = useSelectionCounterViewModel(); + const pgConfig = usePaginationConfig(); + const loadMoreButtonCaption = "Load more"; + + return ( +
+
+ + + +
+
+ + {loadMoreButtonCaption} + +
+
+ + + +
+
+ ); +} +) \ No newline at end of file diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx index 5d0d96a09e..4e01b2fd99 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx @@ -1,13 +1,29 @@ -import { JSX, ReactElement } from "react"; +import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; +import { getGlobalSelectionContext, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection"; +import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context"; +import { observer } from "mobx-react-lite"; +import { ReactElement } from "react"; +import { useFilterAPI, useMainGate, useSelectionHelper, useSortAPI } from "../model/hooks/injection-hooks"; -type GalleryHeaderProps = Omit; +const SelectionContext = getGlobalSelectionContext(); +const SortAPI = getGlobalSortContext(); +const FilterAPI = getGlobalFilterContextObject(); -export function GalleryHeader(props: GalleryHeaderProps): ReactElement | null { - const { children } = props; +export const GalleryHeader = observer(function GalleryHeader(): ReactElement | null { + const { filtersPlaceholder } = useMainGate().props; + const filterAPI = useFilterAPI(); + const sortAPI = useSortAPI(); + const selectionContext = useCreateSelectionContextValue(useSelectionHelper()); - if (!children) { + if (!filtersPlaceholder) { return null; } - return
; -} + return + + +
{filtersPlaceholder}
+
+
+
+}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx new file mode 100644 index 0000000000..b430f9477c --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryItems.tsx @@ -0,0 +1,32 @@ +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 { ListBox } from "./ListBox"; +import { ListItem } from "./ListItem"; + +export const GalleryItems = observer(function GalleryItems() { + const items = useItems().get(); + const config = useGalleryConfig(); + const texts = useTextsService(); + const focusController = useKeyNavFocus(); + + if (items.length < 1) { + return null; + } + + return ( + + + {items.map((item, index) => ( + + ))} + + + ); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx index bab0eee139..f4946b70af 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryRoot.tsx @@ -1,25 +1,15 @@ import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { observer } from "mobx-react-lite"; +import { PropsWithChildren, ReactElement } from "react"; +import { useGalleryRootVM } from "../model/hooks/injection-hooks"; -export type GalleryRootProps = Omit & { - selectable?: boolean; -}; - -export function GalleryRoot(props: GalleryRootProps): ReactElement { - const { className, selectable, children, ...rest } = props; +export const GalleryRoot = observer(function GalleryRoot(props: PropsWithChildren): ReactElement { + const { children } = props; + const vm = useGalleryRootVM(); return ( -
+
{children}
); -} +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx new file mode 100644 index 0000000000..eb8bfbd85f --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx @@ -0,0 +1,26 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; +import { ReactElement } from "react"; +import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; +import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; +import { usePaginationConfig } from "../model/hooks/injection-hooks"; +import { Pagination } from "./Pagination"; + +export function GalleryTobBarControls(): ReactElement { + const counterVM = useSelectionCounterViewModel(); + const pgConfig = usePaginationConfig(); + + return ( +
+
+ + + + +
+ +
+
+
+
+ ); +} diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx new file mode 100644 index 0000000000..cf016313d5 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx @@ -0,0 +1,28 @@ +import { ReactElement } from "react"; +import { EmptyPlaceholder } from "./EmptyPlaceholder"; +import { GalleryContent as Content } from "./GalleryContent"; +import { GalleryFooter as Footer } from "./GalleryFooter"; +import { GalleryFooterControls as FooterControls } from "./GalleryFooterControls"; +import { GalleryHeader as Header } from "./GalleryHeader"; +import { GalleryItems as Items } from "./GalleryItems"; +import { GalleryRoot as Root } from "./GalleryRoot"; +import { GalleryTobBarControls as TopBarControls } from "./GalleryTobBarControls"; +import { GalleryTopBar as TopBar } from "./GalleryTopBar"; + +export function GalleryWidget(): ReactElement { + return ( + + + + +
+ + + + +
+ +
+ + ); +} diff --git a/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx b/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx deleted file mode 100644 index bb6e871fa2..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; -import { getGlobalSelectionContext, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection"; -import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/react/context"; -import { ReactElement, ReactNode } from "react"; -import { useGalleryRootScope } from "../helpers/root-context"; - -const SelectionContext = getGlobalSelectionContext(); -const SortAPI = getGlobalSortContext(); -const FilterAPI = getGlobalFilterContextObject(); - -export function HeaderWidgetsHost(props: { children?: ReactNode }): ReactElement { - const { selectionHelper, rootStore } = useGalleryRootScope(); - const selectionContext = useCreateSelectionContextValue(selectionHelper); - - return ( - - - {props.children} - - - ); -} diff --git a/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx b/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx index 3335e65d8f..866a55d3db 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/ListBox.tsx @@ -1,12 +1,14 @@ import { SelectionType } from "@mendix/widget-plugin-grid/selection"; import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { ReactElement, ReactNode } from "react"; -type ListBoxProps = Omit & { +type ListBoxProps = { lg: number; md: number; sm: number; selectionType: SelectionType; + children?: ReactNode; + className?: string; }; export function ListBox({ children, className, selectionType, lg, md, sm, ...rest }: ListBoxProps): ReactElement { diff --git a/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx b/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx index d0b1644472..20ebac20f7 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx @@ -1,31 +1,38 @@ +import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps"; import classNames from "classnames"; import { ObjectItem } from "mendix"; -import { JSX, ReactElement, RefObject, useMemo } from "react"; -import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps"; -import { PositionInGrid, SelectActionHandler } from "@mendix/widget-plugin-grid/selection"; +import { computed, trace } from "mobx"; +import { observer } from "mobx-react-lite"; +import { ReactElement, RefObject, useMemo } from "react"; import { getAriaProps } from "../features/item-interaction/get-item-aria-props"; +import { useGalleryItemVM, useItemEventsVM, useLayoutService, useSelectActions } from "../model/hooks/injection-hooks"; +import { ListItemButton } from "./ListItemButton"; -import { GalleryItemHelper } from "../typings/GalleryItem"; -import { ItemEventsController } from "../typings/ItemEventsController"; - -type ListItemProps = Omit & { - eventsController: ItemEventsController; - getPosition: (index: number) => PositionInGrid; - helper: GalleryItemHelper; +type ListItemProps = { item: ObjectItem; itemIndex: number; - selectHelper: SelectActionHandler; - preview?: boolean; - label?: string; }; -export function ListItem(props: ListItemProps): ReactElement { - const { eventsController, getPosition, helper, item, itemIndex, selectHelper, label, ...rest } = props; - const clickable = helper.hasOnClick(item) || selectHelper.selectionType !== "None"; - const ariaProps = getAriaProps(item, selectHelper, label); +export const ListItem = observer(function ListItem(props: ListItemProps): ReactElement { + const { item, itemIndex, ...rest } = props; + + const eventsVM = useItemEventsVM().get(); + const selectActions = useSelectActions(); + const itemVM = useGalleryItemVM(); + const getPosition = useLayoutService().getPositionFn; + + const isSelected = computed( + () => { + trace(); + return selectActions.isSelected(item); + }, + { name: "[gallery]:@computed:ListItem:isSelected" } + ).get(); + const clickable = itemVM.hasOnClick(item) || selectActions.selectionType !== "None"; + const ariaProps = getAriaProps(selectActions.selectionType, isSelected, itemVM.label(item)); const { columnIndex, rowIndex } = getPosition(itemIndex); const keyNavProps = useFocusTargetProps({ columnIndex: columnIndex ?? -1, rowIndex }); - const handlers = useMemo(() => eventsController.getProps(item), [eventsController, item]); + const handlers = useMemo(() => eventsVM.getProps(item), [eventsVM, item]); return (
} tabIndex={keyNavProps.tabIndex} > - {helper.render(item)} + {itemVM.hasOnClick(item) === true ? ( + {itemVM.content(item)} + ) : ( + itemVM.content(item) + )}
); -} +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx b/packages/pluggableWidgets/gallery-web/src/components/LoadMore.tsx index fd3a2ca232..df9c4e9118 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 { JSX, ReactNode } from "react"; -import { useGalleryRootScope } from "../helpers/root-context"; +import { usePaginationVM } from "../model/hooks/injection-hooks"; export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): ReactNode { return ( @@ -11,10 +11,8 @@ export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): ReactNod ); } -export const LoadMore = observer(function LoadMore(props: { children: ReactNode }): ReactNode { - const { - rootStore: { paging } - } = useGalleryRootScope(); +export const LoadMore = observer(function LoadMore(): ReactNode { + const paging = usePaginationVM(); if (paging.pagination !== "loadMore") { return null; @@ -24,5 +22,9 @@ export const LoadMore = observer(function LoadMore(props: { children: ReactNode return null; } - return paging.setPage(n => n + 1)}>{props.children}; + return ( + paging.setPage(n => n + 1)}> + Fix me: Add load more caption from props + + ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx b/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx new file mode 100644 index 0000000000..bdbac0fe0b --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/Pagination.tsx @@ -0,0 +1,25 @@ +import { Pagination as PaginationComponent } from "@mendix/widget-plugin-grid/components/Pagination"; +import { observer } from "mobx-react-lite"; +import { ReactNode } from "react"; +import { usePaginationVM } from "../model/hooks/injection-hooks"; + +export const Pagination = observer(function Pagination(): ReactNode { + const paginationVM = usePaginationVM(); + + if (!paginationVM.paginationVisible) return null; + + return ( + 0} + gotoPage={page => paginationVM.setPage(page)} + nextPage={() => paginationVM.setPage(n => n + 1)} + numberOfItems={paginationVM.totalCount} + page={paginationVM.currentPage} + pageSize={paginationVM.pageSize} + showPagingButtons={paginationVM.showPagingButtons} + previousPage={() => paginationVM.setPage(n => n - 1)} + pagination={paginationVM.pagination} + /> + ); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx b/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx deleted file mode 100644 index 12d09ef397..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { If } from "@mendix/widget-plugin-component-kit/If"; -import { observer } from "mobx-react-lite"; -import { useGalleryRootScope } from "../helpers/root-context"; - -type SelectionCounterLocation = "top" | "bottom" | undefined; - -export const SelectionCounter = observer(function SelectionCounter({ - location -}: { - location?: SelectionCounterLocation; -}) { - const { selectionCountStore, itemSelectHelper } = useGalleryRootScope(); - - const containerClass = location === "top" ? "widget-gallery-tb-start" : "widget-gallery-pb-start"; - - const clearButtonAriaLabel = `${selectionCountStore.clearButtonLabel} (${selectionCountStore.selectedCount} selected)`; - - return ( - -
- - {selectionCountStore.selectedCountText} - -  |  - -
-
- ); -}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx deleted file mode 100644 index 3551501fef..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/Gallery.spec.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { listAction, listExpression, setupIntersectionObserverStub } from "@mendix/widget-plugin-test-utils"; -import "@testing-library/jest-dom"; -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, withGalleryContext } from "../../utils/test-utils"; -import { Gallery } from "../Gallery"; - -jest.mock("@mendix/widget-plugin-component-kit/RefreshIndicator", () => ({ - RefreshIndicator: (_props: any) => createElement("div", { "data-testid": "refresh-indicator" }) -})); - -describe("Gallery", () => { - beforeAll(() => { - setupIntersectionObserverStub(); - }); - describe("DOM Structure", () => { - it("renders correctly", () => { - const { asFragment } = render(withGalleryContext()); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders correctly with onclick event", () => { - const { asFragment } = render( - withGalleryContext() - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders RefreshIndicator when `showRefreshIndicator` is true", () => { - const base = mockProps(); - const props = { ...base, showRefreshIndicator: true }; - const { getByTestId } = render(withGalleryContext()); - expect(getByTestId("refresh-indicator")).toBeInTheDocument(); - }); - - it("does not render RefreshIndicator when `showRefreshIndicator` is false", () => { - const base = mockProps(); - const props = { ...base, showRefreshIndicator: false }; - const { queryByTestId } = render(withGalleryContext()); - expect(queryByTestId("refresh-indicator")).toBeNull(); - }); - }); - - describe("with on click action", () => { - it("runs action on item click", async () => { - const execute = jest.fn(); - const props = mockProps({ onClick: listAction(mock => ({ ...mock(), execute })) }); - const { user, getAllByRole } = setup(withGalleryContext()); - const [item] = getAllByRole("listitem"); - - await user.click(item); - await waitFor(() => expect(execute).toHaveBeenCalledTimes(1)); - }); - - 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(withGalleryContext(} />)); - const [item] = getAllByRole("listitem"); - - await user.tab(); - expect(item).toHaveFocus(); - await user.keyboard("[Enter]"); - await waitFor(() => expect(execute).toHaveBeenCalledTimes(1)); - await user.keyboard("[Space]"); - await waitFor(() => expect(execute).toHaveBeenCalledTimes(2)); - }); - }); - - describe("with different configurations per platform", () => { - it("contains correct classes for desktop", () => { - const { getByRole } = render(withGalleryContext()); - const list = getByRole("list"); - expect(list).toHaveClass("widget-gallery-lg-12"); - }); - - it("contains correct classes for tablet", () => { - const { getByRole } = render(withGalleryContext()); - const list = getByRole("list"); - expect(list).toHaveClass("widget-gallery-md-6"); - }); - - it("contains correct classes for phone", () => { - const { getByRole } = render(withGalleryContext()); - const list = getByRole("list"); - expect(list).toHaveClass("widget-gallery-sm-3"); - }); - }); - - describe("with custom classes", () => { - it("contains correct classes in the wrapper", () => { - const { container } = render(withGalleryContext()); - - expect(container.querySelector(".custom-class")).toBeVisible(); - }); - - it("contains correct classes in the items", () => { - const { getAllByRole } = render( - withGalleryContext( - - b.withItemClass(listExpression(() => "custom-class")) - )} - /> - ) - ); - const [item] = getAllByRole("listitem"); - - expect(item).toHaveClass("custom-class"); - }); - }); - - describe("with pagination", () => { - it("renders correctly", () => { - const { asFragment } = render( - withGalleryContext( - - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - 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"); - await user.click(next); - await waitFor(() => expect(setPage).toHaveBeenCalledTimes(1)); - }); - }); - - describe("with empty option", () => { - it("renders correctly", () => { - const { asFragment } = render( - withGalleryContext( - renderWrapper(No items found)} - /> - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - }); - - describe("with accessibility properties", () => { - it("renders correctly without items", () => { - const { asFragment } = render( - withGalleryContext( - renderWrapper(No items found)} - /> - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders correctly with items", () => { - const { asFragment } = render( - withGalleryContext( - `title for '${item.id}'`} - headerTitle="filter title" - emptyMessageTitle="empty message" - emptyPlaceholderRenderer={renderWrapper => renderWrapper(No items found)} - /> - ) - ); - - expect(asFragment()).toMatchSnapshot(); - }); - }); - - describe("without filters", () => { - it("renders structure without header container", () => { - const props = { ...mockProps(), showHeader: false, header: undefined }; - const { asFragment } = render(withGalleryContext()); - - expect(asFragment()).toMatchSnapshot(); - }); - }); -}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx b/packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx new file mode 100644 index 0000000000..ac6d6b3f0f --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/GalleryRoot.spec.tsx @@ -0,0 +1,30 @@ +import { render, RenderOptions } from "@testing-library/react"; +import { Container } from "brandi"; +import { ContainerProvider } from "brandi-react"; +import { ReactElement } from "react"; +import { createGalleryContainer } from "../../model/containers/createGalleryContainer"; +import { mockContainerProps } from "../../utils/mock-container-props"; +import { GalleryRoot } from "../GalleryRoot"; + +/** Function to bind hook context to provided container. */ +const renderWithContainer = ( + ui: ReactElement, + ct: Container, + options?: RenderOptions +): ReturnType => { + return render( + + {ui} + , + options + ); +}; + +describe("GalleryRoot", () => { + it("should render with correct className, style and tabIndex", () => { + const props = mockContainerProps(); + const [container] = createGalleryContainer({ ...props, tabIndex: 42, style: { color: "blue" } }); + const { asFragment } = renderWithContainer(, container); + 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 deleted file mode 100644 index 3c0d5c3e18..0000000000 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap +++ /dev/null @@ -1,492 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Gallery DOM Structure renders correctly 1`] = ` - -