diff --git a/packages/modules/data-widgets/CHANGELOG.md b/packages/modules/data-widgets/CHANGELOG.md index 49f6017a3c..ed248754fe 100644 --- a/packages/modules/data-widgets/CHANGELOG.md +++ b/packages/modules/data-widgets/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We enhanced datagrid selection UI with responsive container queries and improved layout styling for header and footer components. +- We enhanced gallery selection UI with responsive container queries and improved layout styling for header and footer components to match datagrid implementation. + ## [3.6.1] DataWidgets - 2025-10-14 ### [3.6.1] Datagrid 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 e6ecaea017..48aac22092 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -311,7 +311,7 @@ $root: ".widget-datagrid"; justify-content: flex-end; white-space: nowrap; align-items: baseline; - margin: 16px; + margin: 0 16px; color: $pagination-caption-color; .paging-status { @@ -397,6 +397,10 @@ $root: ".widget-datagrid"; } } + &-top-bar { + container: widget-datagrid-header / inline-size; + } + &-content { overflow-x: auto; } @@ -409,6 +413,10 @@ $root: ".widget-datagrid"; display: contents; } + &-footer { + container: widget-datagrid-footer / inline-size; + } + &.widget-datagrid-selection-method-click { .tr.tr-selected .td { background-color: $grid-selected-row-background; @@ -517,7 +525,7 @@ $root: ".widget-datagrid"; .widget-datagrid .widget-datagrid-load-more { display: block !important; - margin: 0 auto; + margin: 0; } .infinite-loading.widget-datagrid-grid-body { @@ -540,21 +548,30 @@ $root: ".widget-datagrid"; grid-column: 1 / -1; } -:where(#{$root}-paging-bottom) { +:where(#{$root}-paging-bottom, #{$root}-padding-top) { display: flex; flex-flow: row nowrap; align-items: center; } -:where(#{$root}-pb-start, #{$root}-pb-end, #{$root}-pb-middle) { +:where(#{$root}-pb-end, #{$root}-tb-end) { + display: flex; + justify-content: flex-end; + align-items: center; +} + +:where(#{$root}-pb-start, #{$root}-tb-start, #{$root}-pb-end, #{$root}-tb-end, #{$root}-pb-middle) { flex-grow: 1; flex-basis: 33.33%; min-height: 20px; + height: 54px; + padding: var(--spacing-small) 0; } -:where(#{$root}-pb-start) { - margin-block: var(--spacing-medium); +:where(#{$root}-pb-start, #{$root}-tb-start) { padding-inline: var(--spacing-medium); + display: flex; + align-items: center; } #{$root}-clear-selection { @@ -565,6 +582,15 @@ $root: ".widget-datagrid"; color: var(--link-color); padding: 0; display: inline-block; + + &:focus:not(:focus-visible) { + outline: none; + } + + &:focus-visible { + outline: 1px solid var(--brand-primary, $brand-primary); + outline-offset: 2px; + } } @keyframes skeleton-loading { @@ -578,3 +604,23 @@ $root: ".widget-datagrid"; transform: rotate(1turn); } } + +@container widget-datagrid-footer (width < 500px) { + #{$root}-paging-bottom { + flex-direction: column; + :where(#{$root}-pb-start, #{$root}-pb-end, #{$root}-pb-middle) { + width: 100%; + justify-content: center; + } + } +} + +@container widget-datagrid-header (width < 500px) { + #{$root}-padding-top { + flex-direction: column-reverse; + :where(#{$root}-tb-start, #{$root}-tb-end) { + width: 100%; + justify-content: center; + } + } +} diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_export-alert.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_export-alert.scss index b3e5460690..42883df20c 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_export-alert.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_export-alert.scss @@ -20,7 +20,7 @@ display: flex; padding: 4px; &:focus-visible { - outline: 1px solid $brand-primary; + outline: 1px solid var(--brand-primary, $brand-primary); } } } 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 1269d26e6a..e9a49a6fef 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_gallery.scss @@ -73,11 +73,18 @@ $gallery-screen-md: $screen-md; } .widget-gallery-filter, - .widget-gallery-empty, - .widget-gallery-pagination { + .widget-gallery-empty { flex: 1; } + &-top-bar { + container: widget-gallery-header / inline-size; + } + + &-footer { + container: widget-gallery-footer / inline-size; + } + /** Helper classes */ @@ -89,20 +96,30 @@ $gallery-screen-md: $screen-md; width: inherit; } -:where(.widget-gallery-footer-controls) { +:where(.widget-gallery-footer-controls, .widget-gallery-top-bar-controls) { display: flex; flex-flow: row nowrap; + align-items: center; } -:where(.widget-gallery-fc-start) { - margin-block: var(--spacing-medium); - padding-inline: var(--spacing-medium); +:where(.widget-gallery-fc-end, .widget-gallery-tb-end) { + display: flex; + justify-content: flex-end; + align-items: center; } -:where(.widget-gallery-fc-start, .widget-gallery-fc-middle, .widget-gallery-fc-end) { +:where(.widget-gallery-fc-start, .widget-gallery-tb-start, .widget-gallery-fc-end, .widget-gallery-tb-end) { flex-grow: 1; flex-basis: 33.33%; min-height: 20px; + height: 54px; + padding: var(--spacing-small) 0; +} + +:where(.widget-gallery-fc-start, .widget-gallery-tb-start) { + padding-inline: var(--spacing-medium); + display: flex; + align-items: center; } .widget-gallery-clear-selection { @@ -113,4 +130,33 @@ $gallery-screen-md: $screen-md; color: var(--link-color); padding: 0; display: inline-block; + + &:focus:not(:focus-visible) { + outline: none; + } + + &:focus-visible { + outline: 1px solid var(--brand-primary, $brand-primary); + outline-offset: 2px; + } +} + +@container widget-gallery-footer (width < 500px) { + .widget-gallery-footer-controls { + flex-direction: column; + :where(.widget-gallery-fc-start, .widget-gallery-fc-end, .widget-gallery-fc-middle) { + width: 100%; + justify-content: center; + } + } +} + +@container widget-gallery-header (width < 500px) { + .widget-gallery-top-bar-controls { + flex-direction: column-reverse; + :where(.widget-gallery-tb-start, .widget-gallery-tb-end) { + width: 100%; + justify-content: center; + } + } } diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx index 9321eba3be..f54fe4aaae 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx @@ -22,7 +22,7 @@ export function withParentProvidedEnumStore

