@geotab/zenith 3.17.0-beta.1 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +26 -29
  2. package/dist/button/button.d.ts +1 -2
  3. package/dist/button/button.js +5 -22
  4. package/dist/button/buttonDisabledProps.d.ts +35 -0
  5. package/dist/button/buttonDisabledProps.js +33 -0
  6. package/dist/dataGrid/withSelectableRows/components/bulkActions/bulkActions.d.ts +1 -1
  7. package/dist/dropdownRaw/dropdownHelper.d.ts +20 -3
  8. package/dist/dropdownRaw/dropdownHelper.js +29 -6
  9. package/dist/dropdownRaw/dropdownRaw.js +22 -6
  10. package/dist/header/components/collapsedItemsControl/collapsedItemsControl.js +1 -1
  11. package/dist/header/headerButton.d.ts +15 -2
  12. package/dist/header/headerButton.js +11 -6
  13. package/dist/index.css +4 -8
  14. package/dist/menu/components/menuButton.js +40 -58
  15. package/dist/pageHeader/pageHeaderButton.d.ts +15 -1
  16. package/dist/pageHeader/pageHeaderButton.js +8 -6
  17. package/dist/pillExpandable/pillExpandable.js +3 -1
  18. package/dist/pillExpandable/pillExpandableSimple.d.ts +2 -1
  19. package/dist/pillExpandable/pillExpandableSimple.js +2 -2
  20. package/dist/table/selectable/interfaces.d.ts +6 -0
  21. package/dist/table/selectable/useDragSelection.d.ts +1 -1
  22. package/dist/table/selectable/useDragSelection.js +4 -4
  23. package/dist/table/selectable/useSelectableRows.d.ts +2 -0
  24. package/dist/table/selectable/useSelectableRows.js +88 -41
  25. package/dist/table/selectable/utils/calculateSelectionState.js +3 -2
  26. package/dist/table/selectable/utils/getCurrentPageSelection.js +9 -4
  27. package/dist/table/selectable/utils/groupExclusions.d.ts +7 -0
  28. package/dist/table/selectable/utils/groupExclusions.js +61 -0
  29. package/dist/table/selectable/utils/isRowChecked.js +1 -1
  30. package/dist/table/table.js +6 -5
  31. package/dist/tooltip/tooltip.d.ts +5 -3
  32. package/dist/tooltip/tooltip.js +11 -9
  33. package/esm/button/button.d.ts +1 -2
  34. package/esm/button/button.js +5 -22
  35. package/esm/button/buttonDisabledProps.d.ts +35 -0
  36. package/esm/button/buttonDisabledProps.js +29 -0
  37. package/esm/dataGrid/withSelectableRows/components/bulkActions/bulkActions.d.ts +1 -1
  38. package/esm/dropdownRaw/dropdownHelper.d.ts +20 -3
  39. package/esm/dropdownRaw/dropdownHelper.js +27 -5
  40. package/esm/dropdownRaw/dropdownRaw.js +23 -7
  41. package/esm/header/components/collapsedItemsControl/collapsedItemsControl.js +1 -1
  42. package/esm/header/headerButton.d.ts +15 -2
  43. package/esm/header/headerButton.js +11 -6
  44. package/esm/menu/components/menuButton.js +40 -58
  45. package/esm/pageHeader/pageHeaderButton.d.ts +15 -1
  46. package/esm/pageHeader/pageHeaderButton.js +8 -6
  47. package/esm/pillExpandable/pillExpandable.js +3 -1
  48. package/esm/pillExpandable/pillExpandableSimple.d.ts +2 -1
  49. package/esm/pillExpandable/pillExpandableSimple.js +2 -2
  50. package/esm/table/selectable/interfaces.d.ts +6 -0
  51. package/esm/table/selectable/useDragSelection.d.ts +1 -1
  52. package/esm/table/selectable/useDragSelection.js +4 -4
  53. package/esm/table/selectable/useSelectableRows.d.ts +2 -0
  54. package/esm/table/selectable/useSelectableRows.js +90 -43
  55. package/esm/table/selectable/utils/calculateSelectionState.js +3 -2
  56. package/esm/table/selectable/utils/getCurrentPageSelection.js +9 -4
  57. package/esm/table/selectable/utils/groupExclusions.d.ts +7 -0
  58. package/esm/table/selectable/utils/groupExclusions.js +55 -0
  59. package/esm/table/selectable/utils/isRowChecked.js +1 -1
  60. package/esm/table/table.js +6 -5
  61. package/esm/tooltip/tooltip.d.ts +5 -3
  62. package/esm/tooltip/tooltip.js +11 -9
  63. package/package.json +1 -1
  64. package/README.md.baseline +0 -1
@@ -2,6 +2,7 @@ import { IButton } from "../button/button";
2
2
  import { TButtonIconPosition } from "../textIconButton/textIconButton";
3
3
  import { TSvgIconSize } from "../svgIcon/svgIconSize";
4
4
  import { IIcon } from "../icons/icon";
5
+ import { TAlignment } from "../absolute/absolute";
5
6
  export interface IPageHeaderButton extends Omit<IButton, "onClick" | "icon" | "title"> {
6
7
  id: string;
7
8
  icon: React.FC<IIcon>;
@@ -9,10 +10,23 @@ export interface IPageHeaderButton extends Omit<IButton, "onClick" | "icon" | "t
9
10
  iconPosition?: TButtonIconPosition;
10
11
  iconClasses?: string;
11
12
  iconSize?: TSvgIconSize;
13
+ /**
14
+ * Tooltip content shown only while the button is `disabled`.
15
+ *
16
+ * When `disabled` and `disabledTooltip` are both set, the button uses `aria-disabled` instead of the native
17
+ * `disabled` attribute, so it stays focusable and hoverable and the tooltip can explain why it is unavailable.
18
+ * In every other case the standard `disabled` attribute is used, and `disabledTooltip` is ignored while the
19
+ * button is enabled.
20
+ */
21
+ disabledTooltip?: React.ReactNode;
12
22
  onClick?: (id: string, event: React.MouseEvent) => void;
13
23
  }
