@geotab/zenith 3.16.0-beta.2 → 3.16.0-beta.3

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 (59) hide show
  1. package/README.md +15 -12
  2. package/dist/absolute/absolute.js +1 -1
  3. package/dist/chart/plugins/linePlugin/getInterpolatedValue.d.ts +2 -2
  4. package/dist/chart/plugins/linePlugin/getInterpolatedValue.js +3 -2
  5. package/dist/chart/plugins/linePlugin/linePlugin.js +2 -1
  6. package/dist/chart/utils/getFormattedLabel.js +5 -1
  7. package/dist/checkbox/checkbox.d.ts +2 -1
  8. package/dist/checkbox/checkbox.js +2 -2
  9. package/dist/checkboxListWithAction/checkboxListWithAction.js +1 -0
  10. package/dist/commonHelpers/hooks/useTrapFocus.d.ts +0 -9
  11. package/dist/commonHelpers/hooks/useTrapFocus.js +14 -66
  12. package/dist/dataGrid/columns/checkboxColumn/checkboxCell.d.ts +2 -1
  13. package/dist/dataGrid/columns/checkboxColumn/checkboxCell.js +1 -1
  14. package/dist/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.d.ts +4 -2
  15. package/dist/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.js +10 -1
  16. package/dist/dataGrid/row/row.js +3 -2
  17. package/dist/dataGrid/withSelectableRows/withSelectableRows.js +73 -12
  18. package/dist/index.css +1 -1
  19. package/dist/table/selectable/interfaces.d.ts +9 -13
  20. package/dist/table/selectable/useDragSelection.d.ts +10 -0
  21. package/dist/table/selectable/useDragSelection.js +139 -0
  22. package/dist/table/selectable/useSelectableRows.d.ts +2 -2
  23. package/dist/table/selectable/useSelectableRows.js +59 -49
  24. package/dist/table/selectable/utils/calculateSelectionState.js +0 -35
  25. package/dist/table/selectable/utils/getDragSelectionRows.d.ts +15 -0
  26. package/dist/table/selectable/utils/getDragSelectionRows.js +28 -0
  27. package/dist/table/selectable/utils/getRangeIds.d.ts +2 -0
  28. package/dist/table/selectable/utils/getRangeIds.js +21 -0
  29. package/dist/table/selectable/utils/isRowChecked.js +1 -12
  30. package/dist/table/table.js +1 -1
  31. package/esm/absolute/absolute.js +1 -1
  32. package/esm/chart/plugins/linePlugin/getInterpolatedValue.d.ts +2 -2
  33. package/esm/chart/plugins/linePlugin/getInterpolatedValue.js +3 -2
  34. package/esm/chart/plugins/linePlugin/linePlugin.js +2 -1
  35. package/esm/chart/utils/getFormattedLabel.js +5 -1
  36. package/esm/checkbox/checkbox.d.ts +2 -1
  37. package/esm/checkbox/checkbox.js +2 -2
  38. package/esm/checkboxListWithAction/checkboxListWithAction.js +1 -0
  39. package/esm/commonHelpers/hooks/useTrapFocus.d.ts +0 -9
  40. package/esm/commonHelpers/hooks/useTrapFocus.js +15 -67
  41. package/esm/dataGrid/columns/checkboxColumn/checkboxCell.d.ts +2 -1
  42. package/esm/dataGrid/columns/checkboxColumn/checkboxCell.js +1 -1
  43. package/esm/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.d.ts +4 -2
  44. package/esm/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.js +10 -1
  45. package/esm/dataGrid/row/row.js +3 -2
  46. package/esm/dataGrid/withSelectableRows/withSelectableRows.js +73 -12
  47. package/esm/table/selectable/interfaces.d.ts +9 -13
  48. package/esm/table/selectable/useDragSelection.d.ts +10 -0
  49. package/esm/table/selectable/useDragSelection.js +135 -0
  50. package/esm/table/selectable/useSelectableRows.d.ts +2 -2
  51. package/esm/table/selectable/useSelectableRows.js +59 -49
  52. package/esm/table/selectable/utils/calculateSelectionState.js +0 -35
  53. package/esm/table/selectable/utils/getDragSelectionRows.d.ts +15 -0
  54. package/esm/table/selectable/utils/getDragSelectionRows.js +24 -0
  55. package/esm/table/selectable/utils/getRangeIds.d.ts +2 -0
  56. package/esm/table/selectable/utils/getRangeIds.js +17 -0
  57. package/esm/table/selectable/utils/isRowChecked.js +1 -12
  58. package/esm/table/table.js +1 -1
  59. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef } from "react";
1
+ import { useCallback } from "react";
2
2
  import { isElementInsideContainer } from "../../dialog/dialogHelpers";
3
3
  import { FOCUSABLE_SELECTOR } from "../../utils/focusableSelector";
4
4
  import { useKeydown } from "./useKeydown";
@@ -6,38 +6,18 @@ import { isChildOf } from "../../utils/isChildOf";
6
6
  import { isElementHidden } from "../../utils/isElementHidden";
7
7
  import { zen } from "../../utils/zen";
8
8
  const findFirstVisible = (elems, isElementVisible) => elems.find(elem => isElementVisible(elem));