( function useEnumFilterAPI(): Result { const ctx = useFilterAPI(); - const slctAPI = useRef(undefined); + const slctAPI = useRef(undefined); if (ctx.hasError) { return error(ctx.error); diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index a859f8820c..9eb1114fff 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added configurable selection count visibility and clear selection button label template for improved row selection management. + ## [3.6.1] - 2025-10-14 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index 2139f6870d..abd7f1cfb2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -75,6 +75,7 @@ export function getProperties( ]); } }); + if (values.pagination === "buttons") { hidePropertyIn(defaultProperties, values, "showNumberOfRows"); } else { @@ -169,7 +170,11 @@ function hideSelectionProperties(defaultProperties: Properties, values: Datagrid } if (itemSelection !== "Multi") { - hidePropertyIn(defaultProperties, values, "keepSelection"); + hidePropertiesIn(defaultProperties, values, [ + "keepSelection", + "selectionCountPosition", + "clearSelectionButtonLabel" + ]); } } diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index b1171e778d..82dcc6a041 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -117,6 +117,7 @@ const Container = observer((props: Props): ReactElement => { pageSize={props.pageSize} paginationType={props.pagination} loadMoreButtonCaption={props.loadMoreButtonCaption?.value} + selectionCountPosition={props.selectionCountPosition} paging={paginationCtrl.showPagination} pagingPosition={props.pagingPosition} showPagingButtons={props.showPagingButtons} diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index c19285b1cd..afb1d00ec4 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -53,6 +53,22 @@ Keep selection If enabled, selected items will stay selected unless cleared by the user or a Nanoflow. + + Show selection count + + + Top + Bottom + Off + + + + Clear selection label + Customize the label of the 'Clear section' button + + Clear selection + + Loading type diff --git a/packages/pluggableWidgets/datagrid-web/src/components/CellElement.tsx b/packages/pluggableWidgets/datagrid-web/src/components/CellElement.tsx index c85651ffdd..4594854cc7 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/CellElement.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/CellElement.tsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import { DOMAttributes, forwardRef, JSX, memo, ReactElement, ReactNode } from "react"; +import { ComponentPropsWithoutRef, DOMAttributes, forwardRef, memo, ReactElement, ReactNode } from "react"; import { AlignmentEnum } from "typings/DatagridProps"; export type CellElementProps = { @@ -14,7 +14,7 @@ export type CellElementProps = { wrapText?: boolean; ["aria-hidden"]?: boolean; tabIndex?: number; -} & Omit; +} & Omit, "children">; const component = forwardRef(function CellElement( { className, borderTop, clickable, previewAsHidden, wrapText, alignment, tabIndex, ...rest }: CellElementProps, diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx index 4a4e61d9ec..96948a3bca 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx @@ -1,7 +1,7 @@ import classNames from "classnames"; -import { JSX, ReactElement } from "react"; +import { ComponentPropsWithoutRef, ReactElement } from "react"; -type P = Omit; +type P = Omit, "role">; export interface GridProps extends P { className?: string; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/SelectionCounter.tsx b/packages/pluggableWidgets/datagrid-web/src/components/SelectionCounter.tsx new file mode 100644 index 0000000000..c39aad32bf --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/SelectionCounter.tsx @@ -0,0 +1,35 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; +import { observer } from "mobx-react-lite"; +import { useDatagridRootScope } from "../helpers/root-context"; + +type SelectionCounterLocation = "top" | "bottom" | undefined; + +export const SelectionCounter = observer(function SelectionCounter({ + location +}: { + location?: SelectionCounterLocation; +}) { + const { selectionCountStore, selectActionHelper } = useDatagridRootScope(); + + const containerClass = location === "top" ? "widget-datagrid-tb-start" : "widget-datagrid-pb-start"; + + const clearButtonAriaLabel = `${selectionCountStore.clearButtonLabel} (${selectionCountStore.selectedCount} selected)`; + + return ( + +

+ + {selectionCountStore.displayCount} + +  |  + +
+ + ); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 70b7ae1aed..5b7644a280 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -9,6 +9,7 @@ import { LoadingTypeEnum, PaginationEnum, PagingPositionEnum, + SelectionCountPositionEnum, ShowPagingButtonsEnum } from "../../typings/DatagridProps"; import { SelectActionHelper } from "../helpers/SelectActionHelper"; @@ -25,6 +26,7 @@ import { WidgetFooter } from "./WidgetFooter"; import { WidgetHeader } from "./WidgetHeader"; import { WidgetRoot } from "./WidgetRoot"; import { WidgetTopBar } from "./WidgetTopBar"; +import { SelectionCounter } from "./SelectionCounter"; export interface WidgetProps { CellComponent: CellComponent; @@ -48,6 +50,7 @@ export interface WidgetProps(props: WidgetProps): ReactElemen headerContent, headerTitle, loadMoreButtonCaption, + selectionCountPosition, numberOfItems, page, pageSize, @@ -131,10 +135,11 @@ const Main = observer((props: WidgetProps): ReactElemen visibleColumns } = props; - const { basicData } = useDatagridRootScope(); + const { basicData, selectionCountStore } = useDatagridRootScope(); const showHeader = !!headerContent; - const showTopBar = paging && (pagingPosition === "top" || pagingPosition === "both"); + const showTopBarPagination = paging && (pagingPosition === "top" || pagingPosition === "both"); + const showFooterPagination = paging && (pagingPosition === "bottom" || pagingPosition === "both"); const pagination = paging ? ( (props: WidgetProps): ReactElemen /> ) : null; + const selectionCount = + selectionCountStore.selectedCount > 0 && selectionCountPosition !== "off" && selectionCountPosition ? ( + + ) : null; + + const showTopbarSelectionCount = selectionCount && selectionCountPosition === "top"; + const showFooterSelectionCount = selectionCount && selectionCountPosition === "bottom"; + const cssGridStyles = gridStyle(visibleColumns, { selectItemColumn: selectActionHelper.showCheckboxColumn, visibilitySelectorColumn: columnsHidable @@ -160,7 +173,10 @@ const Main = observer((props: WidgetProps): ReactElemen return ( - {showTopBar && {pagination}} + {showHeader && {headerContent}} (props: WidgetProps): ReactElemen number) => void; -} & JSX.IntrinsicElements["div"]; +} & ComponentPropsWithoutRef<"div">; export function WidgetFooter(props: WidgetFooterProps): ReactElement | null { - const { pagingPosition, pagination, paginationType, loadMoreButtonCaption, hasMoreItems, setPage, ...rest } = props; + const { pagination, selectionCount, paginationType, loadMoreButtonCaption, hasMoreItems, setPage, ...rest } = props; + return (
-
- -
- {hasMoreItems && paginationType === "loadMore" && ( -
+ {selectionCount} +
+ {pagination} + {hasMoreItems && paginationType === "loadMore" && ( -
- )} -
- {(pagingPosition === "bottom" || pagingPosition === "both") && pagination} + )}
); } - -const SelectionCounter = observer(function SelectionCounter() { - const { selectionCountStore, selectActionHelper } = useDatagridRootScope(); - - return ( - - {selectionCountStore.displayCount} |  - - - ); -}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx index 7e963c1857..f55c0db551 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx @@ -1,8 +1,8 @@ -import { JSX, ReactElement } from "react"; +import { ComponentPropsWithoutRef, ReactElement } from "react"; type WidgetHeaderProps = { headerTitle?: string; -} & JSX.IntrinsicElements["div"]; +} & ComponentPropsWithoutRef<"div">; export function WidgetHeader(props: WidgetHeaderProps): ReactElement | null { const { children, headerTitle, ...rest } = props; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx index b3a6b53de3..5f993efc2e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx @@ -1,8 +1,8 @@ import classNames from "classnames"; -import { JSX, ReactElement, useMemo, useRef } from "react"; +import { ComponentPropsWithoutRef, ReactElement, useMemo, useRef } from "react"; import { SelectionMethod } from "../helpers/SelectActionHelper"; -type P = JSX.IntrinsicElements["div"]; +type P = ComponentPropsWithoutRef<"div">; export interface WidgetRootProps extends P { className?: string; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx index 284e5fd6a3..bfe5b5d46d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx @@ -1,9 +1,19 @@ -import { JSX, ReactElement } from "react"; +import { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react"; + +type WidgetTopBarProps = { + pagination: ReactNode; + selectionCount: ReactNode; +} & ComponentPropsWithoutRef<"div">; + +export function WidgetTopBar(props: WidgetTopBarProps): ReactElement { + const { pagination, selectionCount, ...rest } = props; -export function WidgetTopBar(props: JSX.IntrinsicElements["div"]): ReactElement { return ( -
- {props.children} +
+
+ {selectionCount} + {pagination &&
{pagination}
} +
); } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap index cdc8ba6713..9b26eb9fe5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/Table.spec.tsx.snap @@ -5,6 +5,13 @@ exports[`Table renders the structure correctly 1`] = `
+
+
+
@@ -74,9 +81,6 @@ exports[`Table renders the structure correctly 1`] = `
-
@@ -91,6 +95,13 @@ exports[`Table renders the structure correctly for preview when no header is pro
+
+
+
@@ -160,9 +171,6 @@ exports[`Table renders the structure correctly for preview when no header is pro
-
@@ -177,6 +185,13 @@ exports[`Table renders the structure correctly with column alignments 1`] = `
+
+
+
@@ -279,9 +294,6 @@ exports[`Table renders the structure correctly with column alignments 1`] = `
-
@@ -296,6 +308,13 @@ exports[`Table renders the structure correctly with custom filtering 1`] = `
+
+
+
@@ -369,9 +388,6 @@ exports[`Table renders the structure correctly with custom filtering 1`] = `
-
@@ -386,6 +402,13 @@ exports[`Table renders the structure correctly with dragging 1`] = `
+
+
+
@@ -455,9 +478,6 @@ exports[`Table renders the structure correctly with dragging 1`] = `
-
@@ -472,6 +492,13 @@ exports[`Table renders the structure correctly with dynamic row class 1`] = `
+
+
+
@@ -541,9 +568,6 @@ exports[`Table renders the structure correctly with dynamic row class 1`] = `
-
@@ -558,6 +582,13 @@ exports[`Table renders the structure correctly with empty placeholder 1`] = `
+
+
+
@@ -627,9 +658,6 @@ exports[`Table renders the structure correctly with empty placeholder 1`] = `
-
@@ -644,6 +672,13 @@ exports[`Table renders the structure correctly with filtering 1`] = `
+
+
+
@@ -717,9 +752,6 @@ exports[`Table renders the structure correctly with filtering 1`] = `
-
@@ -734,6 +766,13 @@ exports[`Table renders the structure correctly with header filters and a11y 1`]
+
+
+
-
@@ -830,6 +866,13 @@ exports[`Table renders the structure correctly with header wrapper 1`] = `
+
+
+
@@ -903,9 +946,6 @@ exports[`Table renders the structure correctly with header wrapper 1`] = `
-
@@ -920,6 +960,13 @@ exports[`Table renders the structure correctly with hiding 1`] = `
+
+
+
@@ -1030,9 +1077,6 @@ exports[`Table renders the structure correctly with hiding 1`] = `
-
@@ -1047,6 +1091,13 @@ exports[`Table renders the structure correctly with paging 1`] = `
+
+
+
@@ -1116,9 +1167,6 @@ exports[`Table renders the structure correctly with paging 1`] = `
-
@@ -1223,6 +1271,13 @@ exports[`Table renders the structure correctly with resizing 1`] = `
+
+
+
@@ -1292,9 +1347,6 @@ exports[`Table renders the structure correctly with resizing 1`] = `
-
@@ -1309,6 +1361,13 @@ exports[`Table renders the structure correctly with sorting 1`] = `
+
+
+
@@ -1378,9 +1437,6 @@ exports[`Table renders the structure correctly with sorting 1`] = `
-
@@ -1395,6 +1451,13 @@ exports[`Table with selection method checkbox render an extra column and add cla
+
+
+
@@ -1545,9 +1608,6 @@ exports[`Table with selection method checkbox render an extra column and add cla
-
@@ -1652,6 +1712,13 @@ exports[`Table with selection method rowClick add class to each selected cell 1`
+
+
+
@@ -1759,9 +1826,6 @@ exports[`Table with selection method rowClick add class to each selected cell 1`
-
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts index 7e64c08ec0..c1644d2ea0 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -34,6 +34,7 @@ type RequiredProps = Pick< | "pagination" | "showPagingButtons" | "showNumberOfRows" + | "clearSelectionButtonLabel" >; type Gate = DerivedPropsGate; diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnStore.tsx b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnStore.tsx index dd5264bf2c..7a48f5b404 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnStore.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnStore.tsx @@ -7,7 +7,7 @@ import { ObjectItem, ValueStatus } from "mendix"; -import { JSX, KeyboardEvent, ReactElement, ReactNode } from "react"; +import { ComponentPropsWithoutRef, KeyboardEvent, ReactElement, ReactNode } from "react"; import { AlignmentEnum, ColumnsType } from "../../../../typings/DatagridProps"; import { ColumnId, GridColumn } from "../../../typings/GridColumn"; import { Big } from "big.js"; @@ -269,7 +269,7 @@ function CustomContent({ children: ReactNode; allowEventPropagation: boolean; }): ReactElement { - const wrapperProps: JSX.IntrinsicElements["div"] = allowEventPropagation + const wrapperProps: ComponentPropsWithoutRef<"div"> = allowEventPropagation ? {} : { onClick: stopPropagation, diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index a3e01d2e2d..513d2d6282 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -11,6 +11,8 @@ export type ItemSelectionMethodEnum = "checkbox" | "rowClick"; export type ItemSelectionModeEnum = "toggle" | "clear"; +export type SelectionCountPositionEnum = "top" | "bottom" | "off"; + export type LoadingTypeEnum = "spinner" | "skeleton"; export type ShowContentAsEnum = "attribute" | "dynamicText" | "customContent"; @@ -96,6 +98,8 @@ export interface DatagridContainerProps { itemSelectionMode: ItemSelectionModeEnum; showSelectAllToggle: boolean; keepSelection: boolean; + selectionCountPosition: SelectionCountPositionEnum; + clearSelectionButtonLabel?: DynamicValue; loadingType: LoadingTypeEnum; refreshIndicator: boolean; columns: ColumnsType[]; @@ -148,6 +152,8 @@ export interface DatagridPreviewProps { itemSelectionMode: ItemSelectionModeEnum; showSelectAllToggle: boolean; keepSelection: boolean; + selectionCountPosition: SelectionCountPositionEnum; + clearSelectionButtonLabel: string; loadingType: LoadingTypeEnum; refreshIndicator: boolean; columns: ColumnsPreviewType[]; diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md index b495b5c31c..a325447d39 100644 --- a/packages/pluggableWidgets/gallery-web/CHANGELOG.md +++ b/packages/pluggableWidgets/gallery-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added configurable selection count visibility and clear selection button label template for improved row selection management. + ## [3.6.1] - 2025-10-14 ### Fixed diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts index 10de4a8530..e12b6a9c14 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts @@ -19,7 +19,11 @@ export function getProperties(values: GalleryPreviewProps, defaultProperties: Pr } if (values.itemSelection !== "Multi") { - hidePropertiesIn(defaultProperties, values, ["keepSelection"]); + hidePropertiesIn(defaultProperties, values, [ + "keepSelection", + "selectionCountPosition", + "clearSelectionButtonLabel" + ]); } const usePersonalization = values.storeFilters || values.storeSort; diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx index 02fece07c7..e1994c0b37 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorPreview.tsx @@ -7,7 +7,7 @@ import { getColumnAndRowBasedOnIndex } from "@mendix/widget-plugin-grid/selectio 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, useCallback, useMemo } from "react"; +import { createElement, ReactElement, ReactNode, RefObject, useCallback, useMemo } from "react"; import { GalleryPreviewProps } from "../typings/GalleryProps"; import { Gallery as GalleryComponent } from "./components/Gallery"; import { useItemEventsController } from "./features/item-interaction/ItemEventsController"; @@ -64,7 +64,7 @@ function Preview(props: GalleryPreviewProps): ReactElement { ); return ( -
+
}> ); }); diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml index 7958540ecb..72e82eb550 100644 --- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml @@ -37,6 +37,22 @@ Keep selection If enabled, selected items will stay selected unless cleared by the user or a Nanoflow. + + Show selection count + + + Top + Bottom + Off + + + + Clear selection label + Customize the label of the 'Clear section' button + + Clear selection + + Content placeholder diff --git a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx index 3874d9a388..d40b6a1194 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/Gallery.tsx @@ -14,7 +14,7 @@ import { GalleryTopBar } from "./GalleryTopBar"; import { ListBox } from "./ListBox"; import { ListItem } from "./ListItem"; -import { PaginationEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; +import { PaginationEnum, SelectionCountPositionEnum, ShowPagingButtonsEnum } from "typings/GalleryProps"; import { LoadMore, LoadMoreButton as LoadMorePreview } from "../components/LoadMore"; import { ItemEventsController } from "../typings/ItemEventsController"; import { SelectionCounter } from "./SelectionCounter"; @@ -45,6 +45,7 @@ export interface GalleryProps { ariaLabelListBox?: string; ariaLabelItem?: (item: T) => string | undefined; preview?: boolean; + selectionCountPosition?: SelectionCountPositionEnum; // Helpers focusController: FocusTargetController; @@ -59,20 +60,18 @@ export interface GalleryProps { 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} - /> -
+ 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 = @@ -80,6 +79,14 @@ export function Gallery(props: GalleryProps): ReactElem const showBottomPagination = props.paging && (props.paginationPosition === "bottom" || props.paginationPosition === "both"); + const selectionCounter = + !props.preview && props.selectionCountPosition !== "off" ? ( + + ) : null; + + const showTopSelectionCounter = selectionCounter && props.selectionCountPosition === "top"; + const showBottomSelectionCounter = selectionCounter && props.selectionCountPosition === "bottom"; + return ( (props: GalleryProps): ReactElem selectable={false} data-focusindex={props.tabIndex || 0} > - {showTopPagination && pagination} + +
+ {showTopSelectionCounter && selectionCounter} + {showTopPagination &&
{pagination}
} +
+
{props.showHeader && {props.header}} {props.showRefreshIndicator ? : null} (props: GalleryProps): ReactElem ))}
-
{!props.preview && }
- {props.paginationType === "loadMore" && ( -
- {props.preview && {loadMoreButtonCaption}}{" "} - {!props.preview && {loadMoreButtonCaption}} -
- )} -
{showBottomPagination && pagination}
+ {showBottomSelectionCounter &&
{selectionCounter}
} + +
+ {showBottomPagination && pagination} + {props.paginationType === "loadMore" && + (props.preview ? ( + {loadMoreButtonCaption} + ) : ( + {loadMoreButtonCaption} + ))} +
diff --git a/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx b/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx index f5b69ee760..e311e5b834 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/SelectionCounter.tsx @@ -2,15 +2,34 @@ import { If } from "@mendix/widget-plugin-component-kit/If"; import { observer } from "mobx-react-lite"; import { useGalleryRootScope } from "../helpers/root-context"; -export const SelectionCounter = observer(function SelectionCounter() { +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.displayCount} |  - +
+ + {selectionCountStore.displayCount} + +  |  + +
); }); diff --git a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap b/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap index 293aded5c8..1bf1c40de9 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap +++ b/packages/pluggableWidgets/gallery-web/src/components/__tests__/__snapshots__/Gallery.spec.tsx.snap @@ -8,7 +8,11 @@ exports[`Gallery DOM Structure renders correctly 1`] = ` >