24
+ interface IPageHeaderButtonInternal {
25
+ tooltipAlignment?: TAlignment;
26
+ }
14
27
  export declare const PageHeaderButtonDisplayName = "PageHeaderButton";
15
28
  export declare const PageHeaderButton: {
16
- ({ id, children, icon, iconPosition, className, onClick, title, disabled, focusableWhenDisabled, ...restProps }: IPageHeaderButton): import("react/jsx-runtime").JSX.Element;
29
+ ({ id, children, icon, iconPosition, className, onClick, title, disabledTooltip, tooltipAlignment, disabled, link, ...restProps }: IPageHeaderButton & IPageHeaderButtonInternal): import("react/jsx-runtime").JSX.Element;
17
30
  displayName: string;
18
31
  };
32
+ export {};
@@ -12,13 +12,15 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { ButtonIconPosition, TextIconButton } from "../textIconButton/textIconButton";
14
14
  import { classNames } from "../commonHelpers/classNames/classNames";
15
+ import { Tooltip } from "../tooltip/tooltip";
16
+ import { getButtonDisabledProps } from "../button/buttonDisabledProps";
15
17
  export const PageHeaderButtonDisplayName = "PageHeaderButton";
16
18
  export const PageHeaderButton = (_a) => {
17
- var { id, children, icon, iconPosition = ButtonIconPosition.Start, className = "", onClick, title, disabled, focusableWhenDisabled } = _a, restProps = __rest(_a, ["id", "children", "icon", "iconPosition", "className", "onClick", "title", "disabled", "focusableWhenDisabled"]);
18
- const fullClassName = classNames(["zen-page-header-button", className || ""]);
19
- const props = Object.assign(Object.assign({}, restProps), { disabled,
20
- // Defaults on when disabled; pass `focusableWhenDisabled={false}` to opt back into native `disabled`.
21
- focusableWhenDisabled: focusableWhenDisabled !== null && focusableWhenDisabled !== void 0 ? focusableWhenDisabled : Boolean(disabled) });
22
- return (_jsx(TextIconButton, Object.assign({ id: id, icon: icon, title: title, iconPosition: iconPosition, className: fullClassName, onClick: onClick ? e => onClick(id, e) : undefined }, props, { children: children })));
19
+ var { id, children, icon, iconPosition = ButtonIconPosition.Start, className = "", onClick, title, disabledTooltip, tooltipAlignment = "bottom", disabled, link } = _a, restProps = __rest(_a, ["id", "children", "icon", "iconPosition", "className", "onClick", "title", "disabledTooltip", "tooltipAlignment", "disabled", "link"]);
20
+ const tooltip = disabled ? disabledTooltip : undefined;
21
+ const _b = getButtonDisabledProps({ disabled, focusable: Boolean(tooltip), link }), { className: disabledClassName } = _b, disabledProps = __rest(_b, ["className"]);
22
+ const fullClassName = classNames(["zen-page-header-button", className || "", disabledClassName !== null && disabledClassName !== void 0 ? disabledClassName : ""]);
23
+ const button = (_jsx(TextIconButton, Object.assign({ id: id, icon: icon, title: title, iconPosition: iconPosition, className: fullClassName, onClick: onClick ? e => onClick(id, e) : undefined, link: link }, restProps, disabledProps, { children: children })));
24
+ return tooltip ? (_jsx(Tooltip, { trigger: button, alignment: tooltipAlignment, children: tooltip })) : (button);
23
25
  };
24
26
  PageHeaderButton.displayName = PageHeaderButtonDisplayName;
@@ -420,6 +420,7 @@ const PillExpandableBase = ({
420
420
  }) => {
421
421
  const popupId = useId();
422
422
  const descriptionId = useId();
423
+ const counterId = useId();
423
424
  const [descriptionElement, setDescriptionElement] = useState(null);
424
425
  const isDrive = useDrive();
425
426
  const [isMobile, setIsMobile] = useState(false);
@@ -588,10 +589,11 @@ const PillExpandableBase = ({
588
589
  errorHandler: errorHandler,
589
590
  isBeta: isBeta,
590
591
  isMobile: isMobile,
591
- descriptionId: isPopupOpen && descriptionElement ? descriptionId : undefined,
592
+ descriptionId: [uniqueChildren && !hideCounterNumber ? counterId : undefined, isPopupOpen && descriptionElement ? descriptionId : undefined].filter(Boolean).join(" ") || undefined,
592
593
  ariaHasPopup: "dialog",
593
594
  dataAnalyticsId: dataAnalyticsId
594
595
  }), uniqueChildren && _jsx(PillExpandableSimple, {
596
+ id: counterId,
595
597
  icon: secondaryIcon || effectiveIcon,
596
598
  text: hideCounterNumber ? "" : counterText,
597
599
  includeExpandIcon: false,
@@ -2,6 +2,7 @@ import { FC, RefObject } from "react";
2
2
  import { IPillExpandableBase, TPillType } from "./pillExpandable";
3
3
  import { IIcon } from "../icons/icon";
4
4
  interface IPillExpandableSimple extends IPillExpandableBase {
5
+ id?: string;
5
6
  includeExpandIcon?: boolean;
6
7
  expanded?: boolean;
7
8
  onExpand?: () => void;
@@ -15,7 +16,7 @@ interface IPillExpandableSimple extends IPillExpandableBase {
15
16
  type?: TPillType;
16
17
  }
17
18
  export declare const PillExpandableSimple: {
18
- ({ onExpand, text, expanded, includeExpandIcon, type, icon, isBeta, isMobile, ref, className, descriptionId, ariaHasPopup, isDecorative, dataAnalyticsId }: IPillExpandableSimple): import("react/jsx-runtime").JSX.Element;
19
+ ({ id, onExpand, text, expanded, includeExpandIcon, type, icon, isBeta, isMobile, ref, className, descriptionId, ariaHasPopup, isDecorative, dataAnalyticsId }: IPillExpandableSimple): import("react/jsx-runtime").JSX.Element;
19
20
  displayName: string;
20
21
  };
21
22
  export {};
@@ -5,7 +5,7 @@ import { IconChevronTop } from "../icons/iconChevronTop";
5
5
  import { IconChevronBottom } from "../icons/iconChevronBottom";
6
6
  import { IconException } from "../icons/iconException";
7
7
  import { classNames } from "../commonHelpers/classNames/classNames";
8
- export const PillExpandableSimple = ({ onExpand, text, expanded, includeExpandIcon = true, type = "warning", icon = IconException, isBeta, isMobile, ref, className, descriptionId, ariaHasPopup, isDecorative, dataAnalyticsId
8
+ export const PillExpandableSimple = ({ id, onExpand, text, expanded, includeExpandIcon = true, type = "warning", icon = IconException, isBeta, isMobile, ref, className, descriptionId, ariaHasPopup, isDecorative, dataAnalyticsId
9
9
  // eslint-disable-next-line complexity
10
10
  }) => {
11
11
  const iconDriveClassName = useDriveClassName("icon");
@@ -34,7 +34,7 @@ export const PillExpandableSimple = ({ onExpand, text, expanded, includeExpandIc
34
34
  size: iconSize
35
35
  }) }) }))] }));
