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

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 (31) 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/index.css +1 -1
  13. package/dist/table/selectable/interfaces.d.ts +0 -13
  14. package/dist/table/selectable/useSelectableRows.js +12 -46
  15. package/dist/table/selectable/utils/calculateSelectionState.js +0 -35
  16. package/dist/table/selectable/utils/isRowChecked.js +1 -12
  17. package/esm/absolute/absolute.js +1 -1
  18. package/esm/chart/plugins/linePlugin/getInterpolatedValue.d.ts +2 -2
  19. package/esm/chart/plugins/linePlugin/getInterpolatedValue.js +3 -2
  20. package/esm/chart/plugins/linePlugin/linePlugin.js +2 -1
  21. package/esm/chart/utils/getFormattedLabel.js +5 -1
  22. package/esm/checkbox/checkbox.d.ts +2 -1
  23. package/esm/checkbox/checkbox.js +2 -2
  24. package/esm/checkboxListWithAction/checkboxListWithAction.js +1 -0
  25. package/esm/commonHelpers/hooks/useTrapFocus.d.ts +0 -9
  26. package/esm/commonHelpers/hooks/useTrapFocus.js +15 -67
  27. package/esm/table/selectable/interfaces.d.ts +0 -13
  28. package/esm/table/selectable/useSelectableRows.js +12 -46
  29. package/esm/table/selectable/utils/calculateSelectionState.js +0 -35
  30. package/esm/table/selectable/utils/isRowChecked.js +1 -12
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -49,28 +49,31 @@ Zenith library provides components defined in Zenith Design System. It includes
49
49
 
50
50
  ### 3.16.0
51
51
 
52
- - Add `Tooltip.Inline` subcomponent for inline tooltip triggers
52
+ - Add `Tooltip.InlineTrigger` subcomponent for inline tooltip triggers
53
+ - Add `TrendIndicator` component, the `getTrendIndicatorDirection` helper, and `Table.TrendIndicator`
53
54
  - Add popup button variant for `Nav` items
54
55
  - Add integer-only mode prop to `Range` / `FilterBar.Range`
55
- - Add Arabic language support
56
56
  - Add experimental feature-flag infrastructure (`ZenithExperimentalProvider`, `useExperimentalFeature`)
57
57
  - Expose `Tag` and `ProgressIndicator` as `Table` sub-components (`Table.Tag`, `Table.ProgressIndicator`)
58
- - Complete RTL logical-direction support: CSS logical modifiers, `TAlignment` deprecated in favour of logical direction values, controlled `Popup` migration
59
- - Export `getTrendIndicatorDirection` helper
60
- - Add support for six additional languages
61
- - Fix cross-page selection in selectable tables clearing when a row is unchecked after select-all
58
+ - Add `MODAL_SCROLL_LOCK_CLASS` export to control scroll-lock release timing when a `Modal` closes
59
+ - Rename the Arabic locale from `ar` to `ar-001`; `ar` and any `ar-*` value still resolve to it
60
+ - Migrate component alignment defaults to logical direction values — `ControlledPopup` now defaults to `bottom-start` and `Tooltip` to `end`
61
+ - Complete RTL logical-direction support: `ControlledPopup` migration and `PillExpandable` alignment flipping
62
+ - Remove `isBannerTarget` from the side panel overlay target checks
63
+ - Apply `dataAnalyticsId` to nested `PillExpandable` child pill triggers
64
+ - Resolve 20 npm audit vulnerabilities in Zenith dependencies
65
+ - Add an `ariaLabel` prop to `Checkbox` and accessible names to `Dropdown` multi-select options
62
66
  - Fix multiline `Banner` header spacing when there is no close button
63
67
  - Fix scrolling in `Tabs` and `FiltersBar`
64
- - Fix `Tabs` fading and side panel slide direction in RTL layouts
68
+ - Fix `Tabs` fading in RTL layouts
69
+ - Fix side panel slide direction in RTL layouts
65
70
  - Fix pills and chips scrolling the page to a blank area in RTL
66
- - Fix icons ignoring their component's colour
67
71
  - Fix mobile header overflow and `ReportsPopup` missing from the kebab menu in non-English languages
68
- - Move `Tabs` :focus-visible outline outside the tab to distinguish it from the active state
72
+ - Distinguish the `Tabs` focus ring from the active tab, and move focus to a tab only on user-initiated changes
69
73
  - Fix `BetaPill` synthesising a Send feedback button when `feedbackAction` is undefined
70
- - Migrate component alignment defaults to logical direction values
71
- - Fix Storybook brand URL to resolve based on the environment
74
+ - Fix `Nav` mobile search autofocusing when the search box is closed
72
75
  - Fix `AutoFill` not selecting the chosen option on `DropdownRaw`
73
- - Resolve 20 npm audit vulnerabilities in Zenith dependencies
76
+ - Fix `LineChart` tooltip interpolation and accessible chart dates when a custom x-axis parser is used
74
77
 
75
78
  ### 3.15.0
76
79
 
@@ -111,7 +111,7 @@ const Absolute = ({ isOpen, id, paddingX, paddingY, className, children, trigger
111
111
  returnFocusToTrigger();
112
112
  }
113
113
  }, [isOpen, returnFocusToTrigger]);
114
- const condition = (0, react_1.useCallback)((subscriptionTrigger) => useTrapFocusWithTrigger !== "off" && !!subscriptionTrigger, [useTrapFocusWithTrigger]);
114
+ const condition = (0, react_1.useCallback)(() => (useTrapFocusWithTrigger === "off" ? false : true), [useTrapFocusWithTrigger]);
115
115
  (0, useTrapFocus_1.useTrapFocus)(popupRef, useTrapFocusWithTrigger === "withTrigger" ? triggerRef : undefined, isOpen, condition);
116
116
  (0, useEscape_1.useEscape)(popupRef, handleEscape, isOpen);
117
117
  (0, useArrowKey_1.useArrowKey)(popupRef, handleEscape, isOpen);
@@ -1,3 +1,3 @@
1
- import { Scale } from "chart.js";
1
+ import { Scale, type ChartOptions } from "chart.js";
2
2
  import { ILineChartData } from "../../lineChart/interfaces";
