@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.
- package/README.md +15 -12
- package/dist/absolute/absolute.js +1 -1
- package/dist/chart/plugins/linePlugin/getInterpolatedValue.d.ts +2 -2
- package/dist/chart/plugins/linePlugin/getInterpolatedValue.js +3 -2
- package/dist/chart/plugins/linePlugin/linePlugin.js +2 -1
- package/dist/chart/utils/getFormattedLabel.js +5 -1
- package/dist/checkbox/checkbox.d.ts +2 -1
- package/dist/checkbox/checkbox.js +2 -2
- package/dist/checkboxListWithAction/checkboxListWithAction.js +1 -0
- package/dist/commonHelpers/hooks/useTrapFocus.d.ts +0 -9
- package/dist/commonHelpers/hooks/useTrapFocus.js +14 -66
- package/dist/dataGrid/columns/checkboxColumn/checkboxCell.d.ts +2 -1
- package/dist/dataGrid/columns/checkboxColumn/checkboxCell.js +1 -1
- package/dist/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.d.ts +4 -2
- package/dist/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.js +10 -1
- package/dist/dataGrid/row/row.js +3 -2
- package/dist/dataGrid/withSelectableRows/withSelectableRows.js +73 -12
- package/dist/index.css +1 -1
- package/dist/table/selectable/interfaces.d.ts +9 -13
- package/dist/table/selectable/useDragSelection.d.ts +10 -0
- package/dist/table/selectable/useDragSelection.js +139 -0
- package/dist/table/selectable/useSelectableRows.d.ts +2 -2
- package/dist/table/selectable/useSelectableRows.js +59 -49
- package/dist/table/selectable/utils/calculateSelectionState.js +0 -35
- package/dist/table/selectable/utils/getDragSelectionRows.d.ts +15 -0
- package/dist/table/selectable/utils/getDragSelectionRows.js +28 -0
- package/dist/table/selectable/utils/getRangeIds.d.ts +2 -0
- package/dist/table/selectable/utils/getRangeIds.js +21 -0
- package/dist/table/selectable/utils/isRowChecked.js +1 -12
- package/dist/table/table.js +1 -1
- package/esm/absolute/absolute.js +1 -1
- package/esm/chart/plugins/linePlugin/getInterpolatedValue.d.ts +2 -2
- package/esm/chart/plugins/linePlugin/getInterpolatedValue.js +3 -2
- package/esm/chart/plugins/linePlugin/linePlugin.js +2 -1
- package/esm/chart/utils/getFormattedLabel.js +5 -1
- package/esm/checkbox/checkbox.d.ts +2 -1
- package/esm/checkbox/checkbox.js +2 -2
- package/esm/checkboxListWithAction/checkboxListWithAction.js +1 -0
- package/esm/commonHelpers/hooks/useTrapFocus.d.ts +0 -9
- package/esm/commonHelpers/hooks/useTrapFocus.js +15 -67
- package/esm/dataGrid/columns/checkboxColumn/checkboxCell.d.ts +2 -1
- package/esm/dataGrid/columns/checkboxColumn/checkboxCell.js +1 -1
- package/esm/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.d.ts +4 -2
- package/esm/dataGrid/columns/checkboxColumn/checkboxColumnWrapper.js +10 -1
- package/esm/dataGrid/row/row.js +3 -2
- package/esm/dataGrid/withSelectableRows/withSelectableRows.js +73 -12
- package/esm/table/selectable/interfaces.d.ts +9 -13
- package/esm/table/selectable/useDragSelection.d.ts +10 -0
- package/esm/table/selectable/useDragSelection.js +135 -0
- package/esm/table/selectable/useSelectableRows.d.ts +2 -2
- package/esm/table/selectable/useSelectableRows.js +59 -49
- package/esm/table/selectable/utils/calculateSelectionState.js +0 -35
- package/esm/table/selectable/utils/getDragSelectionRows.d.ts +15 -0
- package/esm/table/selectable/utils/getDragSelectionRows.js +24 -0
- package/esm/table/selectable/utils/getRangeIds.d.ts +2 -0
- package/esm/table/selectable/utils/getRangeIds.js +17 -0
- package/esm/table/selectable/utils/isRowChecked.js +1 -12
- package/esm/table/table.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback
|
|
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
|
|
12
|
+
var _a, _b, _c, _d, _e, _f;
|
|
22
13
|
/* eslint-enable complexity */
|
|
23
14
|
const getChildCondition = () => {
|
|
24
|
-
var _a, _b
|
|
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(
|
|
38
|
-
|
|
39
|
-
!isChildOf(
|
|
40
|
-
: !isChildOf(
|
|
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
|
|
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 (((
|
|
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 && ((
|
|
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 (((
|
|
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 && ((
|
|
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((
|
|
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
|
-
|
|
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) {
|
package/esm/dataGrid/row/row.js
CHANGED
|
@@ -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
|
|
61
|
-
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
};
|