36
36
  if (isDecorative) {
37
- return (_jsx("span", { className: classNames([
37
+ return (_jsx("span", { id: id, className: classNames([
38
38
  "zen-status-pill__container",
39
39
  `zen-status-pill__container--${type}`,
40
40
  containerDriveClassName || "",
@@ -15,6 +15,12 @@ export interface ISelectionValue {
15
15
  * Consumers opting in must resolve the effective selection before running bulk actions.
16
16
  */
17
17
  selected: string[];
18
+ /**
19
+ * Excluded nested groups, including children that have not loaded yet. Only used with
20
+ * allowExclusions and all: true. Preserve this field when saving controlled selection;
21
+ * resolve these groups as well as selected before running a bulk action.
22
+ */
23
+ excludedGroupIds?: string[];
18
24
  }
19
25
  /**
20
26
  * One checkbox row in the order it is rendered. Expanded nested rows are part of this list,
@@ -4,7 +4,7 @@ import { IDragSelectionRow } from "./interfaces";
4
4
  * @param rows every checkbox row in render order, expanded nested rows included, so ranges can
5
5
  * span nesting levels.
6
6
  */
7
- export declare const useDragSelection: (rows: IDragSelectionRow[], gridRef: RefObject<HTMLElement | null> | undefined, onBatchSelect: (rangeIds: string[], select: boolean, base: Set<string>) => void, getBaseSelection: () => Set<string>, isSelectable: (row: IDragSelectionRow) => boolean, isEnabled: boolean) => {
7
+ export declare const useDragSelection: <TSelection = Set<string>>(rows: IDragSelectionRow[], gridRef: RefObject<HTMLElement | null> | undefined, onBatchSelect: (rangeIds: string[], select: boolean, base: TSelection) => void, getBaseSelection: () => TSelection, isSelectable: (row: IDragSelectionRow) => boolean, isEnabled: boolean) => {
8
8
  onCheckboxMouseDown: (id: string, checked: boolean, e: ReactMouseEvent) => void;
9
9
  suppressNextChangeRef: RefObject<boolean>;
10
10
  };
@@ -19,7 +19,7 @@ export const useDragSelection = (rows, gridRef, onBatchSelect, getBaseSelection,
19
19
  isEnabledRef.current = isEnabled;
20
20
  const anchorIdRef = useRef(null);
21
21
  const anchorSelectRef = useRef(true);
22
- const baseSelectedRef = useRef(new Set());
22
+ const baseSelectedRef = useRef(null);
23
23
  const isDraggingRef = useRef(false);
24
24
  const movedRef = useRef(false);
25
25
  const lastResultKeyRef = useRef("");
@@ -28,7 +28,7 @@ export const useDragSelection = (rows, gridRef, onBatchSelect, getBaseSelection,
28
28
  const attachedGridElementRef = useRef(null);
29
29
  const emitRange = useCallback((currentId) => {
30
30
  const anchorId = anchorIdRef.current;
31
- if (anchorId === null) {
31
+ if (anchorId === null || baseSelectedRef.current === null) {
32
32
  return;
33
33
  }
34
34
  const range = getRangeIds(rowsRef.current, anchorId, currentId, isSelectableRef.current);
@@ -82,7 +82,7 @@ export const useDragSelection = (rows, gridRef, onBatchSelect, getBaseSelection,
82
82
  (_c = attachedGridElementRef.current) === null || _c === void 0 ? void 0 : _c.removeEventListener("scroll", onScroll);
83
83
  (_d = attachedGridElementRef.current) === null || _d === void 0 ? void 0 : _d.classList.remove(DRAG_CLASS);
84
84
  attachedGridElementRef.current = null;
85
- }, [onMouseMove, onScroll]); // eslint-disable-line react-hooks/exhaustive-deps
85
+ }, [onMouseMove, onScroll]);
86
86
  const onCheckboxMouseDown = useCallback((id, checked, e) => {
87
87
  var _a, _b, _c, _d, _e;
88
88
  // Right-click between drag-end and the trailing click must not clear suppression.
@@ -92,7 +92,7 @@ export const useDragSelection = (rows, gridRef, onBatchSelect, getBaseSelection,
92
92
  if (!isEnabledRef.current || e.button !== 0) {
93
93
  return;
94
94
  }
95
- if (e.shiftKey && anchorIdRef.current !== null) {
95
+ if (e.shiftKey && anchorIdRef.current !== null && baseSelectedRef.current !== null) {
96
96
  e.preventDefault();
97
97
  const anchorId = anchorIdRef.current;
98
98
  const anchorInList = rowsRef.current.some(row => row.id === anchorId);
@@ -10,6 +10,8 @@ export interface ISelectableRowsOptions<T extends IRowEntity<N>, N extends IEnti
10
10
  onSelect: (selection: ISelectionValue) => void;
11
11
  /** Preserve cross-page Select All using excluded IDs. Consumers must resolve exclusions before running bulk actions. Defaults to false. */
12
12
  allowExclusions?: boolean;
13
+ /** Changes when the dataset represented by the selection is replaced, rather than paged. */
14
+ selectionDatasetId?: string | number;
13
15
  checkboxDisabled?: (entity: T | N, isNested?: boolean) => boolean;
14
16
  checkboxVisible?: (entity: T | N, isNested?: boolean) => boolean;
15
17
  checkboxInHeader?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { injectString } from "../../utils/localization/translationsDictionary";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /* eslint-disable complexity */
4
- import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { SelectionState } from "./interfaces";
6
6
  import { CheckboxCell } from "../../dataGrid/columns/checkboxColumn/checkboxCell";
7
7
  import { SelectableHeader } from "./selectableHeader";
@@ -13,7 +13,8 @@ import { isRowChecked } from "./utils/isRowChecked";
13
13
  import { getCurrentPageSelection } from "./utils/getCurrentPageSelection";
14
14
  import { useDragSelection } from "./useDragSelection";
15
15
  import { getDragSelectionRows } from "./utils/getDragSelectionRows";
16
- import { isShowMoreRow } from "../../commonHelpers/showMoreRow";
16
+ import { getChildrenForSelection, isShowMoreRow } from "../../commonHelpers/showMoreRow";
17
+ import { checkExcludedRow, createExcludedSelection, resolveGroupExclusions } from "./utils/groupExclusions";
17
18
  injectString("cs", "Select all", "Vybrat v\u0161e");
18
19
  injectString("da-DK", "Select all", "V\xE6lg alle");
19
20
  injectString("de", "Select all", "Alle ausw\xE4hlen");
@@ -88,13 +89,14 @@ export const isAllSelection = (all, selectedCount, allowExclusions) => !!all &&
88
89
  export const SelectableWrapperName = "Selectable";
89
90
  // Infinite scroll has no pages, so the header checkbox always means "select all" (never current page).
90
91
  const resolveCheckboxEmitsCurrentPage = (isInfinite, checkboxSelectsCurrentPage, turnOffSelectAll) => isInfinite ? false : checkboxSelectsCurrentPage !== null && checkboxSelectsCurrentPage !== void 0 ? checkboxSelectsCurrentPage : turnOffSelectAll;
91
- const getSelectionMenuState = (allSelected, selections, pageState) => allSelected ? selections.size === 0 ? SelectionState.All : SelectionState.Partial : pageState;
92
92
  export const useSelectableRows = (columns, entities, isMobile, selectMode, setSelectMode, selectable, isInfinite = false, gridRef, expandedRows) => {
93
93
  var _a;
94
94
  const {
95
95
  translate
96
96
  } = useLanguage();
97
- const selections = useMemo(() => getSelections(selectable === null || selectable === void 0 ? void 0 : selectable.selection), [selectable === null || selectable === void 0 ? void 0 : selectable.selection]);
97
+ const previousDatasetId = useRef(selectable === null || selectable === void 0 ? void 0 : selectable.selectionDatasetId);
98
+ const datasetWasReplaced = (selectable === null || selectable === void 0 ? void 0 : selectable.selectionDatasetId) !== undefined && previousDatasetId.current !== undefined && selectable.selectionDatasetId !== previousDatasetId.current;
99
+ const appliedSelections = useMemo(() => getSelections(selectable === null || selectable === void 0 ? void 0 : selectable.selection), [selectable === null || selectable === void 0 ? void 0 : selectable.selection]);
98
100
  const [state, setState] = useState(SelectionState.None);
99
101
  const checkboxVisible = useMemo(() => {
100
102
  const base = (selectable === null || selectable === void 0 ? void 0 : selectable.checkboxVisible) ? selectable.checkboxVisible : (selectable === null || selectable === void 0 ? void 0 : selectable.strategy) !== "nested" ? (_, isNested) => !isNested : () => true;
@@ -110,16 +112,19 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
110
112
  }
111
113
  if (selectable === null || selectable === void 0 ? void 0 : selectable.selectionLimit) {
112
114
  const limit = selectable.selectionLimit;
113
- return entity => selections.size >= limit && !selections.has(entity.id);
115
+ return entity => appliedSelections.size >= limit && !appliedSelections.has(entity.id);
114
116
  }
115
117
  return () => false;
116
- }, [selectable, selections]);
118
+ }, [selectable, appliedSelections]);
117
119
  // `checkRow` and `getCurrentPageSelection` read callbacks off `selectable`, so hand them the
118
120
  // wrapped ones to apply the same exclusions.
119
121
  const wrappedSelectable = useMemo(() => selectable ? Object.assign(Object.assign({}, selectable), {
120
122
  checkboxVisible,
121
123
  checkboxDisabled
122
124
  }) : undefined, [selectable, checkboxVisible, checkboxDisabled]);
125
+ const resolvedSelection = useMemo(() => resolveGroupExclusions(entities, wrappedSelectable, datasetWasReplaced), [datasetWasReplaced, entities, wrappedSelectable]);
126
+ const selections = useMemo(() => getSelections(resolvedSelection), [resolvedSelection]);
127
+ const excludedGroupIds = useMemo(() => new Set((selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) && selectable.strategy === "nested" && (resolvedSelection === null || resolvedSelection === void 0 ? void 0 : resolvedSelection.all) ? resolvedSelection.excludedGroupIds : []), [selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, resolvedSelection === null || resolvedSelection === void 0 ? void 0 : resolvedSelection.all, resolvedSelection === null || resolvedSelection === void 0 ? void 0 : resolvedSelection.excludedGroupIds]);
123
128
  const checkboxPlaceholder = useMemo(() => {
124
129
  if ((selectable === null || selectable === void 0 ? void 0 : selectable.checkboxPlaceholder) !== undefined) {
125
130
  return selectable.checkboxPlaceholder;
@@ -134,21 +139,21 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
134
139
  var _a;
135
140
  return isAllSelection((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all, selections.size, selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions);
136
141
  }, [(_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all, selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions, selections.size]);
137
- const selectableState = useMemo(() => calculateSelectionState(entities, selections, isAllSelected, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy), [checkboxDisabled, checkboxVisible, entities, isAllSelected, selections, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
138
- const selectionMenuState = getSelectionMenuState(isAllSelected, selections, selectableState);
142
+ const selectableState = useMemo(() => calculateSelectionState(entities, new Set([...selections, ...excludedGroupIds]), isAllSelected, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy), [checkboxDisabled, checkboxVisible, entities, isAllSelected, selections, excludedGroupIds, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
139
143
  const firstColumnId = useMemo(() => columns.length > 0 ? columns[0].id : null, [columns]);
140
144
  useEffect(() => {
141
145
  if (selectable && selectable.selection) {
142
- const newSelections = new Set(selectable.selection.selected);
143
- const newState = calculateSelectionState(entities, newSelections, isAllSelected, checkboxVisible, checkboxDisabled, selectable.strategy);
144
- setState(newState);
146
+ setState(selectableState);
145
147
  }
146
- }, [checkboxDisabled, checkboxVisible, entities, selectable, setSelectMode, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, isAllSelected]);
148
+ }, [selectable, selectableState]);
147
149
  const getNewSelection = useCallback((id, checked) => {
148
150
  if (isAllSelected && !(selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions)) {
149
151
  const included = getCurrentPageSelection(entities, wrappedSelectable);
150
152
  checkRow(included, wrappedSelectable, entities, id, checked);
151
- return included;
153
+ return {
154
+ selected: included,
155
+ groups: new Set()
156
+ };
152
157
  }
153
158
  if (isAllSelected) {
154
159
  // MYG-142234: `selections` holds the ids excluded from the "all" selection.
@@ -156,22 +161,29 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
156
161
  // exclusion set - it must never be rebuilt from `entities` (the current page),
157
162
  // otherwise every other page's rows would silently fall out of the selection.
158
163
  const excluded = new Set(selections);
159
- checkRow(excluded, wrappedSelectable, entities, id, !checked);
160
- return excluded;
164
+ const groups = new Set(excludedGroupIds);
165
+ checkExcludedRow(excluded, groups, wrappedSelectable, entities, id, checked);
166
+ return {
167
+ selected: excluded,
168
+ groups
169
+ };
161
170
  }
162
171
  checkRow(selections, wrappedSelectable, entities, id, checked);
163
- return selections;
164
- }, [entities, selectable, wrappedSelectable, selections, isAllSelected]);
172
+ return {
173
+ selected: selections,
174
+ groups: new Set()
175
+ };
176
+ }, [entities, selectable, wrappedSelectable, selections, excludedGroupIds, isAllSelected]);
165
177
  const onSelectListener = useCallback((id, checked) => {
166
178
  const wasAllSelected = !!(selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) && isAllSelected;
167
- const newSelection = getNewSelection(id, checked);
179
+ const {
180
+ selected: newSelection,
181
+ groups
182
+ } = getNewSelection(id, checked);
168
183
  const newState = calculateSelectionState(entities, newSelection, wasAllSelected, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy);
169
184
  // MYG-142234: once selection spans "all" pages, unchecking a row must keep `all: true`
170
185
  // and only record the exclusion - it must not collapse to the current page's ids.
171
- const newValue = wasAllSelected ? {
172
- selected: [...newSelection],
173
- all: true
174
- } : {
186
+ const newValue = wasAllSelected ? createExcludedSelection(newSelection, groups) : {
175
187
  selected: [...newSelection],
176
188
  all: newState === SelectionState.All
177
189
  };
@@ -180,24 +192,43 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
180
192
  setSelectMode(newValue.all || newValue.selected.length > 0);
181
193
  }, [checkboxDisabled, checkboxVisible, entities, selectable, setSelectMode, setState, getNewSelection, isAllSelected]);
182
194
  const onBatchSelect = useCallback((rangeIds, select, base) => {
183
- const result = new Set(base);
195
+ const result = new Set(base.selected);
196
+ const groups = new Set(base.excludedGroupIds);
184
197
  const limit = selectable === null || selectable === void 0 ? void 0 : selectable.selectionLimit;
185
198
  const wasAllSelected = !!(selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) && isAllSelected;
186
199
  for (const id of rangeIds) {
187
200
  if (!wasAllSelected && select && limit !== undefined && result.size >= limit) {
188
201
  break;
189
202
  }
190
- checkRow(result, wrappedSelectable, entities, id, wasAllSelected ? !select : select);
203
+ if (wasAllSelected) {
204
+ checkExcludedRow(result, groups, wrappedSelectable, entities, id, select);
205
+ } else {
206
+ checkRow(result, wrappedSelectable, entities, id, select);
207
+ }
191
208
  }
192
209
  const newState = calculateSelectionState(entities, result, wasAllSelected, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy);
193
- const newValue = {
210
+ const newValue = wasAllSelected ? createExcludedSelection(result, groups) : {
194
211
  selected: [...result],
195
- all: wasAllSelected || newState === SelectionState.All
212
+ all: newState === SelectionState.All
196
213
  };
197
214
  setState(newState);
198
215
  selectable === null || selectable === void 0 ? void 0 : selectable.onSelect(newValue);
199
216
  setSelectMode(newValue.all || newValue.selected.length > 0);
200
217
  }, [entities, selectable, wrappedSelectable, checkboxVisible, checkboxDisabled, setSelectMode, isAllSelected]);
218
+ // Persist group intent separately from concrete IDs so remounts and incremental batches agree.
219
+ useEffect(() => {
220
+ previousDatasetId.current = selectable === null || selectable === void 0 ? void 0 : selectable.selectionDatasetId;
221
+ }, [selectable === null || selectable === void 0 ? void 0 : selectable.selectionDatasetId]);
222
+ useEffect(() => {
223
+ const applied = selectable === null || selectable === void 0 ? void 0 : selectable.selection;
224
+ if (!applied || !resolvedSelection || !selectable.allowExclusions || !applied.all || selectable.strategy !== "nested") {
225
+ return;
226
+ }
227
+ const sameIds = (left = [], right = []) => left.length === right.length && left.every((id, index) => id === right[index]);
228
+ if (!sameIds(applied.selected, resolvedSelection.selected) || !sameIds(applied.excludedGroupIds, resolvedSelection.excludedGroupIds)) {
229
+ selectable.onSelect(resolvedSelection);
230
+ }
231
+ }, [selectable, resolvedSelection]);
201
232
  // Deliberately independent of the current selection, so the drag anchor survives the
202
233
  // selection changes a drag itself produces.
203
234
  const {
@@ -208,7 +239,12 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
208
239
  const entity = entityById.get(row.id);
209
240
  return !!entity && checkboxVisible(entity, row.isNested) && !checkboxDisabled(entity, row.isNested);
210
241
  }, [entityById, checkboxVisible, checkboxDisabled]);
211
- const getBaseSelection = useCallback(() => isAllSelected && !(selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) ? getCurrentPageSelection(entities, wrappedSelectable) : new Set(selections), [isAllSelected, selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions, entities, wrappedSelectable, selections]);
242
+ const getBaseSelection = useCallback(() => Object.assign({
243
+ all: isAllSelected,
244
+ selected: [...(isAllSelected && !(selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) ? getCurrentPageSelection(entities, wrappedSelectable) : selections)]
245
+ }, excludedGroupIds.size > 0 ? {
246
+ excludedGroupIds: [...excludedGroupIds]
247
+ } : {}), [isAllSelected, selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions, entities, wrappedSelectable, selections, excludedGroupIds]);
212
248
  const {
213
249
  onCheckboxMouseDown,
214
250
  suppressNextChangeRef
@@ -221,7 +257,7 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
221
257
  onSelectListener(id, checked);
222
258
  }, [onSelectListener, suppressNextChangeRef]);
223
259
  const onChangeSelectionState = useCallback(newState => {
224
- var _a, _b, _c;
260
+ var _a;
225
261
  if (newState === SelectionState.All) {
226
262
  (selectable === null || selectable === void 0 ? void 0 : selectable.onSelect) && selectable.onSelect({
227
263
  selected: [],
@@ -240,17 +276,16 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
240
276
  }
241
277
  const newSelections = getCurrentPageSelection(entities, wrappedSelectable);
242
278
  setState(newState);
243
- const previousSelections = (_b = (_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.selected) !== null && _b !== void 0 ? _b : [];
244
- const newValue = (selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) && ((_c = selectable.selection) === null || _c === void 0 ? void 0 : _c.all) ? {
245
- selected: previousSelections.filter(id => !newSelections.has(id)),
246
- all: true
247
- } : {
279
+ const previousSelections = [...selections];
280
+ const groups = new Set(excludedGroupIds);
281
+ entities.forEach(entity => groups.delete(entity.id));
282
+ const newValue = (selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) && ((_a = selectable.selection) === null || _a === void 0 ? void 0 : _a.all) ? createExcludedSelection(new Set(previousSelections.filter(id => !newSelections.has(id))), groups) : {
248
283
  selected: [...new Set([...previousSelections, ...newSelections])],
249
284
  all: false
250
285
  };
251
286
  selectable === null || selectable === void 0 ? void 0 : selectable.onSelect(newValue);
252
287
  setSelectMode(newValue.all || newValue.selected.length > 0);
253
- }, [selectable, wrappedSelectable, entities, setSelectMode]);
288
+ }, [selectable, wrappedSelectable, entities, setSelectMode, selections, excludedGroupIds]);
254
289
  const checkboxTitle = useCallback(entity => (selectable === null || selectable === void 0 ? void 0 : selectable.checkboxTitle) ? selectable.checkboxTitle(entity) : firstColumnId ? entity[firstColumnId] || entity.id : entity.id, [firstColumnId, selectable]);
255
290
  const headerTitle = useCallback(() => {
256
291
  if (selectable === null || selectable === void 0 ? void 0 : selectable.headerTitle) {
@@ -266,7 +301,15 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
266
301
  // row must be evaluated through the SelectionState.All (exclusion-aware) branch of
267
302
  // isRowChecked - not the per-page `state`, which can be Partial/None for a page that
268
303
  // contains some of those exclusions.
269
- (entity, isNested) => isRowChecked(selections, isAllSelected ? SelectionState.All : state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, entity, isNested, checkboxVisible, checkboxDisabled), [selections, isAllSelected, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, checkboxVisible, checkboxDisabled]);
304
+ (entity, isNested) => {
305
+ if (!isNested && isAllSelected && excludedGroupIds.has(entity.id)) {
306
+ return {
307
+ indeterminate: false,
308
+ checked: false
309
+ };
310
+ }
311
+ return isRowChecked(selections, isAllSelected ? SelectionState.All : state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, entity, isNested, checkboxVisible, checkboxDisabled);
312
+ }, [selections, excludedGroupIds, isAllSelected, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, checkboxVisible, checkboxDisabled]);
270
313
  const hasSelectable = !!selectable;
271
314
  const selectableCheckboxSelectsCurrentPage = selectable === null || selectable === void 0 ? void 0 : selectable.checkboxSelectsCurrentPage;
272
315
  const selectableTurnOffSelectAll = selectable === null || selectable === void 0 ? void 0 : selectable.turnOffSelectAll;
@@ -332,13 +375,17 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
332
375
  // keep `all: true` - it must never be treated as an inclusion list, or
333
376
  // every other page's selection would be silently wiped out.
334
377
  const currentPageIds = getCurrentPageSelection(entities, wrappedSelectable);
335
- const newExcluded = new Set([...selectable.selection.selected, ...currentPageIds]);
336
- const newState = calculateSelectionState(entities, newExcluded, true, checkboxVisible, checkboxDisabled, selectable.strategy);
378
+ const newExcluded = new Set([...selections, ...currentPageIds]);
379
+ const groups = new Set(excludedGroupIds);
380
+ if (selectable.strategy === "nested") {
381
+ entities.forEach(entity => {
382
+ var _a;
383
+ if ((_a = getChildrenForSelection(entity)) === null || _a === void 0 ? void 0 : _a.length) groups.add(entity.id);
384
+ });
385
+ }
386
+ const newState = calculateSelectionState(entities, new Set([...newExcluded, ...groups]), true, checkboxVisible, checkboxDisabled, selectable.strategy);
337
387
  setState(newState);
338
- selectable.onSelect({
339
- selected: [...newExcluded],
340
- all: true
341
- });
388
+ selectable.onSelect(createExcludedSelection(newExcluded, groups));
342
389
  setSelectMode(true);
343
390
  } else {
344
391
  // Intentionally different from onSelect(None) ("Deselect all" menu item, which clears everything).
@@ -354,7 +401,7 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
354
401
  setSelectMode(remaining.length > 0);
355
402
  }
356
403
  },
357
- calculateSelectionState: () => selectionMenuState,
404
+ calculateSelectionState: () => isAllSelected ? selections.size === 0 && excludedGroupIds.size === 0 ? SelectionState.All : SelectionState.Partial : selections.size === 0 ? SelectionState.None : selectableState === SelectionState.CurrentPage ? SelectionState.CurrentPage : SelectionState.Partial,
358
405
  checkboxInHeader: checkboxInHeaderValue,
359
406
  header: selectableHeader,
360
407
  onSelect: newState => {
@@ -367,7 +414,7 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
367
414
  return cell;
368
415
  }
369
416
  };
370
- }, [checkboxDisabled, checkboxTitle, checkboxVisible, headerTitle, onChangeSelectionState, onCheckboxChange, onCheckboxMouseDown, hasSelectable, checkboxEmitsCurrentPage, checkboxInHeaderValue, selectableHeader, selectableTurnOffSelectAll, isInfinite, selectable, wrappedSelectable, selectionMenuState, state, isMobile, checkboxPlaceholder, isEntityChecked, entities, setSelectMode]);
417
+ }, [checkboxDisabled, checkboxTitle, checkboxVisible, headerTitle, onChangeSelectionState, onCheckboxChange, onCheckboxMouseDown, hasSelectable, checkboxEmitsCurrentPage, checkboxInHeaderValue, selectableHeader, selectableTurnOffSelectAll, isInfinite, selectable, wrappedSelectable, selectableState, isAllSelected, selections, excludedGroupIds, state, isMobile, checkboxPlaceholder, isEntityChecked, entities, setSelectMode]);
371
418
  const selectableFeedWrapper = useCallback((primaryData, secondaryData, entity, isNested) => {
372
419
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
373
420
  if (!entity) {
@@ -1,5 +1,6 @@
1
1
  import { getChildrenForSelection } from "../../../commonHelpers/showMoreRow";
2
2
  import { SelectionState } from "../interfaces";
3
+ const areAllLoading = (children) => children.every(child => child.isLoading);
3
4
  export const calculateSelectionState = (entities, selections, all, checkboxVisible, checkboxDisabled, strategy) => {
4
5
  if (selections.size === 0) {
5
6
  return all ? SelectionState.All : SelectionState.None;
@@ -9,7 +10,7 @@ export const calculateSelectionState = (entities, selections, all, checkboxVisib
9
10
  // otherwise-fully-selected set (see ISelectionValue), rather than included ids.
10
11
  const isExcluded = (entity) => {
11
12
  const children = strategy === "nested" ? getChildrenForSelection(entity) : undefined;
12
- if (children && children.length > 0) {
13
+ if (children && children.length > 0 && !areAllLoading(children)) {
13
14
  return children.every(child => {
14
15
  if (!checkboxVisible(child, true)) {
15
16
  return true;
@@ -24,7 +25,7 @@ export const calculateSelectionState = (entities, selections, all, checkboxVisib
24
25
  };
25
26
  const isPartiallyExcluded = (entity) => {
26
27
  const children = strategy === "nested" ? getChildrenForSelection(entity) : undefined;
27
- if (children && children.length > 0) {
28
+ if (children && children.length > 0 && !areAllLoading(children)) {
28
29
  return children.some(child => selections.has(child.id));
29
30
  }
30
31
  return selections.has(entity.id);
@@ -1,15 +1,20 @@
1
1
  import { getChildrenForSelection, isShowMoreRow } from "../../../commonHelpers/showMoreRow";
2
+ const isLoadingRow = (entity) => !!entity.isLoading;
2
3
  export const getCurrentPageSelection = (entities, selectable) => {
3
4
  const newSelections = new Set();
4
5
  entities.forEach(entity => {
5
- const isVisible = !entity.isLoading && !isShowMoreRow(entity) && ((selectable === null || selectable === void 0 ? void 0 : selectable.checkboxVisible) ? selectable.checkboxVisible(entity) : true);
6
+ if (isLoadingRow(entity) || isShowMoreRow(entity)) {
7
+ return;
8
+ }
9
+ const isVisible = (selectable === null || selectable === void 0 ? void 0 : selectable.checkboxVisible) ? selectable.checkboxVisible(entity) : true;
6
10
  const isEnabled = !(selectable === null || selectable === void 0 ? void 0 : selectable.checkboxDisabled) || !selectable.checkboxDisabled(entity);
7
11
  const children = (selectable === null || selectable === void 0 ? void 0 : selectable.strategy) === "nested" ? getChildrenForSelection(entity) : undefined;
8
12
  if (children && children.length > 0) {
9
13
  children.forEach(child => {
10
- const isChildVisible = !child.isLoading &&
11
- !isShowMoreRow(child) &&
12
- ((selectable === null || selectable === void 0 ? void 0 : selectable.checkboxVisible) ? selectable.checkboxVisible(child, true) : true);
14
+ if (isLoadingRow(child) || isShowMoreRow(child)) {
15
+ return;
16
+ }
17
+ const isChildVisible = (selectable === null || selectable === void 0 ? void 0 : selectable.checkboxVisible) ? selectable.checkboxVisible(child, true) : true;
13
18
  const isChildEnabled = !(selectable === null || selectable === void 0 ? void 0 : selectable.checkboxDisabled) || !selectable.checkboxDisabled(child, true);
14
19
  if (isChildVisible && isChildEnabled) {
15
20
  newSelections.add(child.id);
@@ -0,0 +1,7 @@
1
+ import { IEntityWithId } from "../../../commonHelpers/entity";
2
+ import { IRowEntity } from "../../../dataGrid/row/row";
3
+ import { ISelectionValue } from "../interfaces";
4
+ import { ISelectableRowsOptions } from "../useSelectableRows";
5
+ export declare const createExcludedSelection: (selected: Set<string>, groups: Set<string>) => ISelectionValue;
6
+ export declare const resolveGroupExclusions: <T extends IRowEntity<N>, N extends IEntityWithId>(entities: T[], selectable: ISelectableRowsOptions<T, N> | undefined, pruneMissingGroups?: boolean) => ISelectionValue | undefined;
7
+ export declare const checkExcludedRow: <T extends IRowEntity<N>, N extends IEntityWithId>(selected: Set<string>, groups: Set<string>, selectable: ISelectableRowsOptions<T, N> | undefined, entities: T[], id: string, checked: boolean) => void;
@@ -0,0 +1,55 @@
1
+ import { getChildrenForSelection } from "../../../commonHelpers/showMoreRow";
2
+ import { checkRow } from "./checkRow";
3
+ import { getCurrentPageSelection } from "./getCurrentPageSelection";
4
+ export const createExcludedSelection = (selected, groups) => (Object.assign({ all: true, selected: [...selected] }, (groups.size > 0 ? { excludedGroupIds: [...groups] } : {})));
5
+ export const resolveGroupExclusions = (entities, selectable, pruneMissingGroups = false) => {
6
+ var _a, _b;
7
+ const selection = selectable === null || selectable === void 0 ? void 0 : selectable.selection;
8
+ if (!(selectable === null || selectable === void 0 ? void 0 : selectable.allowExclusions) || !(selection === null || selection === void 0 ? void 0 : selection.all) || selectable.strategy !== "nested") {
9
+ return selection;
10
+ }
11
+ const selected = new Set(selection.selected);
12
+ const groups = new Set(selection.excludedGroupIds);
13
+ for (const entity of entities) {
14
+ if (selected.has(entity.id) && ((_a = getChildrenForSelection(entity)) === null || _a === void 0 ? void 0 : _a.length)) {
15
+ groups.add(entity.id);
16
+ }
17
+ }
18
+ for (const id of groups) {
19
+ const entity = entities.find(item => item.id === id);
20
+ if (!entity && pruneMissingGroups) {
21
+ selected.delete(id);
22
+ groups.delete(id);
23
+ continue;
24
+ }
25
+ if (!entity)
26
+ continue;
27
+ if ((_b = getChildrenForSelection(entity)) === null || _b === void 0 ? void 0 : _b.length) {
28
+ selected.delete(id);
29
+ }
30
+ for (const childId of getCurrentPageSelection([entity], selectable)) {
31
+ selected.add(childId);
32
+ }
33
+ }
34
+ return createExcludedSelection(selected, groups);
35
+ };
36
+ export const checkExcludedRow = (selected, groups, selectable, entities, id, checked) => {
37
+ var _a, _b;
38
+ if ((selectable === null || selectable === void 0 ? void 0 : selectable.strategy) === "nested") {
39
+ const entity = entities.find(item => item.id === id);
40
+ if (checked) {
41
+ groups.delete(id);
42
+ selected.delete(id);
43
+ // Selecting a child overrides the whole-group intent, retaining the other concrete exclusions.
44
+ for (const parent of entities) {
45
+ if ((_a = getChildrenForSelection(parent)) === null || _a === void 0 ? void 0 : _a.some(child => child.id === id)) {
46
+ groups.delete(parent.id);
47
+ }
48
+ }
49
+ }
50
+ else if (entity && ((_b = getChildrenForSelection(entity)) === null || _b === void 0 ? void 0 : _b.length)) {
51
+ groups.add(id);
52
+ }
53
+ }
54
+ checkRow(selected, selectable, entities, id, !checked);
55
+ };
@@ -24,7 +24,7 @@ export const isRowChecked = (selections, state, strategy, entity, isNested, chec
24
24
  if (children && children.length > 0) {
25
25
  const selectableChildren = filterSelectable(children, checkboxVisible, checkboxDisabled);
26
26
  if (selectableChildren.length === 0) {
27
- return { indeterminate: false, checked: true };
27
+ return { indeterminate: false, checked: !selections.has(entity.id) };
28
28
  }
29
29
  const allChildrenChecked = selectableChildren.every(child => !selections.has(child.id));
30
30
  const someChildrenChecked = selectableChildren.some(child => !selections.has(child.id));