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/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 79b2ac631b..6158e3d77d 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -307,52 +307,6 @@ $root: ".widget-datagrid"; width: 100%; } -.pagination-bar { - display: flex; - justify-content: flex-end; - white-space: nowrap; - align-items: baseline; - margin: 16px; - color: $pagination-caption-color; - - .paging-status { - padding: 0 8px 0; - } - - .pagination-button { - padding: 6px; - color: var(--gray-darker, $gray-darker); - border-color: transparent; - background-color: transparent; - - &:hover { - color: var(--brand-primary, $brand-primary); - border-color: transparent; - background-color: transparent; - } - - &:disabled { - border-color: transparent; - background-color: transparent; - } - - &:focus:not(:focus-visible) { - outline: none; - } - - &:focus-visible { - outline: 1px solid var(--brand-primary, $brand-primary); - } - } - .pagination-icon { - position: relative; - top: 4px; - display: inline-block; - width: 20px; - height: 20px; - } -} - /* Column selector for hidable columns outside DG context */ /* List of columns to select */ .column-selectors { @@ -522,6 +476,10 @@ $root: ".widget-datagrid"; } } +.widget-datagrid .progress-bar { + margin: var(--spacing-medium, 16px); +} + .widget-datagrid .widget-datagrid-load-more { display: block !important; margin: var(--spacing-small, 8px) 0; 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 4848ae75a7..38885d75b2 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss @@ -5,6 +5,7 @@ ========================================================================== */ $gallery-screen-lg: $screen-lg; $gallery-screen-md: $screen-md; +$root: ".widget-gallery"; @mixin grid-items($number, $suffix) { @for $i from 1 through $number { @@ -162,3 +163,19 @@ $gallery-screen-md: $screen-md; } } } + +#{$root}-btn-link { + cursor: pointer; + background: transparent; + border: none; + color: var(--link-color); + padding: 0.3em 0.5em; + border-radius: 6px; + display: inline-block; + white-space: nowrap; + + &:hover, + &:focus-visible { + background-color: var(--brand-primary-50, #e6e7f2); + } +} diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_pagination-bar.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_pagination-bar.scss new file mode 100644 index 0000000000..5e9e0e26cf --- /dev/null +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_pagination-bar.scss @@ -0,0 +1,44 @@ +.pagination-bar { + display: flex; + justify-content: flex-end; + white-space: nowrap; + align-items: baseline; + color: $pagination-caption-color; + + .paging-status { + padding: 0 8px 0; + } + + .pagination-button { + padding: 6px; + color: var(--gray-darker, $gray-darker); + border-color: transparent; + background-color: transparent; + + &:hover { + color: var(--brand-primary, $brand-primary); + border-color: transparent; + background-color: transparent; + } + + &:disabled { + border-color: transparent; + background-color: transparent; + } + + &:focus:not(:focus-visible) { + outline: none; + } + + &:focus-visible { + outline: 1px solid var(--brand-primary, $brand-primary); + } + } + .pagination-icon { + position: relative; + top: 4px; + display: inline-block; + width: 20px; + height: 20px; + } +} diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss index a9c5fb31c7..d27c9fc081 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/main.scss @@ -1,5 +1,6 @@ @import "../../../theme/web/custom-variables"; @import "variables"; +@import "pagination-bar"; @import "datagrid"; @import "datagrid-filters"; @import "datagrid-dropdown-filter"; diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index c7e8f0b31a..0fb3991f3e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -1,4 +1,5 @@ import { If } from "@mendix/widget-plugin-component-kit/If"; +import { Pagination as PagingButtons } from "@mendix/widget-plugin-grid/components/Pagination"; import cn from "classnames"; import { GUID, ObjectItem } from "mendix"; import { Selectable } from "mendix/preview/Selectable"; @@ -51,13 +52,18 @@ const numberOfItems = 3; const cls = { root: "widget-datagrid", topBar: "widget-datagrid-top-bar table-header", + pagingTop: "widget-datagrid-padding-top", + ptStart: "widget-datagrid-tb-start", + ptEnd: "widget-datagrid-tb-end", header: "widget-datagrid-header header-filters", content: "widget-datagrid-content", grid: "widget-datagrid-grid table", gridHeader: "widget-datagrid-grid-head", gridBody: "widget-datagrid-grid-body table-content", + footer: "widget-datagrid-footer table-footer", pb: "widget-datagrid-padding-bottom", pbStart: "widget-datagrid-pb-start", + pbMid: "widget-datagrid-pb-middle", pbEnd: "widget-datagrid-pb-end" }; @@ -79,9 +85,7 @@ export function preview(props: DatagridPreviewProps): ReactElement { - - - + ); @@ -96,8 +100,15 @@ function WidgetRoot({ children }: PropsWithChildren): ReactElement { ); } -function WidgetTopBar({ children }: PropsWithChildren): ReactElement { - return
{children}
; +function WidgetTopBar(): ReactElement { + return ( +
+
+
{useTopCounter() ? : null}
+
{usePagingTop() ? : null}
+
+
+ ); } function WidgetHeader(): ReactNode { @@ -113,8 +124,23 @@ function WidgetContent({ children }: PropsWithChildren): ReactElement { return
{children}
; } -function WidgetFooter({ children }: PropsWithChildren): ReactElement { - return
{children}
; +function WidgetFooter(): ReactElement { + const props = useProps(); + return ( +
+
+
{useBottomCounter() ? : null}
+
+ {props.pagination === "loadMore" ? ( + + ) : null} +
+
{usePagingBot() ? : null}
+
+
+ ); } function Grid({ children }: PropsWithChildren): ReactElement { @@ -269,14 +295,36 @@ function EmptyPlaceholder(): ReactElement { ); } -function PaddingBottom(): ReactElement { +const SelectionCounter = (): ReactNode => { + const props = useProps(); return ( -
-
-
+
+ + {props.selectedCountTemplateSingular} + +  |  +
); -} +}; + +const Pagination = (): ReactNode => { + const props = useProps(); + return ( + {}} + nextPage={() => {}} + numberOfItems={props.pageSize ?? 20} + page={0} + pageSize={props.pageSize ?? 10} + showPagingButtons={"always"} + previousPage={() => {}} + pagination={props.pagination} + /> + ); +}; function useColumns(): ColumnsPreviewType[] { const { columns } = useProps(); @@ -320,3 +368,25 @@ function useGridStyle(): CSSProperties { "--widgets-grid-template-columns": sizes.join(" ") } as CSSProperties; } + +function useTopCounter(): boolean { + const { itemSelection, selectionCounterPosition } = useProps(); + return itemSelection === "Multi" && selectionCounterPosition === "top"; +} + +function useBottomCounter(): boolean { + const { itemSelection, selectionCounterPosition } = useProps(); + return itemSelection === "Multi" && selectionCounterPosition === "bottom"; +} + +function usePagingTop(): boolean { + const props = useProps(); + const visible = props.showNumberOfRows || props.pagination === "buttons"; + return visible && props.pagingPosition !== "bottom"; +} + +function usePagingBot(): boolean { + const props = useProps(); + const visible = props.showNumberOfRows || props.pagination === "buttons"; + return visible && props.pagingPosition !== "top"; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx index ccaf2820ad..5433e25ce2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx @@ -13,11 +13,7 @@ export const WidgetFooter = observer(function WidgetFooter(): ReactElement | nul const selectionCounterVM = useSelectionCounterViewModel(); const customPagination = useCustomPagination(); - const showFooter = - selectionCounterVM.isBottomCounterVisible || - paging.paginationVisible || - paging.showVirtualScrollingWithRowCount || - paging.showLoadMore; + const showFooter = selectionCounterVM.isBottomCounterVisible || paging.paginationVisible || paging.loadMoreVisible; if (!showFooter) { return null; @@ -31,7 +27,7 @@ export const WidgetFooter = observer(function WidgetFooter(): ReactElement | nul
- +
+
+ ); +}; + +const Root = ({ children }: PropsWithChildren): ReactNode => { + const props = useProps(); + return ( +
+ {children} +
+ ); +}; + +const TopControls = (): ReactNode => { + return ( +
+
{useTopCounter() ? : null}
+
{usePagingTop() ? : null}
+
+ ); +}; - const clickActionHelper = useClickActionHelper({ onClick: props.onClick, onClickTrigger: "none" }); +const Header = (): ReactNode => { + const props = useProps(); - const itemEventsController = useItemEventsController( - selectHelper, - clickActionHelper, - focusController, - numberOfColumns, - props.itemSelectionMode + return ( +
+ +
+ +
); +}; - const sortAPI = useMemo( - () => - ({ - version: 1, - host: new SortStoreHost() - }) as const, - [] +const Item = ({ className }: { className?: string }): ReactNode => { + const props = useProps(); + return ( + +
+ ); +}; + +const Content = (): ReactNode => { + const props = useProps(); + const { desktopItems: lg, tabletItems: md, phoneItems: sm } = props; + const rows = 3; + const lgCount = (lg ?? 0) * rows - 1; + const mdCount = (md ?? 0) * rows - 1; + const smCount = (sm ?? 0) * rows - 1; return ( -
}> - ReactElement) => ( - - {renderWrapper(null)} - - ), - [emptyPlaceholder] - )} - header={ - - -
- - - } - showHeader - hasMoreItems={false} - items={items} - itemHelper={useItemPreviewHelper({ - contentValue: props.content, - hasOnClick: props.onClick !== null +
+
+ > + + {Array.from({ length: lgCount }).map((_, index) => ( + + ))} + {Array.from({ length: mdCount }).map((_, index) => ( + + ))} + {Array.from({ length: smCount }).map((_, index) => ( + + ))} +
); -} +}; + +const Footer = (): ReactNode => { + const props = useProps(); + return ( +
+
+
{useBottomCounter() ? : null}
+
+ {props.pagination === "loadMore" ? ( + {props.loadMoreButtonCaption} + ) : null} +
+
{usePagingBot() ? : null}
+
+
+ ); +}; export function preview(props: GalleryPreviewProps): ReactElement { return createElement(Preview, props); } + +function useTopCounter(): boolean { + const { itemSelection, selectionCountPosition } = useProps(); + return itemSelection === "Multi" && selectionCountPosition === "top"; +} + +function useBottomCounter(): boolean { + const { itemSelection, selectionCountPosition } = useProps(); + return itemSelection === "Multi" && selectionCountPosition === "bottom"; +} + +function usePagingTop(): boolean { + const props = useProps(); + const visible = props.showTotalCount || props.pagination === "buttons"; + return visible && props.pagingPosition !== "bottom"; +} + +function usePagingBot(): boolean { + const props = useProps(); + const visible = props.showTotalCount || props.pagination === "buttons"; + return visible && props.pagingPosition !== "top"; +} 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..465915afcd 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,8 +69,12 @@ + + Auto select first item + Automatically select the first item + - Item click toggles selection + Toggle on click Defines item selection behavior. Yes @@ -50,37 +94,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 +123,12 @@ Load more - - Show total count + + Custom pagination + + + + Custom pagination @@ -107,6 +139,10 @@ Auto + + Show total count + + Position of pagination @@ -124,8 +160,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 +201,6 @@ - - - On click trigger - - - Single click - Double click - - - - On click action - - - - On selection change - - - @@ -194,7 +233,7 @@ - + Filter section @@ -212,20 +251,30 @@ 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 + + + + 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..61965a9d70 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().isVirtualScrolling; 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..0ba2b728c3 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryFooterControls.tsx @@ -0,0 +1,36 @@ +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 { useCustomPagination, 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 customPagination = useCustomPagination(); + const loadMoreButtonCaption = "Load more"; + + return ( +
+
+ + + +
+
+ + {loadMoreButtonCaption} + +
+
+ + + + {customPagination.get()} +
+
+ ); +}); diff --git a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx index 5d0d96a09e..38df8e4df5 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryHeader.tsx @@ -1,13 +1,31 @@ -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..98a8da0b2c --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryTobBarControls.tsx @@ -0,0 +1,27 @@ +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 { Pagination } from "./Pagination"; + +export const GalleryTobBarControls = observer(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..6ae56d0843 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/GalleryWidget.tsx @@ -0,0 +1,30 @@ +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"; +import { RefreshStatus } from "./RefreshStatus"; + +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..5b1c8a0ca9 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/ListItem.tsx @@ -1,31 +1,37 @@ +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 } 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( + () => { + 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} + data-selected={isSelected ? true : undefined} > - {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..4cc45ccb5b 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, useTextsService } from "../model/hooks/injection-hooks"; export function LoadMoreButton(props: JSX.IntrinsicElements["button"]): ReactNode { return ( @@ -11,10 +11,9 @@ 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(); + const texts = useTextsService(); if (paging.pagination !== "loadMore") { return null; @@ -24,5 +23,5 @@ 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)}>{texts.loadMoreCaption}; }); 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/RefreshIndicator.tsx b/packages/pluggableWidgets/gallery-web/src/components/RefreshIndicator.tsx new file mode 100644 index 0000000000..7e96a92703 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/RefreshIndicator.tsx @@ -0,0 +1,16 @@ +import classNames from "classnames"; +import { ReactElement } from "react"; + +type RefreshIndicatorProps = { + className?: string; +}; + +export function RefreshIndicator({ className }: RefreshIndicatorProps): ReactElement { + return ( +
+
+ +
+
+ ); +} diff --git a/packages/pluggableWidgets/gallery-web/src/components/RefreshStatus.tsx b/packages/pluggableWidgets/gallery-web/src/components/RefreshStatus.tsx new file mode 100644 index 0000000000..0b566baec3 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/components/RefreshStatus.tsx @@ -0,0 +1,19 @@ +import { observer } from "mobx-react-lite"; +import { ReactNode } from "react"; +import { useLoaderViewModel } from "../model/hooks/injection-hooks"; + +export const RefreshStatus = observer(function RefreshStatus(): ReactNode { + const loaderVM = useLoaderViewModel(); + + if (!loaderVM.showRefreshIndicator) return null; + + if (!loaderVM.isRefreshing) return null; + + return ( +
+
+ +
+
+ ); +}); 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`] = ` - -