3
- export declare function getInterpolatedValue(xScale: Scale, data: ILineChartData, datasetIndex: number, xPixel: number): number | null;
3
+ export declare function getInterpolatedValue(xScale: Scale, data: ILineChartData, datasetIndex: number, xPixel: number, parsing?: ChartOptions<"line">["parsing"]): number | null;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getInterpolatedValue = getInterpolatedValue;
4
- function getInterpolatedValue(xScale, data, datasetIndex, xPixel) {
4
+ function getInterpolatedValue(xScale, data, datasetIndex, xPixel, parsing) {
5
5
  const dataset = data.datasets[datasetIndex];
6
6
  const dataPoints = dataset.data;
7
7
  if (dataPoints.length < 2) {
@@ -18,7 +18,8 @@ function getInterpolatedValue(xScale, data, datasetIndex, xPixel) {
18
18
  if (y === null) {
19
19
  continue;
20
20
  }
21
- const pointXPixel = xScale.getPixelForValue(x);
21
+ const parsedX = parsing === false ? x : xScale.parse(x, i);
22
+ const pointXPixel = xScale.getPixelForValue(parsedX);
22
23
  if (pointXPixel <= xPixel && pointXPixel > lowerXPixel) {
23
24
  lowerPoint = y;
24
25
  lowerXPixel = pointXPixel;
@@ -60,6 +60,7 @@ const LinePlugin = (containerId, options, isDark) => ({
60
60
  let lowestValueY = Infinity;
61
61
  const items = datasets
62
62
  .map((dataset, dsIndex) => {
63
+ var _a;
63
64
  if (!chart.isDatasetVisible(dsIndex)) {
64
65
  return undefined;
65
66
  }
@@ -67,7 +68,7 @@ const LinePlugin = (containerId, options, isDark) => ({
67
68
  return undefined;
68
69
  }
69
70
  const xScale = chart.scales.x;
70
- const value = (0, getInterpolatedValue_1.getInterpolatedValue)(xScale, chart.data, dsIndex, x);
71
+ const value = (0, getInterpolatedValue_1.getInterpolatedValue)(xScale, chart.data, dsIndex, x, (_a = dataset.parsing) !== null && _a !== void 0 ? _a : chart.options.parsing);
71
72
  const yAxisID = dataset.yAxisID || "y";
72
73
  const yScale = chart.scales[yAxisID];
73
74
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -6,14 +6,18 @@ function getFormattedLabel(value, dateFormatter, options) {
6
6
  var _a, _b, _c;
7
7
  const scaleType = (_b = (_a = options === null || options === void 0 ? void 0 : options.scales) === null || _a === void 0 ? void 0 : _a.x) === null || _b === void 0 ? void 0 : _b.type;
8
8
  let format;
9
+ let parser;
9
10
  if (((_c = options === null || options === void 0 ? void 0 : options.scales) === null || _c === void 0 ? void 0 : _c.x) && scaleType) {
10
11
  const scaleFormat = options.scales.x[scaleType];
11
12
  if ((scaleFormat === null || scaleFormat === void 0 ? void 0 : scaleFormat.unit) && scaleFormat.displayFormats && scaleFormat.displayFormats[scaleFormat.unit]) {
12
13
  format = scaleFormat.displayFormats[scaleFormat.unit];
13
14
  }
15
+ if (options.parsing !== false && scaleFormat && "parser" in scaleFormat && typeof scaleFormat.parser === "function") {
16
+ parser = scaleFormat.parser;
17
+ }
14
18
  }
15
19
  if (value instanceof Date || (typeof value === "string" && (0, isDateString_1.isDateString)(value))) {
16
- return dateFormatter(value, format);
20
+ return dateFormatter(parser ? new Date(parser(value)) : value, format);
17
21
  }
18
22
  return String(value);
19
23
  }
@@ -8,6 +8,7 @@ export interface ICheckbox extends IZenComponentProps {
8
8
  checked?: boolean;
9
9
  indeterminate?: boolean;
10
10
  title?: string;
11
+ ariaLabel?: string;
11
12
  disabled?: boolean;
12
13
  fullWidth?: boolean;
13
14
  reverse?: boolean;
@@ -16,4 +17,4 @@ export interface ICheckbox extends IZenComponentProps {
16
17
  tabIndex?: number;
17
18
  describedBy?: string;
18
19
  }
19
- export declare const Checkbox: ({ reverse, fullWidth, id, checked, indeterminate, title, className, children, disabled, wrapped, describedBy, ...rest }: ICheckbox) => import("react/jsx-runtime").JSX.Element;
20
+ export declare const Checkbox: ({ reverse, fullWidth, id, checked, indeterminate, title, ariaLabel, className, children, disabled, wrapped, describedBy, ...rest }: ICheckbox) => import("react/jsx-runtime").JSX.Element;
@@ -56,14 +56,14 @@ const checkboxIconCheckmark_1 = require("./checkboxIconCheckmark");
56
56
  const checkboxIconMinus_1 = require("./checkboxIconMinus");
57
57
  const useMobile_1 = require("../commonHelpers/hooks/useMobile");
58
58
  const Checkbox = (_a) => {
59
- var { reverse, fullWidth, id, checked, indeterminate, title, className = "", children, disabled, wrapped, describedBy } = _a, rest = __rest(_a, ["reverse", "fullWidth", "id", "checked", "indeterminate", "title", "className", "children", "disabled", "wrapped", "describedBy"]);
59
+ var { reverse, fullWidth, id, checked, indeterminate, title, ariaLabel, className = "", children, disabled, wrapped, describedBy } = _a, rest = __rest(_a, ["reverse", "fullWidth", "id", "checked", "indeterminate", "title", "ariaLabel", "className", "children", "disabled", "wrapped", "describedBy"]);
60
60
  const state = (0, checkboxUtils_1.getCheckboxState)(checked, indeterminate);
61
61
  const generatedId = react_1.default.useId();
62
62
  const intId = id || generatedId;
63
63
  const driveClasses = (0, useDriveClassName_1.useDriveClassName)("zen-checkbox");
64
64
  const isMobile = (0, useMobile_1.useMobile)();
65
65
  const checkboxIconClasses = (0, react_1.useMemo)(() => (0, classNames_1.classNames)(["zen-checkbox__icon", disabled ? "zen-checkbox__icon--disabled" : ""]), [disabled]);
66
- const checkbox = ((0, jsx_runtime_1.jsx)(triStateCheckbox_1.TriStateCheckbox, Object.assign({ className: "zen-checkbox__input", id: intId, checked: checked, indeterminate: indeterminate, disabled: disabled, "aria-label": title, "aria-describedby": describedBy }, rest)));
66
+ const checkbox = ((0, jsx_runtime_1.jsx)(triStateCheckbox_1.TriStateCheckbox, Object.assign({ className: "zen-checkbox__input", id: intId, checked: checked, indeterminate: indeterminate, disabled: disabled, "aria-label": ariaLabel || title, "aria-describedby": describedBy }, rest)));
67
67
  const iconAndLabel = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: "zen-checkbox__box", "aria-hidden": true, children: state === checkboxState_1.CheckboxState.Indeterminate ? ((0, jsx_runtime_1.jsx)(checkboxIconMinus_1.CheckboxIconMinus, { className: checkboxIconClasses, size: driveClasses ? "bigger" : "medium" })) : state === checkboxState_1.CheckboxState.On ? ((0, jsx_runtime_1.jsx)(checkboxIconCheckmark_1.CheckboxIconCheckmark, { className: checkboxIconClasses, size: driveClasses ? "bigger" : "medium" })) : null }), children ? ((0, jsx_runtime_1.jsx)("div", { className: (0, classNames_1.classNames)(["zen-checkbox__label-text", fullWidth ? "zen-checkbox__label-text--full-width" : ""]), children: children })) : null] }));
68
68
  if (wrapped) {
69
69
  return ((0, jsx_runtime_1.jsxs)("label", { className: (0, classNames_1.classNames)(["zen-checkbox", driveClasses || "", className]), title: title, children: [checkbox, (0, jsx_runtime_1.jsx)("div", { className: (0, classNames_1.classNames)(["zen-checkbox__label", reverse ? "zen-checkbox__label--reverse" : ""]), children: iconAndLabel })] }));
@@ -453,6 +453,7 @@ const CheckboxListWithAction = ({
453
453
  checked: opt.checked,
454
454
  disabled: opt.blocked,
455
455
  title: translate("Toggle {groupName} filter").replace("{groupName}", titleForRender),
456
+ ariaLabel: titleForRender,
456
457
  describedBy: opt.isWithAction ? navigationInstructionsId : undefined,
457
458
  "data-indeterminate": opt.partialChecked,
458
459
  tabIndex: getTabIndex(ind, "checkbox", opt.blocked),
@@ -1,13 +1,4 @@
1
1
  import { RefObject } from "react";
2
2
  type TElementToBool = (element: HTMLElement) => boolean;
3
- /**
4
- * Traps keyboard (Tab) focus within `containerRef` (and optionally its `triggerRef`).
5
- *
6
- * Nested-popup contract: for correct nested-popup behaviour, a focused element that triggers an
7
- * open child popup must expose a `data-popup-id` attribute whose value is the `id` of the child
8
- * popup element rendered in the DOM. When present, focus is deferred to that child popup's trap.
9
- * This contract is fulfilled automatically by `ControlledPopup`; callers using `useTrapFocus`
10
- * directly must set `data-popup-id` themselves or nested popups will silently lose focus handling.
11
- */
12
3
  export declare const useTrapFocus: (containerRef: RefObject<HTMLElement | null>, triggerRef: RefObject<HTMLElement | null> | undefined, subscriptionTrigger: unknown, condition?: (t: unknown) => boolean, isElementVisible?: TElementToBool) => void;
13
4
  export {};
@@ -9,38 +9,18 @@ const isChildOf_1 = require("../../utils/isChildOf");
9
9
  const isElementHidden_1 = require("../../utils/isElementHidden");
10
10
  const zen_1 = require("../../utils/zen");
11
11
  const findFirstVisible = (elems, isElementVisible) => elems.find(elem => isElementVisible(elem));
12
- /**
13
- * Traps keyboard (Tab) focus within `containerRef` (and optionally its `triggerRef`).
14
- *
15
- * Nested-popup contract: for correct nested-popup behaviour, a focused element that triggers an
16
- * open child popup must expose a `data-popup-id` attribute whose value is the `id` of the child
17
- * popup element rendered in the DOM. When present, focus is deferred to that child popup's trap.
18
- * This contract is fulfilled automatically by `ControlledPopup`; callers using `useTrapFocus`
19
- * directly must set `data-popup-id` themselves or nested popups will silently lose focus handling.
20
- */
21
12
  const useTrapFocus = (containerRef, triggerRef, subscriptionTrigger, condition, isElementVisible) => {
22
13
  /* eslint-disable complexity */
23
14
  const handleKeyDown = (0, react_1.useCallback)(e => {
24
- var _a, _b, _c, _d, _e, _f, _g;
15
+ var _a, _b, _c, _d, _e, _f;
25
16
  /* eslint-enable complexity */
26
17
  const getChildCondition = () => {
27
- var _a, _b, _c;
28
- const targetEl = e.target;
29
- // If the target is a trigger for an open child popup, defer to that popup's
30
- // focus trap. Key on data-popup-id + DOM presence so this works even when
31
- // preventAttributesAutoSet suppresses aria-expanded.
32
- const dataPopupId = targetEl.getAttribute("data-popup-id");
33
- if (dataPopupId &&
34
- ((_a = zen_1.zen.document) === null || _a === void 0 ? void 0 : _a.getElementById(dataPopupId)) &&
35
- (0, isChildOf_1.isChildOf)(targetEl, containerRef.current) &&
36
- targetEl !== (triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current)) {
37
- return true;
38
- }
18
+ var _a, _b;
39
19
  return triggerRef
40
- ? !(0, isChildOf_1.isChildOf)(targetEl, containerRef.current) &&
41
- targetEl !== ((_b = zen_1.zen.document) === null || _b === void 0 ? void 0 : _b.body) &&
42
- !(0, isChildOf_1.isChildOf)(targetEl, triggerRef.current)
43
- : !(0, isChildOf_1.isChildOf)(targetEl, containerRef.current) && targetEl !== ((_c = zen_1.zen.document) === null || _c === void 0 ? void 0 : _c.body);
20
+ ? !(0, isChildOf_1.isChildOf)(e.target, containerRef.current) &&
21
+ e.target !== ((_a = zen_1.zen.document) === null || _a === void 0 ? void 0 : _a.body) &&
22
+ !(0, isChildOf_1.isChildOf)(e.target, triggerRef.current)
23
+ : !(0, isChildOf_1.isChildOf)(e.target, containerRef.current) && e.target !== ((_b = zen_1.zen.document) === null || _b === void 0 ? void 0 : _b.body);
44
24
  };
45
25
  if (!containerRef.current || getChildCondition()) {
46
26
  return;
@@ -50,14 +30,9 @@ const useTrapFocus = (containerRef, triggerRef, subscriptionTrigger, condition,
50
30
  : !(0, dialogHelpers_1.isElementInsideContainer)(target, containerEl.current) &&
51
31
  !(0, dialogHelpers_1.isElementInsideContainer)(target, triggerEl.current);
52
32
  const isTargetOutside = e.target ? checkIsElementOutside(e.target, containerRef, triggerRef) : undefined;
53
- const triggerElement = (_a = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current) !== null && _a !== void 0 ? _a : null;
54
- const triggerSelf = (triggerElement === null || triggerElement === void 0 ? void 0 : triggerElement.matches(focusableSelector_1.FOCUSABLE_SELECTOR)) ? [triggerElement] : [];
55
- const triggerChildren = Array.from((_b = triggerElement === null || triggerElement === void 0 ? void 0 : triggerElement.querySelectorAll(focusableSelector_1.FOCUSABLE_SELECTOR)) !== null && _b !== void 0 ? _b : []);
56
- // querySelectorAll only returns descendants (never the element itself), so triggerSelf
57
- // and triggerChildren are disjoint by definition and need no deduplication.
58
- const triggerSelectors = [...triggerSelf, ...triggerChildren];
33
+ const triggerSelectors = ((_a = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(focusableSelector_1.FOCUSABLE_SELECTOR)) || [];
59
34
  const containerSelectors = containerRef.current.querySelectorAll(focusableSelector_1.FOCUSABLE_SELECTOR);
60
- const focusable = triggerRef ? [...triggerSelectors, ...Array.from(containerSelectors)] : [...Array.from(containerSelectors)];
35
+ const focusable = triggerRef ? [...Array.from(triggerSelectors), ...Array.from(containerSelectors)] : [...Array.from(containerSelectors)];
61
36
  if (!focusable.length) {
62
37
  return;
63
38
  }
@@ -74,59 +49,32 @@ const useTrapFocus = (containerRef, triggerRef, subscriptionTrigger, condition,
74
49
  return;
75
50
  }
76
51
  if (e.shiftKey) {
77
- if (((_c = zen_1.zen.document) === null || _c === void 0 ? void 0 : _c.activeElement) === first) {
52
+ if (((_b = zen_1.zen.document) === null || _b === void 0 ? void 0 : _b.activeElement) === first) {
78
53
  e.preventDefault();
79
54
  last === null || last === void 0 ? void 0 : last.focus();
80
55
  return;
81
56
  }
82
- if (firstContainerFocusable && ((_d = zen_1.zen.document) === null || _d === void 0 ? void 0 : _d.activeElement) === firstContainerFocusable) {
57
+ if (firstContainerFocusable && ((_c = zen_1.zen.document) === null || _c === void 0 ? void 0 : _c.activeElement) === firstContainerFocusable) {
83
58
  e.preventDefault();
84
59
  lastTriggerFocusable === null || lastTriggerFocusable === void 0 ? void 0 : lastTriggerFocusable.focus();
85
60
  }
86
61
  return;
87
62
  }
88
- if (((_e = zen_1.zen.document) === null || _e === void 0 ? void 0 : _e.activeElement) === last) {
63
+ if (((_d = zen_1.zen.document) === null || _d === void 0 ? void 0 : _d.activeElement) === last) {
89
64
  e.preventDefault();
90
65
  first === null || first === void 0 ? void 0 : first.focus();
91
66
  return;
92
67
  }
93
- if (lastTriggerFocusable && ((_f = zen_1.zen.document) === null || _f === void 0 ? void 0 : _f.activeElement) === lastTriggerFocusable) {
68
+ if (lastTriggerFocusable && ((_e = zen_1.zen.document) === null || _e === void 0 ? void 0 : _e.activeElement) === lastTriggerFocusable) {
94
69
  e.preventDefault();
95
70
  firstContainerFocusable === null || firstContainerFocusable === void 0 ? void 0 : firstContainerFocusable.focus();
96
71
  }
97
- else if (isLastChildOfFirst && first && (0, isChildOf_1.isChildOf)((_g = zen_1.zen.document) === null || _g === void 0 ? void 0 : _g.activeElement, first)) {
72
+ else if (isLastChildOfFirst && first && (0, isChildOf_1.isChildOf)((_f = zen_1.zen.document) === null || _f === void 0 ? void 0 : _f.activeElement, first)) {
98
73
  e.preventDefault();
99
74
  }
100
75
  return;
101
76
  }
102
- },
103
- // condition is consumed by useKeydown below, not read by handleKeyDown directly; its absence from deps is intentional.
104
- [containerRef, triggerRef, isElementVisible]);
77
+ }, [containerRef, triggerRef, isElementVisible]);
105
78
  (0, useKeydown_1.useKeydown)(handleKeyDown, subscriptionTrigger, condition);
106
- // When withTrigger mode closes (condition goes false), restore focus to the trigger
107
- // if focus landed on body (which happens when the popup's focused element is unmounted).
108
- // This restoration is intentionally scoped to the "focused element unmounted → body" case;
109
- // the general close case (e.g. click-outside, item selection) is handled separately by the
110
- // returnFocusToTrigger effect in absolute.tsx. Both are idempotent, so overlap is harmless.
111
- // Initialized to false because popups mount closed; the guard below reconciles the first
112
- // active/inactive edge, so a popup that mounts already open still resolves correctly.
113
- const isActiveRef = (0, react_1.useRef)(false);
114
- (0, react_1.useEffect)(() => {
115
- var _a, _b;
116
- const isActive = condition ? condition(subscriptionTrigger) : !!subscriptionTrigger;
117
- // Guard: short-circuit when the raw open/close state is unchanged to avoid spurious
118
- // focus restoration if an unstable condition reference re-fires this effect.
119
- if (!!subscriptionTrigger === isActiveRef.current) {
120
- isActiveRef.current = isActive;
121
- return;
122
- }
123
- if (!isActive && isActiveRef.current && (triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current)) {
124
- const active = (_a = zen_1.zen.document) === null || _a === void 0 ? void 0 : _a.activeElement;
125
- if (!active || active === ((_b = zen_1.zen.document) === null || _b === void 0 ? void 0 : _b.body)) {
126
- triggerRef.current.focus();
127
- }
128
- }
129
- isActiveRef.current = isActive;
130
- }, [condition, subscriptionTrigger, triggerRef]);
131
79
  };
132
80
  exports.useTrapFocus = useTrapFocus;
package/dist/index.css CHANGED
@@ -13083,7 +13083,7 @@ html:lang(ar) .zen-radio__wrapper--mobile .zen-radio__label {
13083
13083
  .zen-waiting--inline {
13084
13084
  position: relative;
13085
13085
  width: 2.5rem;
13086
- height: 1.5rem;
13086
+ height: 2.5rem;
13087
13087
  }
13088
13088
  .zen-waiting__spinner-text {
13089
13089
  font-family: Roboto, "Segoe UI", Segoe, "Helvetica Neue", Helvetica, sans-serif;
@@ -5,19 +5,6 @@ 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
  }
@@ -142,30 +142,19 @@ const useSelectableRows = (columns, entities, isMobile, selectMode, setSelectMod
142
142
  }
143
143
  }, [checkboxDisabled, checkboxVisible, entities, selectable, setSelectMode, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
144
144
  const getNewSelection = (0, react_1.useCallback)((id, checked) => {
145
- var _a;
146
- if ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) {
147
- // MYG-142234: `selections` holds the ids excluded from the "all" selection.
148
- // Toggling a row here must only ever add/remove that single id from the
149
- // exclusion set - it must never be rebuilt from `entities` (the current page),
150
- // otherwise every other page's rows would silently fall out of the selection.
151
- const excluded = new Set(selections);
152
- (0, checkRow_1.checkRow)(excluded, selectable, entities, id, !checked);
153
- return excluded;
145
+ if (state === interfaces_1.SelectionState.All) {
146
+ const newSelection = (0, getCurrentPageSelection_1.getCurrentPageSelection)(entities, selectable);
147
+ (0, checkRow_1.checkRow)(newSelection, selectable, entities, id, false);
148
+ return newSelection;
154
149
  }
155
150
  (0, checkRow_1.checkRow)(selections, selectable, entities, id, checked);
156
151
  return selections;
157
- }, [entities, selectable, selections]);
152
+ }, [state, entities, selectable, selections]);
158
153
  const onSelectListener = (0, react_1.useCallback)((id, checked) => {
159
154
  var _a;
160
- const wasAllSelected = ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) || false;
161
155
  const newSelection = getNewSelection(id, checked);
162
- const newState = (0, calculateSelectionState_1.calculateSelectionState)(entities, newSelection, wasAllSelected, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy);
163
- // MYG-142234: once selection spans "all" pages, unchecking a row must keep `all: true`
164
- // and only record the exclusion - it must not collapse to the current page's ids.
165
- const newValue = wasAllSelected ? {
166
- selected: [...newSelection],
167
- all: true
168
- } : {
156
+ const newState = (0, calculateSelectionState_1.calculateSelectionState)(entities, newSelection, ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) || false, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy);
157
+ const newValue = {
169
158
  selected: [...newSelection],
170
159
  all: newState === interfaces_1.SelectionState.All
171
160
  };
@@ -174,7 +163,7 @@ const useSelectableRows = (columns, entities, isMobile, selectMode, setSelectMod
174
163
  setSelectMode(newValue.all || newValue.selected.length > 0);
175
164
  }, [checkboxDisabled, checkboxVisible, entities, selectable, setSelectMode, setState, getNewSelection]);
176
165
  const onChangeSelectionState = (0, react_1.useCallback)(newState => {
177
- var _a, _b, _c;
166
+ var _a, _b;
178
167
  if (newState === interfaces_1.SelectionState.All) {
179
168
  (selectable === null || selectable === void 0 ? void 0 : selectable.onSelect) && selectable.onSelect({
180
169
  selected: [],
@@ -193,11 +182,7 @@ const useSelectableRows = (columns, entities, isMobile, selectMode, setSelectMod
193
182
  }
194
183
  const newSelections = (0, getCurrentPageSelection_1.getCurrentPageSelection)(entities, selectable);
195
184
  setState(newState);
196
- // MYG-142234: previous `selected` ids mean something different depending on `all` -
197
- // included ids when `all` is false, excluded ids when `all` is true. Switching to an
198
- // explicit "select current page" here always leaves `all: false`, so a previous
199
- // exclusion list must not be reinterpreted as an inclusion list.
200
- const previousSelections = ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) ? [] : (_c = (_b = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _b === void 0 ? void 0 : _b.selected) !== null && _c !== void 0 ? _c : [];
185
+ 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 : [];
201
186
  const mergedSelections = new Set([...previousSelections, ...newSelections]);
202
187
  const newValue = {
203
188
  selected: [...mergedSelections],
@@ -216,12 +201,7 @@ const useSelectableRows = (columns, entities, isMobile, selectMode, setSelectMod
216
201
  }
217
202
  return translate("Deselect all");
218
203
  }, [selectable, selectableState, state, translate]);
219
- const isEntityChecked = (0, react_1.useCallback)(
220
- // MYG-142234: while `selection.all` is true, `selections` holds excluded ids, and every
221
- // row must be evaluated through the SelectionState.All (exclusion-aware) branch of
222
- // isRowChecked - not the per-page `state`, which can be Partial/None for a page that
223
- // contains some of those exclusions.
224
- (entity, isNested) => (0, isRowChecked_1.isRowChecked)(selections, isAllSelected ? interfaces_1.SelectionState.All : state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, entity, isNested), [selections, isAllSelected, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
204
+ const isEntityChecked = (0, react_1.useCallback)((entity, isNested) => (0, isRowChecked_1.isRowChecked)(selections, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, entity, isNested), [selections, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
225
205
  const hasSelectable = !!selectable;
226
206
  const selectableCheckboxSelectsCurrentPage = selectable === null || selectable === void 0 ? void 0 : selectable.checkboxSelectsCurrentPage;
227
207
  const selectableTurnOffSelectAll = selectable === null || selectable === void 0 ? void 0 : selectable.turnOffSelectAll;
@@ -273,30 +253,16 @@ const useSelectableRows = (columns, entities, isMobile, selectMode, setSelectMod
273
253
  checked: state === interfaces_1.SelectionState.CurrentPage || state === interfaces_1.SelectionState.All,
274
254
  indeterminate: state === interfaces_1.SelectionState.Partial,
275
255
  onChange: newValue => {
276
- var _a, _b, _c;
256
+ var _a, _b;
277
257
  if (newValue) {
278
258
  const newState = checkboxEmitsCurrentPage ? interfaces_1.SelectionState.CurrentPage : interfaces_1.SelectionState.All;
279
259
  setState(newState);
280
260
  onChangeSelectionState(newState);
281
- } else if ((_a = selectable.selection) === null || _a === void 0 ? void 0 : _a.all) {
282
- // MYG-142234: in exclusion mode, `selected` holds excluded ids. Unchecking
283
- // the header must add the current page's ids to that exclusion set and
284
- // keep `all: true` - it must never be treated as an inclusion list, or
285
- // every other page's selection would be silently wiped out.
286
- const currentPageIds = (0, getCurrentPageSelection_1.getCurrentPageSelection)(entities, selectable);
287
- const newExcluded = new Set([...selectable.selection.selected, ...currentPageIds]);
288
- const newState = (0, calculateSelectionState_1.calculateSelectionState)(entities, newExcluded, true, checkboxVisible, checkboxDisabled, selectable.strategy);
289
- setState(newState);
290
- selectable.onSelect({
291
- selected: [...newExcluded],
292
- all: true
293
- });
294
- setSelectMode(true);
295
261
  } else {
296
262
  // Intentionally different from onSelect(None) ("Deselect all" menu item, which clears everything).
297
263
  // Unchecking the header checkbox only removes the current page from the selection, preserving other pages.
298
264
  const currentPageIds = (0, getCurrentPageSelection_1.getCurrentPageSelection)(entities, selectable);
299
- const remaining = ((_c = (_b = selectable.selection) === null || _b === void 0 ? void 0 : _b.selected) !== null && _c !== void 0 ? _c : []).filter(id => !currentPageIds.has(id));
265
+ const remaining = ((_b = (_a = selectable.selection) === null || _a === void 0 ? void 0 : _a.selected) !== null && _b !== void 0 ? _b : []).filter(id => !currentPageIds.has(id));
300
266
  const newState = remaining.length > 0 ? interfaces_1.SelectionState.Partial : interfaces_1.SelectionState.None;
301
267
  setState(newState);
302
268
  selectable.onSelect({
@@ -6,41 +6,6 @@ const calculateSelectionState = (entities, selections, all, checkboxVisible, che
6
6
  if (selections.size === 0) {
7
7
  return all ? interfaces_1.SelectionState.All : interfaces_1.SelectionState.None;
8
8
  }
9
- if (all) {
10
- // When `all` is true, `selections` holds the ids explicitly excluded from the
11
- // otherwise-fully-selected set (see ISelectionValue), rather than included ids.
12
- const isExcluded = (entity) => {
13
- if (strategy === "nested" && entity.children) {
14
- if (entity.children.length === 0) {
15
- return false;
16
- }
17
- return entity.children.every(child => {
18
- if (!checkboxVisible(child)) {
19
- return true;
20
- }
21
- if (checkboxDisabled(child)) {
22
- return true;
23
- }
24
- return selections.has(child.id);
25
- });
26
- }
27
- return selections.has(entity.id);
28
- };
29
- const isPartiallyExcluded = (entity) => {
30
- if (strategy === "nested" && entity.children) {
31
- return entity.children.some(child => selections.has(child.id));
32
- }
33
- return selections.has(entity.id);
34
- };
35
- const relevantEntities = entities.filter(entity => checkboxVisible(entity) && !checkboxDisabled(entity));
36
- if (relevantEntities.length === 0) {
37
- return interfaces_1.SelectionState.All;
38
- }
39
- if (relevantEntities.every(entity => !isPartiallyExcluded(entity))) {
40
- return interfaces_1.SelectionState.All;
41
- }
42
- return relevantEntities.every(entity => isExcluded(entity)) ? interfaces_1.SelectionState.None : interfaces_1.SelectionState.Partial;
43
- }
44
9
  return entities.every(entity => {
45
10
  if (!checkboxVisible(entity)) {
46
11
  return true;
@@ -4,18 +4,7 @@ exports.isRowChecked = void 0;
4
4
  const interfaces_1 = require("../interfaces");
5
5
  const isRowChecked = (selections, state, strategy, entity, isNested) => {
6
6
  if (state === interfaces_1.SelectionState.All) {
7
- // When the overall selection is "all", `selections` holds the ids that have been
8
- // explicitly excluded from that selection (see ISelectionValue), not included ids.
9
- if (isNested) {
10
- return { indeterminate: false, checked: !selections.has(entity.id) };
11
- }
12
- const mainEntity = entity;
13
- if (strategy === "nested" && mainEntity.children && mainEntity.children.length > 0) {
14
- const allChildrenChecked = mainEntity.children.every(child => !selections.has(child.id));
15
- const someChildrenChecked = mainEntity.children.some(child => !selections.has(child.id));
16
- return { indeterminate: someChildrenChecked && !allChildrenChecked, checked: allChildrenChecked };
17
- }
18
- return { indeterminate: false, checked: !selections.has(entity.id) };
7
+ return { indeterminate: false, checked: true };
19
8
  }
20
9
  if (isNested) {
21
10
  return { indeterminate: false, checked: selections.has(entity.id) };
@@ -108,7 +108,7 @@ export const Absolute = ({ isOpen, id, paddingX, paddingY, className, children,
108
108
  returnFocusToTrigger();
109
109
  }
110
110
  }, [isOpen, returnFocusToTrigger]);
111
- const condition = useCallback((subscriptionTrigger) => useTrapFocusWithTrigger !== "off" && !!subscriptionTrigger, [useTrapFocusWithTrigger]);
111
+ const condition = useCallback(() => (useTrapFocusWithTrigger === "off" ? false : true), [useTrapFocusWithTrigger]);
112
112
  useTrapFocus(popupRef, useTrapFocusWithTrigger === "withTrigger" ? triggerRef : undefined, isOpen, condition);
113
113
  useEscape(popupRef, handleEscape, isOpen);
114
114
  useArrowKey(popupRef, handleEscape, isOpen);
@@ -1,3 +1,3 @@
1
- import { Scale } from "chart.js";
1
+ import { Scale, type ChartOptions } from "chart.js";
2
2
  import { ILineChartData } from "../../lineChart/interfaces";
3
- export declare function getInterpolatedValue(xScale: Scale, data: ILineChartData, datasetIndex: number, xPixel: number): number | null;
3
+ export declare function getInterpolatedValue(xScale: Scale, data: ILineChartData, datasetIndex: number, xPixel: number, parsing?: ChartOptions<"line">["parsing"]): number | null;
@@ -1,4 +1,4 @@
1
- export function getInterpolatedValue(xScale, data, datasetIndex, xPixel) {
1
+ export function getInterpolatedValue(xScale, data, datasetIndex, xPixel, parsing) {
2
2
  const dataset = data.datasets[datasetIndex];
3
3
  const dataPoints = dataset.data;
4
4
  if (dataPoints.length < 2) {
@@ -15,7 +15,8 @@ export function getInterpolatedValue(xScale, data, datasetIndex, xPixel) {
15
15
  if (y === null) {
16
16
  continue;
17
17
  }
18
- const pointXPixel = xScale.getPixelForValue(x);
18
+ const parsedX = parsing === false ? x : xScale.parse(x, i);
19
+ const pointXPixel = xScale.getPixelForValue(parsedX);
19
20
  if (pointXPixel <= xPixel && pointXPixel > lowerXPixel) {
20
21
  lowerPoint = y;
21
22
  lowerXPixel = pointXPixel;
@@ -57,6 +57,7 @@ export const LinePlugin = (containerId, options, isDark) => ({
57
57
  let lowestValueY = Infinity;
58
58
  const items = datasets
59
59
  .map((dataset, dsIndex) => {
60
+ var _a;
60
61
  if (!chart.isDatasetVisible(dsIndex)) {
61
62
  return undefined;
62
63
  }
@@ -64,7 +65,7 @@ export const LinePlugin = (containerId, options, isDark) => ({
64
65
  return undefined;
65
66
  }
66
67
  const xScale = chart.scales.x;
67
- const value = getInterpolatedValue(xScale, chart.data, dsIndex, x);
68
+ const value = getInterpolatedValue(xScale, chart.data, dsIndex, x, (_a = dataset.parsing) !== null && _a !== void 0 ? _a : chart.options.parsing);
68
69
  const yAxisID = dataset.yAxisID || "y";
69
70
  const yScale = chart.scales[yAxisID];
70
71
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -3,14 +3,18 @@ export function getFormattedLabel(value, dateFormatter, options) {
3
3
  var _a, _b, _c;
4
4
  const scaleType = (_b = (_a = options === null || options === void 0 ? void 0 : options.scales) === null || _a === void 0 ? void 0 : _a.x) === null || _b === void 0 ? void 0 : _b.type;
5
5
  let format;
6
+ let parser;
6
7
  if (((_c = options === null || options === void 0 ? void 0 : options.scales) === null || _c === void 0 ? void 0 : _c.x) && scaleType) {
7
8
  const scaleFormat = options.scales.x[scaleType];
8
9
  if ((scaleFormat === null || scaleFormat === void 0 ? void 0 : scaleFormat.unit) && scaleFormat.displayFormats && scaleFormat.displayFormats[scaleFormat.unit]) {
9
10
  format = scaleFormat.displayFormats[scaleFormat.unit];
10
11
  }
12
+ if (options.parsing !== false && scaleFormat && "parser" in scaleFormat && typeof scaleFormat.parser === "function") {
13
+ parser = scaleFormat.parser;
14
+ }
11
15
  }
12
16
  if (value instanceof Date || (typeof value === "string" && isDateString(value))) {
13
- return dateFormatter(value, format);
17
+ return dateFormatter(parser ? new Date(parser(value)) : value, format);
14
18
  }
15
19
  return String(value);
16
20
  }
@@ -8,6 +8,7 @@ export interface ICheckbox extends IZenComponentProps {
8
8
  checked?: boolean;
9
9
  indeterminate?: boolean;
10
10
  title?: string;
11
+ ariaLabel?: string;
11
12
  disabled?: boolean;
12
13
  fullWidth?: boolean;
13
14
  reverse?: boolean;
@@ -16,4 +17,4 @@ export interface ICheckbox extends IZenComponentProps {
16
17
  tabIndex?: number;
17
18
  describedBy?: string;
18
19
  }
19
- export declare const Checkbox: ({ reverse, fullWidth, id, checked, indeterminate, title, className, children, disabled, wrapped, describedBy, ...rest }: ICheckbox) => import("react/jsx-runtime").JSX.Element;
20
+ export declare const Checkbox: ({ reverse, fullWidth, id, checked, indeterminate, title, ariaLabel, className, children, disabled, wrapped, describedBy, ...rest }: ICheckbox) => import("react/jsx-runtime").JSX.Element;
@@ -20,14 +20,14 @@ import { CheckboxIconCheckmark } from "./checkboxIconCheckmark";
20
20
  import { CheckboxIconMinus } from "./checkboxIconMinus";
21
21
  import { useMobile } from "../commonHelpers/hooks/useMobile";
22
22
  export const Checkbox = (_a) => {
23
- var { reverse, fullWidth, id, checked, indeterminate, title, className = "", children, disabled, wrapped, describedBy } = _a, rest = __rest(_a, ["reverse", "fullWidth", "id", "checked", "indeterminate", "title", "className", "children", "disabled", "wrapped", "describedBy"]);
23
+ var { reverse, fullWidth, id, checked, indeterminate, title, ariaLabel, className = "", children, disabled, wrapped, describedBy } = _a, rest = __rest(_a, ["reverse", "fullWidth", "id", "checked", "indeterminate", "title", "ariaLabel", "className", "children", "disabled", "wrapped", "describedBy"]);
24
24
  const state = getCheckboxState(checked, indeterminate);
25
25
  const generatedId = React.useId();
26
26
  const intId = id || generatedId;
27
27
  const driveClasses = useDriveClassName("zen-checkbox");
28
28
  const isMobile = useMobile();
29
29
  const checkboxIconClasses = useMemo(() => classNames(["zen-checkbox__icon", disabled ? "zen-checkbox__icon--disabled" : ""]), [disabled]);
30
- const checkbox = (_jsx(TriStateCheckbox, Object.assign({ className: "zen-checkbox__input", id: intId, checked: checked, indeterminate: indeterminate, disabled: disabled, "aria-label": title, "aria-describedby": describedBy }, rest)));
30
+ const checkbox = (_jsx(TriStateCheckbox, Object.assign({ className: "zen-checkbox__input", id: intId, checked: checked, indeterminate: indeterminate, disabled: disabled, "aria-label": ariaLabel || title, "aria-describedby": describedBy }, rest)));
31
31
  const iconAndLabel = (_jsxs(_Fragment, { children: [_jsx("div", { className: "zen-checkbox__box", "aria-hidden": true, children: state === CheckboxState.Indeterminate ? (_jsx(CheckboxIconMinus, { className: checkboxIconClasses, size: driveClasses ? "bigger" : "medium" })) : state === CheckboxState.On ? (_jsx(CheckboxIconCheckmark, { className: checkboxIconClasses, size: driveClasses ? "bigger" : "medium" })) : null }), children ? (_jsx("div", { className: classNames(["zen-checkbox__label-text", fullWidth ? "zen-checkbox__label-text--full-width" : ""]), children: children })) : null] }));
32
32
  if (wrapped) {
33
33
  return (_jsxs("label", { className: classNames(["zen-checkbox", driveClasses || "", className]), title: title, children: [checkbox, _jsx("div", { className: classNames(["zen-checkbox__label", reverse ? "zen-checkbox__label--reverse" : ""]), children: iconAndLabel })] }));
@@ -445,6 +445,7 @@ export const CheckboxListWithAction = ({
445
445
  checked: opt.checked,
446
446
  disabled: opt.blocked,
447
447
  title: translate("Toggle {groupName} filter").replace("{groupName}", titleForRender),
448
+ ariaLabel: titleForRender,
448
449
  describedBy: opt.isWithAction ? navigationInstructionsId : undefined,
449
450
  "data-indeterminate": opt.partialChecked,
450
451
  tabIndex: getTabIndex(ind, "checkbox", opt.blocked),
@@ -1,13 +1,4 @@
1
1
  import { RefObject } from "react";
2
2
  type TElementToBool = (element: HTMLElement) => boolean;
3
- /**
4
- * Traps keyboard (Tab) focus within `containerRef` (and optionally its `triggerRef`).
5
- *
6
- * Nested-popup contract: for correct nested-popup behaviour, a focused element that triggers an
7
- * open child popup must expose a `data-popup-id` attribute whose value is the `id` of the child
8
- * popup element rendered in the DOM. When present, focus is deferred to that child popup's trap.
9
- * This contract is fulfilled automatically by `ControlledPopup`; callers using `useTrapFocus`
10
- * directly must set `data-popup-id` themselves or nested popups will silently lose focus handling.
11
- */
12
3
  export declare const useTrapFocus: (containerRef: RefObject<HTMLElement | null>, triggerRef: RefObject<HTMLElement | null> | undefined, subscriptionTrigger: unknown, condition?: (t: unknown) => boolean, isElementVisible?: TElementToBool) => void;
13
4
  export {};
@@ -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
  };
@@ -5,19 +5,6 @@ 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
  }
@@ -133,30 +133,19 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
133
133
  }
134
134
  }, [checkboxDisabled, checkboxVisible, entities, selectable, setSelectMode, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
135
135
  const getNewSelection = useCallback((id, checked) => {
136
- var _a;
137
- if ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) {
138
- // MYG-142234: `selections` holds the ids excluded from the "all" selection.
139
- // Toggling a row here must only ever add/remove that single id from the
140
- // exclusion set - it must never be rebuilt from `entities` (the current page),
141
- // otherwise every other page's rows would silently fall out of the selection.
142
- const excluded = new Set(selections);
143
- checkRow(excluded, selectable, entities, id, !checked);
144
- return excluded;
136
+ if (state === SelectionState.All) {
137
+ const newSelection = getCurrentPageSelection(entities, selectable);
138
+ checkRow(newSelection, selectable, entities, id, false);
139
+ return newSelection;
145
140
  }
146
141
  checkRow(selections, selectable, entities, id, checked);
147
142
  return selections;
148
- }, [entities, selectable, selections]);
143
+ }, [state, entities, selectable, selections]);
149
144
  const onSelectListener = useCallback((id, checked) => {
150
145
  var _a;
151
- const wasAllSelected = ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) || false;
152
146
  const newSelection = getNewSelection(id, checked);
153
- const newState = calculateSelectionState(entities, newSelection, wasAllSelected, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy);
154
- // MYG-142234: once selection spans "all" pages, unchecking a row must keep `all: true`
155
- // and only record the exclusion - it must not collapse to the current page's ids.
156
- const newValue = wasAllSelected ? {
157
- selected: [...newSelection],
158
- all: true
159
- } : {
147
+ const newState = calculateSelectionState(entities, newSelection, ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) || false, checkboxVisible, checkboxDisabled, selectable === null || selectable === void 0 ? void 0 : selectable.strategy);
148
+ const newValue = {
160
149
  selected: [...newSelection],
161
150
  all: newState === SelectionState.All
162
151
  };
@@ -165,7 +154,7 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
165
154
  setSelectMode(newValue.all || newValue.selected.length > 0);
166
155
  }, [checkboxDisabled, checkboxVisible, entities, selectable, setSelectMode, setState, getNewSelection]);
167
156
  const onChangeSelectionState = useCallback(newState => {
168
- var _a, _b, _c;
157
+ var _a, _b;
169
158
  if (newState === SelectionState.All) {
170
159
  (selectable === null || selectable === void 0 ? void 0 : selectable.onSelect) && selectable.onSelect({
171
160
  selected: [],
@@ -184,11 +173,7 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
184
173
  }
185
174
  const newSelections = getCurrentPageSelection(entities, selectable);
186
175
  setState(newState);
187
- // MYG-142234: previous `selected` ids mean something different depending on `all` -
188
- // included ids when `all` is false, excluded ids when `all` is true. Switching to an
189
- // explicit "select current page" here always leaves `all: false`, so a previous
190
- // exclusion list must not be reinterpreted as an inclusion list.
191
- const previousSelections = ((_a = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _a === void 0 ? void 0 : _a.all) ? [] : (_c = (_b = selectable === null || selectable === void 0 ? void 0 : selectable.selection) === null || _b === void 0 ? void 0 : _b.selected) !== null && _c !== void 0 ? _c : [];
176
+ 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 : [];
192
177
  const mergedSelections = new Set([...previousSelections, ...newSelections]);
193
178
  const newValue = {
194
179
  selected: [...mergedSelections],
@@ -207,12 +192,7 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
207
192
  }
208
193
  return translate("Deselect all");
209
194
  }, [selectable, selectableState, state, translate]);
210
- const isEntityChecked = useCallback(
211
- // MYG-142234: while `selection.all` is true, `selections` holds excluded ids, and every
212
- // row must be evaluated through the SelectionState.All (exclusion-aware) branch of
213
- // isRowChecked - not the per-page `state`, which can be Partial/None for a page that
214
- // contains some of those exclusions.
215
- (entity, isNested) => isRowChecked(selections, isAllSelected ? SelectionState.All : state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, entity, isNested), [selections, isAllSelected, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
195
+ const isEntityChecked = useCallback((entity, isNested) => isRowChecked(selections, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy, entity, isNested), [selections, state, selectable === null || selectable === void 0 ? void 0 : selectable.strategy]);
216
196
  const hasSelectable = !!selectable;
217
197
  const selectableCheckboxSelectsCurrentPage = selectable === null || selectable === void 0 ? void 0 : selectable.checkboxSelectsCurrentPage;
218
198
  const selectableTurnOffSelectAll = selectable === null || selectable === void 0 ? void 0 : selectable.turnOffSelectAll;
@@ -264,30 +244,16 @@ export const useSelectableRows = (columns, entities, isMobile, selectMode, setSe
264
244
  checked: state === SelectionState.CurrentPage || state === SelectionState.All,
265
245
  indeterminate: state === SelectionState.Partial,
266
246
  onChange: newValue => {
267
- var _a, _b, _c;
247
+ var _a, _b;
268
248
  if (newValue) {
269
249
  const newState = checkboxEmitsCurrentPage ? SelectionState.CurrentPage : SelectionState.All;
270
250
  setState(newState);
271
251
  onChangeSelectionState(newState);
272
- } else if ((_a = selectable.selection) === null || _a === void 0 ? void 0 : _a.all) {
273
- // MYG-142234: in exclusion mode, `selected` holds excluded ids. Unchecking
274
- // the header must add the current page's ids to that exclusion set and
275
- // keep `all: true` - it must never be treated as an inclusion list, or
276
- // every other page's selection would be silently wiped out.
277
- const currentPageIds = getCurrentPageSelection(entities, selectable);
278
- const newExcluded = new Set([...selectable.selection.selected, ...currentPageIds]);
279
- const newState = calculateSelectionState(entities, newExcluded, true, checkboxVisible, checkboxDisabled, selectable.strategy);
280
- setState(newState);
281
- selectable.onSelect({
282
- selected: [...newExcluded],
283
- all: true
284
- });
285
- setSelectMode(true);
286
252
  } else {
287
253
  // Intentionally different from onSelect(None) ("Deselect all" menu item, which clears everything).
288
254
  // Unchecking the header checkbox only removes the current page from the selection, preserving other pages.
289
255
  const currentPageIds = getCurrentPageSelection(entities, selectable);
290
- const remaining = ((_c = (_b = selectable.selection) === null || _b === void 0 ? void 0 : _b.selected) !== null && _c !== void 0 ? _c : []).filter(id => !currentPageIds.has(id));
256
+ const remaining = ((_b = (_a = selectable.selection) === null || _a === void 0 ? void 0 : _a.selected) !== null && _b !== void 0 ? _b : []).filter(id => !currentPageIds.has(id));
291
257
  const newState = remaining.length > 0 ? SelectionState.Partial : SelectionState.None;
292
258
  setState(newState);
293
259
  selectable.onSelect({
@@ -3,41 +3,6 @@ export const calculateSelectionState = (entities, selections, all, checkboxVisib
3
3
  if (selections.size === 0) {
4
4
  return all ? SelectionState.All : SelectionState.None;
5
5
  }
6
- if (all) {
7
- // When `all` is true, `selections` holds the ids explicitly excluded from the
8
- // otherwise-fully-selected set (see ISelectionValue), rather than included ids.
9
- const isExcluded = (entity) => {
10
- if (strategy === "nested" && entity.children) {
11
- if (entity.children.length === 0) {
12
- return false;
13
- }
14
- return entity.children.every(child => {
15
- if (!checkboxVisible(child)) {
16
- return true;
17
- }
18
- if (checkboxDisabled(child)) {
19
- return true;
20
- }
21
- return selections.has(child.id);
22
- });
23
- }
24
- return selections.has(entity.id);
25
- };
26
- const isPartiallyExcluded = (entity) => {
27
- if (strategy === "nested" && entity.children) {
28
- return entity.children.some(child => selections.has(child.id));
29
- }
30
- return selections.has(entity.id);
31
- };
32
- const relevantEntities = entities.filter(entity => checkboxVisible(entity) && !checkboxDisabled(entity));
33
- if (relevantEntities.length === 0) {
34
- return SelectionState.All;
35
- }
36
- if (relevantEntities.every(entity => !isPartiallyExcluded(entity))) {
37
- return SelectionState.All;
38
- }
39
- return relevantEntities.every(entity => isExcluded(entity)) ? SelectionState.None : SelectionState.Partial;
40
- }
41
6
  return entities.every(entity => {
42
7
  if (!checkboxVisible(entity)) {
43
8
  return true;
@@ -1,18 +1,7 @@
1
1
  import { SelectionState } from "../interfaces";
2
2
  export const isRowChecked = (selections, state, strategy, entity, isNested) => {
3
3
  if (state === SelectionState.All) {
4
- // When the overall selection is "all", `selections` holds the ids that have been
5
- // explicitly excluded from that selection (see ISelectionValue), not included ids.
6
- if (isNested) {
7
- return { indeterminate: false, checked: !selections.has(entity.id) };
8
- }
9
- const mainEntity = entity;
10
- if (strategy === "nested" && mainEntity.children && mainEntity.children.length > 0) {
11
- const allChildrenChecked = mainEntity.children.every(child => !selections.has(child.id));
12
- const someChildrenChecked = mainEntity.children.some(child => !selections.has(child.id));
13
- return { indeterminate: someChildrenChecked && !allChildrenChecked, checked: allChildrenChecked };
14
- }
15
- return { indeterminate: false, checked: !selections.has(entity.id) };
4
+ return { indeterminate: false, checked: true };
16
5
  }
17
6
  if (isNested) {
18
7
  return { indeterminate: false, checked: selections.has(entity.id) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geotab/zenith",
3
- "version": "3.16.0-beta.2",
3
+ "version": "3.16.0-beta.4",
4
4
  "description": "Zenith components library on React",
5
5
  "main": "dist/index.js",
6
6
  "types": "esm/index.d.ts",