diff --git a/packages/modules/data-widgets/CHANGELOG.md b/packages/modules/data-widgets/CHANGELOG.md index 08715434ba..4fab57747c 100644 --- a/packages/modules/data-widgets/CHANGELOG.md +++ b/packages/modules/data-widgets/CHANGELOG.md @@ -6,6 +6,10 @@ 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. + ## [3.5.0] DataWidgets - 2025-09-16 ### Changed 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 d4cda7edde..5945f09237 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; } :where(.widget-datagrid-grid.infinite-loading) { @@ -529,21 +537,30 @@ $root: ".widget-datagrid"; z-index: 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 { @@ -567,3 +584,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/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 19ab167c0d..e2efba99dc 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.4.0] - 2025-09-12 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 9ff9f6bb04..4eaf381991 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..a1edb73ec2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -244,6 +244,22 @@ Both + + Show selection count + + + Top + Bottom + Off + + + + Clear selection label + Customize the label of the 'Clear section' button + + Clear selection + + Load more caption 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..8ba2c046e6 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/SelectionCounter.tsx @@ -0,0 +1,27 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; +import { observer } from "mobx-react-lite"; +import { createElement } from "react"; +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"; + + return ( + + + {selectionCountStore.displayCount} | + + {selectionCountStore.clearButtonLabel} + + + + ); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 032fd9f74d..05b9739616 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 - - - - {hasMoreItems && paginationType === "loadMore" && ( - + {selectionCount} + + {pagination} + {hasMoreItems && paginationType === "loadMore" && ( setPage && setPage(prev => prev + 1)} @@ -30,25 +27,9 @@ export function WidgetFooter(props: WidgetFooterProps): ReactElement | null { > {loadMoreButtonCaption} - - )} - - {(pagingPosition === "bottom" || pagingPosition === "both") && pagination} + )} ); } - -const SelectionCounter = observer(function SelectionCounter() { - const { selectionCountStore, selectActionHelper } = useDatagridRootScope(); - - return ( - - {selectionCountStore.displayCount} | - - Clear selection - - - ); -}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx index 50eebeeb01..d3417b84f4 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx @@ -1,9 +1,19 @@ -import { createElement, ReactElement } from "react"; +import { createElement, ReactElement, ReactNode } from "react"; + +type WidgetTopBarProps = { + pagination: ReactNode; + selectionCount: ReactNode; +} & JSX.IntrinsicElements["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/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index a3e01d2e2d..1b5baa1072 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -53,6 +53,8 @@ export type ShowPagingButtonsEnum = "always" | "auto"; export type PagingPositionEnum = "bottom" | "top" | "both"; +export type SelectionCountPositionEnum = "top" | "bottom" | "off"; + export type ShowEmptyPlaceholderEnum = "none" | "custom"; export type OnClickTriggerEnum = "single" | "double"; @@ -105,6 +107,8 @@ export interface DatagridContainerProps { showPagingButtons: ShowPagingButtonsEnum; showNumberOfRows: boolean; pagingPosition: PagingPositionEnum; + selectionCountPosition: SelectionCountPositionEnum; + clearSelectionButtonLabel?: DynamicValue; loadMoreButtonCaption?: DynamicValue; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder?: ReactNode; @@ -157,6 +161,8 @@ export interface DatagridPreviewProps { showPagingButtons: ShowPagingButtonsEnum; showNumberOfRows: boolean; pagingPosition: PagingPositionEnum; + selectionCountPosition: SelectionCountPositionEnum; + clearSelectionButtonLabel: string; loadMoreButtonCaption: string; showEmptyPlaceholder: ShowEmptyPlaceholderEnum; emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; diff --git a/packages/shared/widget-plugin-grid/src/selection/stores/SelectionCountStore.ts b/packages/shared/widget-plugin-grid/src/selection/stores/SelectionCountStore.ts index 96227850a2..73b2a6a93a 100644 --- a/packages/shared/widget-plugin-grid/src/selection/stores/SelectionCountStore.ts +++ b/packages/shared/widget-plugin-grid/src/selection/stores/SelectionCountStore.ts @@ -6,26 +6,34 @@ type Gate = DerivedPropsGate<{ itemSelection?: SelectionSingleValue | SelectionMultiValue; selectedCountTemplateSingular?: DynamicValue; selectedCountTemplatePlural?: DynamicValue; + clearSelectionButtonLabel?: DynamicValue; }>; export class SelectionCountStore { private gate: Gate; private singular: string = "%d row selected"; private plural: string = "%d rows selected"; + private defaultClearLabel: string = "Clear selection"; - constructor(gate: Gate, spec: { singular?: string; plural?: string } = {}) { + constructor(gate: Gate, spec: { singular?: string; plural?: string; clearLabel?: string } = {}) { this.singular = spec.singular ?? this.singular; this.plural = spec.plural ?? this.plural; + this.defaultClearLabel = spec.clearLabel ?? this.defaultClearLabel; this.gate = gate; makeObservable(this, { displayCount: computed, selectedCount: computed, fmtSingular: computed, - fmtPlural: computed + fmtPlural: computed, + clearButtonLabel: computed }); } + get clearButtonLabel(): string { + return this.gate.props.clearSelectionButtonLabel?.value || this.defaultClearLabel; + } + get fmtSingular(): string { return this.gate.props.selectedCountTemplateSingular?.value || this.singular; }