9
- /**
10
- * Traps keyboard (Tab) focus within `containerRef` (and optionally its `triggerRef`).
11
- *
12
- * Nested-popup contract: for correct nested-popup behaviour, a focused element that triggers an
13
- * open child popup must expose a `data-popup-id` attribute whose value is the `id` of the child
14
- * popup element rendered in the DOM. When present, focus is deferred to that child popup's trap.
15
- * This contract is fulfilled automatically by `ControlledPopup`; callers using `useTrapFocus`
16
- * directly must set `data-popup-id` themselves or nested popups will silently lose focus handling.
17
- */
18
9
  export const useTrapFocus = (containerRef, triggerRef, subscriptionTrigger, condition, isElementVisible) => {
19
10
  /* eslint-disable complexity */
20
11
  const handleKeyDown = useCallback(e => {
21
- var _a, _b, _c, _d, _e, _f, _g;
12
+ var _a, _b, _c, _d, _e, _f;
22
13
  /* eslint-enable complexity */
23
14
  const getChildCondition = () => {
24
- var _a, _b, _c;
25
- const targetEl = e.target;
26
- // If the target is a trigger for an open child popup, defer to that popup's
27
- // focus trap. Key on data-popup-id + DOM presence so this works even when
28
- // preventAttributesAutoSet suppresses aria-expanded.
29
- const dataPopupId = targetEl.getAttribute("data-popup-id");
30
- if (dataPopupId &&
31
- ((_a = zen.document) === null || _a === void 0 ? void 0 : _a.getElementById(dataPopupId)) &&
32
- isChildOf(targetEl, containerRef.current) &&
33
- targetEl !== (triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current)) {
34
- return true;
35
- }
15
+ var _a, _b;
36
16
  return triggerRef
37
- ? !isChildOf(targetEl, containerRef.current) &&
38
- targetEl !== ((_b = zen.document) === null || _b === void 0 ? void 0 : _b.body) &&
39
- !isChildOf(targetEl, triggerRef.current)
40
- : !isChildOf(targetEl, containerRef.current) && targetEl !== ((_c = zen.document) === null || _c === void 0 ? void 0 : _c.body);
17
+ ? !isChildOf(e.target, containerRef.current) &&
18
+ e.target !== ((_a = zen.document) === null || _a === void 0 ? void 0 : _a.body) &&
19
+ !isChildOf(e.target, triggerRef.current)
20
+ : !isChildOf(e.target, containerRef.current) && e.target !== ((_b = zen.document) === null || _b === void 0 ? void 0 : _b.body);
41
21
  };
42
22
  if (!containerRef.current || getChildCondition()) {
43
23
  return;
@@ -47,14 +27,9 @@ export const useTrapFocus = (containerRef, triggerRef, subscriptionTrigger, cond
47
27
  : !isElementInsideContainer(target, containerEl.current) &&
48
28
  !isElementInsideContainer(target, triggerEl.current);
49
29
  const isTargetOutside = e.target ? checkIsElementOutside(e.target, containerRef, triggerRef) : undefined;
50
- const triggerElement = (_a = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current) !== null && _a !== void 0 ? _a : null;
51
- const triggerSelf = (triggerElement === null || triggerElement === void 0 ? void 0 : triggerElement.matches(FOCUSABLE_SELECTOR)) ? [triggerElement] : [];
52
- const triggerChildren = Array.from((_b = triggerElement === null || triggerElement === void 0 ? void 0 : triggerElement.querySelectorAll(FOCUSABLE_SELECTOR)) !== null && _b !== void 0 ? _b : []);
53
- // querySelectorAll only returns descendants (never the element itself), so triggerSelf
54
- // and triggerChildren are disjoint by definition and need no deduplication.
55
- const triggerSelectors = [...triggerSelf, ...triggerChildren];
30
+ const triggerSelectors = ((_a = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(FOCUSABLE_SELECTOR)) || [];
56
31
  const containerSelectors = containerRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
57
- const focusable = triggerRef ? [...triggerSelectors, ...Array.from(containerSelectors)] : [...Array.from(containerSelectors)];
32
+ const focusable = triggerRef ? [...Array.from(triggerSelectors), ...Array.from(containerSelectors)] : [...Array.from(containerSelectors)];
58
33
  if (!focusable.length) {
59
34
  return;
60
35
  }
@@ -71,58 +46,31 @@ export const useTrapFocus = (containerRef, triggerRef, subscriptionTrigger, cond
71
46
  return;
72
47
  }
73
48
  if (e.shiftKey) {
74
- if (((_c = zen.document) === null || _c === void 0 ? void 0 : _c.activeElement) === first) {
49
+ if (((_b = zen.document) === null || _b === void 0 ? void 0 : _b.activeElement) === first) {
75
50
  e.preventDefault();
76
51
  last === null || last === void 0 ? void 0 : last.focus();
77
52
  return;
78
53
  }
79
- if (firstContainerFocusable && ((_d = zen.document) === null || _d === void 0 ? void 0 : _d.activeElement) === firstContainerFocusable) {
54
+ if (firstContainerFocusable && ((_c = zen.document) === null || _c === void 0 ? void 0 : _c.activeElement) === firstContainerFocusable) {
80
55
  e.preventDefault();
81
56
  lastTriggerFocusable === null || lastTriggerFocusable === void 0 ? void 0 : lastTriggerFocusable.focus();
82
57
  }
83
58
  return;
84
59
  }
85
- if (((_e = zen.document) === null || _e === void 0 ? void 0 : _e.activeElement) === last) {
60
+ if (((_d = zen.document) === null || _d === void 0 ? void 0 : _d.activeElement) === last) {
86
61
  e.preventDefault();
87
62
  first === null || first === void 0 ? void 0 : first.focus();
88
63
  return;
89
64
  }
90
- if (lastTriggerFocusable && ((_f = zen.document) === null || _f === void 0 ? void 0 : _f.activeElement) === lastTriggerFocusable) {
65
+ if (lastTriggerFocusable && ((_e = zen.document) === null || _e === void 0 ? void 0 : _e.activeElement) === lastTriggerFocusable) {
91
66
  e.preventDefault();
92
67
  firstContainerFocusable === null || firstContainerFocusable === void 0 ? void 0 : firstContainerFocusable.focus();
93
68
  }
94
- else if (isLastChildOfFirst && first && isChildOf((_g = zen.document) === null || _g === void 0 ? void 0 : _g.activeElement, first)) {
69
+ else if (isLastChildOfFirst && first && isChildOf((_f = zen.document) === null || _f === void 0 ? void 0 : _f.activeElement, first)) {
95
70
  e.preventDefault();
96
71
  }
97
72
  return;
98
73
  }
99
- },
100
- // condition is consumed by useKeydown below, not read by handleKeyDown directly; its absence from deps is intentional.
101
- [containerRef, triggerRef, isElementVisible]);
74
+ }, [containerRef, triggerRef, isElementVisible]);
102
75
  useKeydown(handleKeyDown, subscriptionTrigger, condition);
103
- // When withTrigger mode closes (condition goes false), restore focus to the trigger
104
- // if focus landed on body (which happens when the popup's focused element is unmounted).
105
- // This restoration is intentionally scoped to the "focused element unmounted → body" case;
106
- // the general close case (e.g. click-outside, item selection) is handled separately by the
107
- // returnFocusToTrigger effect in absolute.tsx. Both are idempotent, so overlap is harmless.
108
- // Initialized to false because popups mount closed; the guard below reconciles the first
109
- // active/inactive edge, so a popup that mounts already open still resolves correctly.
110
- const isActiveRef = useRef(false);
111
- useEffect(() => {
112
- var _a, _b;
113
- const isActive = condition ? condition(subscriptionTrigger) : !!subscriptionTrigger;
114
- // Guard: short-circuit when the raw open/close state is unchanged to avoid spurious
115
- // focus restoration if an unstable condition reference re-fires this effect.
116
- if (!!subscriptionTrigger === isActiveRef.current) {
117
- isActiveRef.current = isActive;
118
- return;
119
- }
120
- if (!isActive && isActiveRef.current && (triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current)) {
121
- const active = (_a = zen.document) === null || _a === void 0 ? void 0 : _a.activeElement;
122
- if (!active || active === ((_b = zen.document) === null || _b === void 0 ? void 0 : _b.body)) {
123
- triggerRef.current.focus();
124
- }
125
- }
126
- isActiveRef.current = isActive;
127
- }, [condition, subscriptionTrigger, triggerRef]);
128
76
  };
@@ -1,4 +1,4 @@
1
- import { FC, PropsWithChildren, ReactElement } from "react";
1
+ import { FC, MouseEvent as ReactMouseEvent, PropsWithChildren, ReactElement } from "react";
2
2
  import { IZenIdComponentProps } from "../../../commonHelpers/zenComponent";
3
3
  import "./checkboxColumn.less";
4
4
  interface ICheckboxCellProps extends IZenIdComponentProps {
@@ -8,6 +8,7 @@ interface ICheckboxCellProps extends IZenIdComponentProps {
8
8
  disabled?: boolean;
9
9
  children?: ReactElement<typeof CheckboxCell.Content>;
10
10
  onChange?: (id: string, checked: boolean) => void;
11
+ onMouseDown?: (id: string, checked: boolean, e: ReactMouseEvent) => void;
11
12
  }
12
13
  interface ICheckboxCell extends FC<ICheckboxCellProps> {
13
14
  Content: FC<PropsWithChildren>;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Checkbox } from "../../../checkbox/checkbox";
3
3
  import { classNames } from "../../../commonHelpers/classNames/classNames";
4
- export const CheckboxCell = ({ id, checked, indeterminate, label, disabled, className, onChange, children }) => (_jsxs("div", { className: classNames(["zen-checkbox-column", className || ""]), children: [_jsx(Checkbox, { className: "zen-checkbox-column__checkbox", checked: checked, indeterminate: indeterminate, disabled: disabled, title: label, onChange: e => onChange && onChange(id, e.target.checked), wrapped: true }), children] }));
4
+ export const CheckboxCell = ({ id, checked, indeterminate, label, disabled, className, onChange, onMouseDown, children }) => (_jsxs("div", { className: classNames(["zen-checkbox-column", className || ""]), onMouseDown: onMouseDown ? e => onMouseDown(id, checked, e) : undefined, children: [_jsx(Checkbox, { className: "zen-checkbox-column__checkbox", checked: checked, indeterminate: indeterminate, disabled: disabled, title: label, onChange: e => onChange && onChange(id, e.target.checked), wrapped: true }), children] }));
5
5
  CheckboxCell.Content = function content({ children }) {
6
6
  return _jsx("div", { className: "zen-checkbox-column__content", children: children });
7
7
  };
@@ -1,4 +1,4 @@
1
- import { ReactNode, RefObject } from "react";
1
+ import { MouseEvent as ReactMouseEvent, ReactNode, RefObject } from "react";
2
2
  import { IListColumnWrapper } from "../../listColumn";
3
3
  import "./checkboxColumn.less";
4
4
  import { SimpleEventHandler } from "../../../commonHelpers/simpleEventHandler";
@@ -17,7 +17,8 @@ export interface ICheckboxColumnWrapper<T extends object> extends IListColumnWra
17
17
  attach(event: "toggle", handler: (state: [Event, HTMLButtonElement]) => void): this;
18
18
  attach(event: "change", handler: (state: [string, boolean]) => void): this;
19
19
  attach(event: "change-all", handler: (state: [boolean]) => void): this;
20
- detach(event: "change" | "toggle" | "change-all", handler?: (state: unknown) => void): this;
20
+ attach(event: "mousedown", handler: (state: [string, boolean, ReactMouseEvent]) => void): this;
21
+ detach(event: "change" | "toggle" | "change-all" | "mousedown", handler?: (state: unknown) => void): this;
21
22
  }
22
23
  export declare enum SelectionState {
23
24
  All = 0,
@@ -34,6 +35,7 @@ export declare class CheckboxColumnWrapper<T extends IEntityWithId> extends Simp
34
35
  private state;
35
36
  private readonly actionsClickHandler;
36
37
  private readonly changeHandler;
38
+ private readonly mouseDownHandler;
37
39
  private readonly changeHandlerAll;
38
40
  constructor(selections: Set<string>, options: ICheckboxColumnOptions<T>, menuTriggerRef: RefObject<HTMLButtonElement | null>);
39
41
  get selectionState(): SelectionState;
@@ -64,6 +64,15 @@ export class CheckboxColumnWrapper extends SimpleEventHandler {
64
64
  this.fire("change", [id, checked]);
65
65
  }
66
66
  });
67
+ // Only fired for enabled checkboxes, so a disabled row can never become a drag anchor.
68
+ Object.defineProperty(this, "mouseDownHandler", {
69
+ enumerable: true,
70
+ configurable: true,
71
+ writable: true,
72
+ value: (id, checked, e) => {
73
+ this.fire("mousedown", [id, checked, e]);
74
+ }
75
+ });
67
76
  Object.defineProperty(this, "changeHandlerAll", {
68
77
  enumerable: true,
69
78
  configurable: true,
@@ -89,7 +98,7 @@ export class CheckboxColumnWrapper extends SimpleEventHandler {
89
98
  }
90
99
  const allowed = this.condition(entity);
91
100
  const isDisable = this.disabled ? this.disabled(entity) : this.state === SelectionState.All;
92
- const cell = (_jsx(CheckboxCell, { id: entity.id, className: allowed ? "" : "zen-checkbox-column--no-checkbox", checked: this.state === SelectionState.All || this.selections.has(entity.id), indeterminate: false, disabled: isDisable, label: this.options.createTitle(entity), onChange: this.changeHandler, children: _jsx(CheckboxCell.Content, { children: value }) }));
101
+ const cell = (_jsx(CheckboxCell, { id: entity.id, className: allowed ? "" : "zen-checkbox-column--no-checkbox", checked: this.state === SelectionState.All || this.selections.has(entity.id), indeterminate: false, disabled: isDisable, label: this.options.createTitle(entity), onChange: this.changeHandler, onMouseDown: isDisable ? undefined : this.mouseDownHandler, children: _jsx(CheckboxCell.Content, { children: value }) }));
93
102
  return cell;
94
103
  }
95
104
  renderHeader(title) {
@@ -57,7 +57,8 @@ const Row = ({ id, entity, columns, visibleRows, flexibleColumnWidth, isLastRow,
57
57
  return acc;
58
58
  }, { cells: [], colspan: 0 }).cells, [columns, expanded, getCellParams, isVisible, onExpandedChange, isActive, active, onClick]);
59
59
  const cells = useMemo(() => renderCells(entity, false), [entity, renderCells]);
60
- const nestedCells = useMemo(() => (expanded && entity.children ? entity.children : []).map(nestedEntity => renderCells(nestedEntity, true)), [entity.children, expanded, renderCells]);
61
- return (_jsxs(_Fragment, { children: [_jsx("tr", { "data-row-id": id, id: mainRowId, role: "row", "aria-level": isTreeGrid ? 1 : undefined, "aria-posinset": isTreeGrid ? ariaPosinset : undefined, "aria-setsize": isTreeGrid ? ariaSetsize : undefined, "aria-expanded": isTreeGrid ? expanded : undefined, className: classNames(["zen-data-grid__row", className || "", isLastRow ? "zen-data-grid__row--last" : ""]), children: cells }, mainRowId), nestedCells.map((nestedRow, index) => (_jsx("tr", { id: `${mainRowId}_child_${index}`, role: "row", "aria-level": 2, "aria-posinset": index + 1, "aria-setsize": nestedCells.length, className: "zen-data-grid__row zen-data-grid__row--nested", children: nestedRow }, `${mainRowId}_child_${index}`)))] }));
60
+ const nestedEntities = useMemo(() => (expanded && entity.children ? entity.children : []), [entity.children, expanded]);
61
+ const nestedCells = useMemo(() => nestedEntities.map(nestedEntity => renderCells(nestedEntity, true)), [nestedEntities, renderCells]);
62
+ return (_jsxs(_Fragment, { children: [_jsx("tr", { "data-row-id": id, id: mainRowId, role: "row", "aria-level": isTreeGrid ? 1 : undefined, "aria-posinset": isTreeGrid ? ariaPosinset : undefined, "aria-setsize": isTreeGrid ? ariaSetsize : undefined, "aria-expanded": isTreeGrid ? expanded : undefined, className: classNames(["zen-data-grid__row", className || "", isLastRow ? "zen-data-grid__row--last" : ""]), children: cells }, mainRowId), nestedCells.map((nestedRow, index) => (_jsx("tr", { "data-row-id": nestedEntities[index].id, id: `${mainRowId}_child_${index}`, role: "row", "aria-level": 2, "aria-posinset": index + 1, "aria-setsize": nestedCells.length, className: "zen-data-grid__row zen-data-grid__row--nested", children: nestedRow }, `${mainRowId}_child_${index}`)))] }));
62
63
  };
63
64
  export const DataGridRow = memo(Row);
@@ -7,6 +7,8 @@ import { SelectList } from "../../selectList/selectList";
7
7
  import { CheckboxState } from "../../checkbox/checkboxState";
8
8
  import { SelectableRowsColumnWrapper } from "./columns/selectableRowsColumnWrapper";
9
9
  import { useLanguage } from "../../utils/localization/useLanguage";
10
+ import { useDragSelection } from "../../table/selectable/useDragSelection";
11
+ import { getDragSelectionRows } from "../../table/selectable/utils/getDragSelectionRows";
10
12
  injectString("cs", "Select all", "Vybrat v\u0161e");
11
13
  injectString("da-DK", "Select all", "V\xE6lg alle");
12
14
  injectString("de", "Select all", "Alle ausw\xE4hlen");
@@ -200,13 +202,29 @@ export const withSelectableRows = DataGridComponent => {
200
202
  const selectMenuRef = useRef(null);
201
203
  const selections = getSelections(selection);
202
204
  const allSelected = selection !== undefined && selection.allSelected;
203
- const wrapper = useMemo(() => selectMode ? new SelectableRowsColumnWrapper(selections, {
204
- createTitle: options.createTitle,
205
- headerTitle: options.headerTitle,
206
- condition: options.condition || options.checkboxVisible,
207
- disabled: options.checkboxDisabled ? options.checkboxDisabled : entity => options.selectionLimit !== undefined ? !selections.has(entity.id) && selections.size >= options.selectionLimit : false,
208
- header: options.selectionLimit === undefined && options.checkboxDisabled === undefined && options.checkboxVisible === undefined && options.condition === undefined || options.checkboxInHeader === true && !options.selectionLimit
209
- }, selectMenuRef) : null, [selectMode, selections, options.createTitle, options.headerTitle, options.condition, options.checkboxVisible, options.checkboxDisabled, options.selectionLimit, options.checkboxInHeader]);
205
+ const isCheckboxVisible = useCallback(entity => {
206
+ const condition = options.condition || options.checkboxVisible;
207
+ return condition ? condition(entity) : true;
208
+ }, [options.condition, options.checkboxVisible]);
209
+ const isCheckboxDisabled = useCallback(entity => {
210
+ if (options.checkboxDisabled) {
211
+ return options.checkboxDisabled(entity);
212
+ }
213
+ if (options.selectionLimit !== undefined) {
214
+ return !selections.has(entity.id) && selections.size >= options.selectionLimit;
215
+ }
216
+ return false;
217
+ }, [options, selections]);
218
+ const wrapper = useMemo(() => {
219
+ var _a;
220
+ return selectMode ? new SelectableRowsColumnWrapper(selections, {
221
+ createTitle: options.createTitle,
222
+ headerTitle: options.headerTitle,
223
+ condition: options.condition || options.checkboxVisible,
224
+ disabled: (_a = options.checkboxDisabled) !== null && _a !== void 0 ? _a : isCheckboxDisabled,
225
+ header: options.selectionLimit === undefined && options.checkboxDisabled === undefined && options.checkboxVisible === undefined && options.condition === undefined || options.checkboxInHeader === true && !options.selectionLimit
226
+ }, selectMenuRef) : null;
227
+ }, [selectMode, selections, options.createTitle, options.headerTitle, options.condition, options.checkboxVisible, options.checkboxDisabled, options.selectionLimit, options.checkboxInHeader, isCheckboxDisabled]);
210
228
  if (wrapper) {
211
229
  wrapper.selectionState = calculateSelectionState(entities, selections, allSelected, options);
212
230
  }
@@ -214,9 +232,10 @@ export const withSelectableRows = DataGridComponent => {
214
232
  const newSelections = new Set(selections);
215
233
  entities.forEach(entity => {
216
234
  if (checked) {
217
- const isVisible = options.condition ? options.condition(entity) : options.checkboxVisible ? options.checkboxVisible(entity) : true;
235
+ // Intentionally not isCheckboxDisabled: "select all" ignores the selection
236
+ // limit, it only skips rows the consumer explicitly disabled.
218
237
  const isEnabled = !options.checkboxDisabled || !options.checkboxDisabled(entity);
219
- if (isVisible && isEnabled) {
238
+ if (isCheckboxVisible(entity) && isEnabled) {
220
239
  newSelections.add(entity.id);
221
240
  }
222
241
  return;
@@ -229,8 +248,49 @@ export const withSelectableRows = DataGridComponent => {
229
248
  allSelected: newState === SelectionState.All,
230
249
  turnOffSelectAll: selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll
231
250
  });
232
- }, [selections, entities, options, onSelect, selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll]);
251
+ }, [selections, entities, options, onSelect, selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll, isCheckboxVisible]);
252
+ const {
253
+ rows: dragRows,
254
+ entityById
255
+ } = useMemo(() => getDragSelectionRows(entities), [entities]);
256
+ const isRowSelectable = useCallback(row => {
257
+ const entity = entityById.get(row.id);
258
+ return !!entity && isCheckboxVisible(entity) && !isCheckboxDisabled(entity);
259
+ }, [entityById, isCheckboxVisible, isCheckboxDisabled]);
260
+ // "Select all" is held as a flag rather than a list of ids, so materialise the page before
261
+ // a range starts editing it — otherwise the range would be applied to an empty selection.
262
+ const getBaseSelection = useCallback(() => allSelected ? new Set(entities.map(entity => entity.id)) : new Set(selections), [allSelected, entities, selections]);
263
+ const onBatchSelect = useCallback((rangeIds, select, base) => {
264
+ const result = new Set(base);
265
+ const limit = options.selectionLimit;
266
+ for (const id of rangeIds) {
267
+ if (select) {
268
+ if (limit !== undefined && result.size >= limit) {
269
+ break;
270
+ }
271
+ result.add(id);
272
+ } else {
273
+ result.delete(id);
274
+ }
275
+ }
276
+ const newState = calculateSelectionState(entities, result, false, options);
277
+ onSelect({
278
+ selected: [...result],
279
+ allSelected: newState === SelectionState.All,
280
+ turnOffSelectAll: selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll
281
+ });
282
+ }, [entities, options, onSelect, selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll]);
283
+ const {
284
+ onCheckboxMouseDown,
285
+ suppressNextChangeRef
286
+ } = useDragSelection(dragRows, props.ref, onBatchSelect, getBaseSelection, isRowSelectable, selectMode);
233
287
  const onSelectListener = useCallback(([id, checked]) => {
288
+ // A drag or shift-click has already emitted the whole range; swallow the click that
289
+ // ends it so the row it landed on is not toggled back.
290
+ if (suppressNextChangeRef.current) {
291
+ suppressNextChangeRef.current = false;
292
+ return;
293
+ }
234
294
  if (allSelected) {
235
295
  const newSelection = new Set(entities.map(entity => entity.id));
236
296
  newSelection.delete(id);
@@ -252,7 +312,7 @@ export const withSelectableRows = DataGridComponent => {
252
312
  allSelected: newState === SelectionState.All,
253
313
  turnOffSelectAll: selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll
254
314
  });
255
- }, [entities, selections, allSelected, options, onSelect, selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll]);
315
+ }, [entities, selections, allSelected, options, onSelect, selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll, suppressNextChangeRef]);
256
316
  const onSelectAll = useCallback(([checked]) => {
257
317
  if (checked && ((selection === null || selection === void 0 ? void 0 : selection.turnOffSelectAll) || options.turnOffSelectAll)) {
258
318
  selectAll(true);
@@ -301,10 +361,11 @@ export const withSelectableRows = DataGridComponent => {
301
361
  wrapper.attach("change", onSelectListener);
302
362
  wrapper.attach("change-all", onSelectAll);
303
363
  wrapper.attach("toggle", onTriggerClick);
364
+ wrapper.attach("mousedown", ([id, checked, e]) => onCheckboxMouseDown(id, checked, e));
304
365
  return () => {
305
366
  wrapper.detach();
306
367
  };
307
- }, [wrapper, onSelectListener, onSelectAll, onTriggerClick]);
368
+ }, [wrapper, onSelectListener, onSelectAll, onTriggerClick, onCheckboxMouseDown]);
308
369
  const listColumns = useMemo(() => {
309
370
  const newColumns = columns.map(c => Object.assign({}, c));
310
371
  // Remove CheckboxColumnWrappers from wrappers list
@@ -5,19 +5,15 @@ export declare enum SelectionState {
5
5
  None = "none"
6
6
  }
7
7
  export interface ISelectionValue {
8
- /**
9
- * Whether every selectable row across every page is selected.
10
- */
11
8
  all: boolean;
12
- /**
13
- * The meaning of this list depends on `all`:
14
- * - when `all` is `false`, it is the ids explicitly *included* in the selection.
15
- * - when `all` is `true`, it is the ids explicitly *excluded* from the otherwise
16
- * fully-selected set (e.g. rows unchecked after a "select all" across paginated data).
17
- *
18
- * This lets `all: true` represent "all except these ids" without needing to know the
19
- * full id universe (MYG-142234) - unchecking a row while `all` is true only ever adds
20
- * to this exclusion list; it must never be rebuilt from the currently rendered page.
21
- */
22
9
  selected: string[];
23
10
  }
11
+ /**
12
+ * One checkbox row in the order it is rendered. Expanded nested rows are part of this list,
13
+ * directly after their parent, so a drag or shift-click range can span nesting levels the
14
+ * same way it spans sibling rows.
15
+ */
16
+ export interface IDragSelectionRow {
17
+ id: string;
18
+ isNested: boolean;
19
+ }
@@ -0,0 +1,10 @@
1
+ import { MouseEvent as ReactMouseEvent, RefObject } from "react";
2
+ import { IDragSelectionRow } from "./interfaces";
3
+ /**
4
+ * @param rows every checkbox row in render order, expanded nested rows included, so ranges can
5
+ * span nesting levels.
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) => {
8
+ onCheckboxMouseDown: (id: string, checked: boolean, e: ReactMouseEvent) => void;
9
+ suppressNextChangeRef: RefObject<boolean>;
10
+ };
@@ -0,0 +1,135 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { zen } from "../../utils/zen";
3
+ import { getRangeIds } from "./utils/getRangeIds";
4
+ const DRAG_CLASS = "zen-not-selectable";
5
+ /**
6
+ * @param rows every checkbox row in render order, expanded nested rows included, so ranges can
7
+ * span nesting levels.
8
+ */
9
+ export const useDragSelection = (rows, gridRef, onBatchSelect, getBaseSelection, isSelectable, isEnabled) => {
10
+ const rowsRef = useRef(rows);
11
+ rowsRef.current = rows;
12
+ const getBaseSelectionRef = useRef(getBaseSelection);
13
+ getBaseSelectionRef.current = getBaseSelection;
14
+ const onBatchSelectRef = useRef(onBatchSelect);
15
+ onBatchSelectRef.current = onBatchSelect;
16
+ const isSelectableRef = useRef(isSelectable);
17
+ isSelectableRef.current = isSelectable;
18
+ const isEnabledRef = useRef(isEnabled);
19
+ isEnabledRef.current = isEnabled;
20
+ const anchorIdRef = useRef(null);
21
+ const anchorSelectRef = useRef(true);
22
+ const baseSelectedRef = useRef(new Set());
23
+ const isDraggingRef = useRef(false);
24
+ const movedRef = useRef(false);
25
+ const lastResultKeyRef = useRef("");
26
+ const lastPointRef = useRef(null);
27
+ const suppressNextChangeRef = useRef(false);
28
+ const attachedGridElementRef = useRef(null);
29
+ const emitRange = useCallback((currentId) => {
30
+ const anchorId = anchorIdRef.current;
31
+ if (anchorId === null) {
32
+ return;
33
+ }
34
+ const range = getRangeIds(rowsRef.current, anchorId, currentId, isSelectableRef.current);
35
+ if (range.length === 0) {
36
+ return;
37
+ }
38
+ const select = anchorSelectRef.current;
39
+ const key = `${select}|${range.join(",")}`;
40
+ if (key === lastResultKeyRef.current) {
41
+ return;
42
+ }
43
+ lastResultKeyRef.current = key;
44
+ onBatchSelectRef.current(range, select, baseSelectedRef.current);
45
+ }, []);
46
+ const extend = useCallback((x, y) => {
47
+ var _a, _b;
48
+ lastPointRef.current = { x, y };
49
+ const row = (_b = (_a = zen.document) === null || _a === void 0 ? void 0 : _a.elementFromPoint(x, y)) === null || _b === void 0 ? void 0 : _b.closest("[data-row-id]");
50
+ const currentId = row === null || row === void 0 ? void 0 : row.getAttribute("data-row-id");
51
+ if (!currentId) {
52
+ return;
53
+ }
54
+ if (currentId !== anchorIdRef.current) {
55
+ movedRef.current = true;
56
+ }
57
+ if (!movedRef.current) {
58
+ return;
59
+ }
60
+ emitRange(currentId);
61
+ }, [emitRange]);
62
+ const onMouseMove = useCallback((e) => {
63
+ extend(e.clientX, e.clientY);
64
+ }, [extend]);
65
+ const onScroll = useCallback(() => {
66
+ const point = lastPointRef.current;
67
+ if (point) {
68
+ extend(point.x, point.y);
69
+ }
70
+ }, [extend]);
71
+ const endDrag = useCallback(() => {
72
+ var _a, _b, _c, _d;
73
+ if (!isDraggingRef.current) {
74
+ return;
75
+ }
76
+ isDraggingRef.current = false;
77
+ if (movedRef.current) {
78
+ suppressNextChangeRef.current = true;
79
+ }
80
+ (_a = zen.document) === null || _a === void 0 ? void 0 : _a.removeEventListener("mousemove", onMouseMove);
81
+ (_b = zen.document) === null || _b === void 0 ? void 0 : _b.removeEventListener("mouseup", endDrag);
82
+ (_c = attachedGridElementRef.current) === null || _c === void 0 ? void 0 : _c.removeEventListener("scroll", onScroll);
83
+ (_d = attachedGridElementRef.current) === null || _d === void 0 ? void 0 : _d.classList.remove(DRAG_CLASS);
84
+ attachedGridElementRef.current = null;
85
+ }, [onMouseMove, onScroll]); // eslint-disable-line react-hooks/exhaustive-deps
86
+ const onCheckboxMouseDown = useCallback((id, checked, e) => {
87
+ var _a, _b, _c, _d, _e;
88
+ // Right-click between drag-end and the trailing click must not clear suppression.
89
+ if (e.button === 0) {
90
+ suppressNextChangeRef.current = false;
91
+ }
92
+ if (!isEnabledRef.current || e.button !== 0) {
93
+ return;
94
+ }
95
+ if (e.shiftKey && anchorIdRef.current !== null) {
96
+ e.preventDefault();
97
+ const anchorId = anchorIdRef.current;
98
+ const anchorInList = rowsRef.current.some(row => row.id === anchorId);
99
+ if (!anchorInList) {
100
+ // The anchor is no longer rendered (e.g. its parent was collapsed, or the list was
101
+ // re-filtered/paginated since). Drop the stale anchor and let the target row's own
102
+ // onChange fire normally, rather than swallowing it as a silent no-op.
103
+ anchorIdRef.current = null;
104
+ return;
105
+ }
106
+ // Gmail-style: every shift-click re-applies the range to the selection captured when
107
+ // the anchor was clicked, so moving the target closer to the anchor deselects the rows
108
+ // that dropped out of the range. Shift-clicking the anchor itself collapses the range
109
+ // to nothing, restoring that captured selection — the anchor included.
110
+ const range = id === anchorId ? [] : getRangeIds(rowsRef.current, anchorId, id, isSelectableRef.current);
111
+ // Suppress even when the range is empty to prevent the anchor from double-toggling
112
+ // on a same-row shift-click.
113
+ suppressNextChangeRef.current = true;
114
+ onBatchSelectRef.current(range, anchorSelectRef.current, baseSelectedRef.current);
115
+ return;
116
+ }
117
+ anchorIdRef.current = id;
118
+ anchorSelectRef.current = !checked;
119
+ baseSelectedRef.current = getBaseSelectionRef.current();
120
+ isDraggingRef.current = true;
121
+ movedRef.current = false;
122
+ lastResultKeyRef.current = "";
123
+ lastPointRef.current = { x: e.clientX, y: e.clientY };
124
+ attachedGridElementRef.current = (_a = gridRef === null || gridRef === void 0 ? void 0 : gridRef.current) !== null && _a !== void 0 ? _a : null;
125
+ (_b = attachedGridElementRef.current) === null || _b === void 0 ? void 0 : _b.classList.add(DRAG_CLASS);
126
+ (_c = zen.document) === null || _c === void 0 ? void 0 : _c.addEventListener("mousemove", onMouseMove);
127
+ (_d = zen.document) === null || _d === void 0 ? void 0 : _d.addEventListener("mouseup", endDrag);
128
+ (_e = attachedGridElementRef.current) === null || _e === void 0 ? void 0 : _e.addEventListener("scroll", onScroll);
129
+ }, [gridRef, onMouseMove, onScroll, endDrag]);
130
+ useEffect(() => {
131
+ anchorIdRef.current = null;
132
+ }, [rows]);
133
+ useEffect(() => endDrag, [endDrag]);
134
+ return { onCheckboxMouseDown, suppressNextChangeRef };
135
+ };
@@ -1,4 +1,4 @@
1
- import { ReactNode } from "react";
1
+ import { ReactNode, RefObject } from "react";
2
2
  import { IListColumn } from "../table";
3
3
  import { ISelectionValue, SelectionState } from "./interfaces";
4
4
  import "./selectableColumn.less";
@@ -33,7 +33,7 @@ export declare const getEmptySelection: () => {
33
33
  selected: string[];
34
34
  };
35
35
  export declare const SelectableWrapperName = "Selectable";
36
- export declare const useSelectableRows: <T extends IRowEntity<N>, N extends IEntityWithId>(columns: IListColumn<T, N>[], entities: T[], isMobile: boolean, selectMode: boolean, setSelectMode: (value: boolean) => void, selectable?: ISelectableRowsOptions<T, N>, isInfinite?: boolean) => {
36
+ export declare const useSelectableRows: <T extends IRowEntity<N>, N extends IEntityWithId>(columns: IListColumn<T, N>[], entities: T[], isMobile: boolean, selectMode: boolean, setSelectMode: (value: boolean) => void, selectable?: ISelectableRowsOptions<T, N>, isInfinite?: boolean, gridRef?: RefObject<HTMLElement | null>, expandedRows?: string[]) => {
37
37
  selectableColumns: IListColumn<T, N>[];
38
38
  selectableFeedWrapper: (primaryData: ReactNode, secondaryData: ReactNode, entity: T | N, isNested: boolean) => [ReactNode, ReactNode];
39
39
  };