@geotab/zenith 3.7.0-beta.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +1 -0
  2. package/dist/checkbox/checkbox.d.ts +3 -1
  3. package/dist/checkbox/checkbox.js +5 -8
  4. package/dist/checkboxListWithAction/checkboxListWithAction.d.ts +3 -1
  5. package/dist/checkboxListWithAction/checkboxListWithAction.js +185 -3
  6. package/dist/dropdown/dropdownPopup.d.ts +1 -1
  7. package/dist/dropdown/dropdownTrigger.d.ts +1 -1
  8. package/dist/dropdownRaw/dropdownList.d.ts +2 -1
  9. package/dist/dropdownRaw/dropdownList.js +15 -4
  10. package/dist/dropdownRaw/dropdownPopup.d.ts +3 -1
  11. package/dist/dropdownRaw/dropdownPopup.js +35 -5
  12. package/dist/dropdownRaw/dropdownRaw.js +82 -7
  13. package/dist/dropdownRaw/dropdownSearchableTrigger.d.ts +2 -1
  14. package/dist/dropdownRaw/dropdownSearchableTrigger.js +7 -2
  15. package/dist/dropdownRaw/dropdownTrigger.d.ts +2 -1
  16. package/dist/dropdownRaw/dropdownTrigger.js +3 -5
  17. package/dist/footerButtons/footerButtons.d.ts +6 -4
  18. package/dist/footerButtons/footerButtons.js +3 -8
  19. package/dist/groupsFilterRaw/groupsFilterInitialState.js +16 -0
  20. package/dist/groupsFilterRaw/groupsFilterTrigger.js +4 -0
  21. package/dist/index.css +14 -0
  22. package/dist/nav/navItem/navItem.d.ts +1 -0
  23. package/dist/nav/navItem/navItem.js +4 -1
  24. package/dist/utils/localization/translations/cs.json +2 -1
  25. package/dist/utils/localization/translations/de.json +2 -1
  26. package/dist/utils/localization/translations/en.json +2 -1
  27. package/dist/utils/localization/translations/es.json +2 -1
  28. package/dist/utils/localization/translations/fr-FR.json +2 -1
  29. package/dist/utils/localization/translations/fr.json +2 -1
  30. package/dist/utils/localization/translations/id.json +2 -1
  31. package/dist/utils/localization/translations/it.json +2 -1
  32. package/dist/utils/localization/translations/ja.json +2 -1
  33. package/dist/utils/localization/translations/ms.json +2 -1
  34. package/dist/utils/localization/translations/nl.json +2 -1
  35. package/dist/utils/localization/translations/pl.json +2 -1
  36. package/dist/utils/localization/translations/pt-BR.json +2 -1
  37. package/dist/utils/localization/translations/sv.json +2 -1
  38. package/dist/utils/localization/translations/th.json +2 -1
  39. package/dist/utils/localization/translations/tr.json +2 -1
  40. package/dist/utils/localization/translations/zh-Hans.json +2 -1
  41. package/esm/checkbox/checkbox.d.ts +3 -1
  42. package/esm/checkbox/checkbox.js +5 -8
  43. package/esm/checkboxListWithAction/checkboxListWithAction.d.ts +3 -1
  44. package/esm/checkboxListWithAction/checkboxListWithAction.js +186 -4
  45. package/esm/dropdown/dropdownPopup.d.ts +1 -1
  46. package/esm/dropdown/dropdownTrigger.d.ts +1 -1
  47. package/esm/dropdownRaw/dropdownList.d.ts +2 -1
  48. package/esm/dropdownRaw/dropdownList.js +16 -5
  49. package/esm/dropdownRaw/dropdownPopup.d.ts +3 -1
  50. package/esm/dropdownRaw/dropdownPopup.js +37 -7
  51. package/esm/dropdownRaw/dropdownRaw.js +82 -7
  52. package/esm/dropdownRaw/dropdownSearchableTrigger.d.ts +2 -1
  53. package/esm/dropdownRaw/dropdownSearchableTrigger.js +7 -2
  54. package/esm/dropdownRaw/dropdownTrigger.d.ts +2 -1
  55. package/esm/dropdownRaw/dropdownTrigger.js +3 -5
  56. package/esm/footerButtons/footerButtons.d.ts +6 -4
  57. package/esm/footerButtons/footerButtons.js +3 -8
  58. package/esm/groupsFilterRaw/groupsFilterInitialState.js +16 -0
  59. package/esm/groupsFilterRaw/groupsFilterTrigger.js +4 -0
  60. package/esm/nav/navItem/navItem.d.ts +1 -0
  61. package/esm/nav/navItem/navItem.js +5 -2
  62. package/esm/utils/localization/translations/cs.json +2 -1
  63. package/esm/utils/localization/translations/de.json +2 -1
  64. package/esm/utils/localization/translations/en.json +2 -1
  65. package/esm/utils/localization/translations/es.json +2 -1
  66. package/esm/utils/localization/translations/fr-FR.json +2 -1
  67. package/esm/utils/localization/translations/fr.json +2 -1
  68. package/esm/utils/localization/translations/id.json +2 -1
  69. package/esm/utils/localization/translations/it.json +2 -1
  70. package/esm/utils/localization/translations/ja.json +2 -1
  71. package/esm/utils/localization/translations/ms.json +2 -1
  72. package/esm/utils/localization/translations/nl.json +2 -1
  73. package/esm/utils/localization/translations/pl.json +2 -1
  74. package/esm/utils/localization/translations/pt-BR.json +2 -1
  75. package/esm/utils/localization/translations/sv.json +2 -1
  76. package/esm/utils/localization/translations/th.json +2 -1
  77. package/esm/utils/localization/translations/tr.json +2 -1
  78. package/esm/utils/localization/translations/zh-Hans.json +2 -1
  79. package/package.json +1 -4
@@ -1,6 +1,6 @@
1
1
  import { injectString } from "../utils/localization/translationsDictionary";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Fragment, useId } from "react";
3
+ import { Fragment, useCallback, useEffect, useId, useRef, useState } from "react";
4
4
  import { Checkbox } from "../checkbox/checkbox";
5
5
  import { classNames } from "../commonHelpers/classNames/classNames";
6
6
  import { IconChevronRight } from "../icons/iconChevronRight";
@@ -84,13 +84,15 @@ injectString("tr", "Toggle {groupName} filter", "{groupName} filtresini de\u011F
84
84
  injectString("zh-Hans", "Toggle {groupName} filter", "\u5207\u6362 {groupName} \u7B5B\u9009\u5668");
85
85
  injectString("zh-TW", "Toggle {groupName} filter", "\u5207\u63DB\u300C{groupName}\u300D\u7BE9\u9078\u689D\u4EF6");
86
86
  injectString("ro-RO", "Toggle {groupName} filter", "Activa\u021Bi filtrul {groupName}");
87
+ injectString("en", "Has additional actions. Use right arrow to access button, left arrow to return.", "Has additional actions. Use right arrow to access button, left arrow to return.");
87
88
  export const CheckboxListWithAction = ({
88
89
  label,
89
90
  options,
90
91
  onChange,
91
92
  onClick,
92
93
  search = [],
93
- className
94
+ className,
95
+ handleTab
94
96
  }) => {
95
97
  const {
96
98
  translate
@@ -98,6 +100,11 @@ export const CheckboxListWithAction = ({
98
100
  const driveComponentClass = useDriveClassName("zen-checkbox-list-with-action");
99
101
  const listId = useId();
100
102
  const checkboxId = `${listId}_check_list-item_`;
103
+ const navigationInstructionsId = `${listId}_navigation_instructions`;
104
+ const listRef = useRef(null);
105
+ const [focusedRowIndex, setFocusedRowIndex] = useState(0);
106
+ const [focusedElementType, setFocusedElementType] = useState("checkbox");
107
+ const isInitialized = useRef(false);
101
108
  const handleChange = e => {
102
109
  const evtTarget = e.target;
103
110
  const elId = evtTarget.getAttribute("id").replace(checkboxId, "");
@@ -112,7 +119,171 @@ export const CheckboxListWithAction = ({
112
119
  const evtTarget = e.target;
113
120
  const elId = evtTarget.getAttribute("data-btn") || ((_a = evtTarget.parentElement) === null || _a === void 0 ? void 0 : _a.getAttribute("data-btn")) || "";
114
121
  onClick(elId);
122
+ setFocusedElementType("checkbox");
115
123
  };
124
+ const getFocusableElements = useCallback(() => {
125
+ if (!listRef.current || !options) return [];
126
+ const elements = [];
127
+ options.forEach((opt, rowIndex) => {
128
+ var _a;
129
+ const listItem = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(`li[data-item="${opt.property}"]`);
130
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
131
+ if (listItem) {
132
+ const checkbox = listItem.querySelector('input[type="checkbox"]');
133
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
134
+ if (checkbox) {
135
+ elements.push({
136
+ element: checkbox,
137
+ rowIndex,
138
+ type: "checkbox",
139
+ blocked: opt.blocked
140
+ });
141
+ }
142
+ if (opt.isWithAction) {
143
+ const button = listItem.querySelector("button[data-btn]");
144
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
145
+ if (button) {
146
+ elements.push({
147
+ element: button,
148
+ rowIndex,
149
+ type: "button",
150
+ blocked: opt.blocked
151
+ });
152
+ }
153
+ }
154
+ }
155
+ });
156
+ return elements;
157
+ }, [options]);
158
+ const updateTabIndex = useCallback((newRowIndex, newElementType) => {
159
+ const elements = getFocusableElements();
160
+ elements.forEach(({
161
+ element,
162
+ rowIndex,
163
+ type,
164
+ blocked
165
+ }) => {
166
+ const isActive = rowIndex === newRowIndex && type === newElementType && !blocked;
167
+ element.setAttribute("tabindex", isActive ? "0" : "-1");
168
+ });
169
+ }, [getFocusableElements]);
170
+ const handleKeyDown = useCallback(
171
+ // eslint-disable-next-line complexity
172
+ e => {
173
+ var _a;
174
+ const elements = getFocusableElements();
175
+ if (elements.length === 0 || !options) return;
176
+ let newRowIndex = focusedRowIndex;
177
+ let newElementType = focusedElementType;
178
+ let preventDefault = false;
179
+ if (e.key === "Enter") {
180
+ const evtTarget = e.target;
181
+ const elId = ((_a = evtTarget.getAttribute("id")) === null || _a === void 0 ? void 0 : _a.replace(checkboxId, "")) || "";
182
+ elId && onChange({
183
+ id: elId,
184
+ state: evtTarget.checked,
185
+ partial: evtTarget.dataset.indeterminate === "true"
186
+ });
187
+ return;
188
+ }
189
+ if (e.key === "ArrowDown") {
190
+ // Go to next row
191
+ do {
192
+ newRowIndex = (newRowIndex + 1) % options.length;
193
+ } while (options[newRowIndex].blocked && newRowIndex !== focusedRowIndex);
194
+ preventDefault = true;
195
+ } else if (e.key === "ArrowUp") {
196
+ // Go to previous row
197
+ do {
198
+ newRowIndex = (newRowIndex - 1 + options.length) % options.length;
199
+ } while (options[newRowIndex].blocked && newRowIndex !== focusedRowIndex);
200
+ preventDefault = true;
201
+ } else if (e.key === "ArrowRight") {
202
+ // Go to button if exists in current row and not blocked
203
+ if (options[focusedRowIndex].isWithAction && focusedElementType === "checkbox" && !options[focusedRowIndex].blocked) {
204
+ newElementType = "button";
205
+ preventDefault = true;
206
+ }
207
+ } else if (e.key === "ArrowLeft") {
208
+ // Go to checkbox if currently on button and checkbox not blocked
209
+ if (focusedElementType === "button" && !options[focusedRowIndex].blocked) {
210
+ newElementType = "checkbox";
211
+ preventDefault = true;
212
+ }
213
+ } else if (e.key === "Home") {
214
+ // Go to first non-blocked row
215
+ newRowIndex = 0;
216
+ while (newRowIndex < options.length && options[newRowIndex].blocked) {
217
+ newRowIndex++;
218
+ }
219
+ if (newRowIndex < options.length) {
220
+ newElementType = "checkbox";
221
+ preventDefault = true;
222
+ }
223
+ } else if (e.key === "End") {
224
+ // Go to last non-blocked row
225
+ newRowIndex = options.length - 1;
226
+ while (newRowIndex >= 0 && options[newRowIndex].blocked) {
227
+ newRowIndex--;
228
+ }
229
+ if (newRowIndex >= 0) {
230
+ newElementType = "checkbox";
231
+ preventDefault = true;
232
+ }
233
+ } else if (e.key === "Tab") {
234
+ if (handleTab) {
235
+ handleTab(e, e.shiftKey);
236
+ return;
237
+ }
238
+ }
239
+ if (preventDefault) {
240
+ e.preventDefault();
241
+ setFocusedRowIndex(newRowIndex);
242
+ setFocusedElementType(newElementType);
243
+ updateTabIndex(newRowIndex, newElementType);
244
+ // Find and focus the target element
245
+ const targetElement = elements.find(({
246
+ rowIndex,
247
+ type,
248
+ blocked
249
+ }) => rowIndex === newRowIndex && type === newElementType && !blocked);
250
+ targetElement === null || targetElement === void 0 ? void 0 : targetElement.element.focus();
251
+ }
252
+ }, [getFocusableElements, options, focusedRowIndex, focusedElementType, checkboxId, onChange, handleTab, updateTabIndex]);
253
+ const handleFocus = useCallback(e => {
254
+ const elements = getFocusableElements();
255
+ const focusedElement = e.target;
256
+ const targetElementInfo = elements.find(({
257
+ element
258
+ }) => element === focusedElement);
259
+ if (targetElementInfo && !targetElementInfo.blocked && (targetElementInfo.rowIndex !== focusedRowIndex || targetElementInfo.type !== focusedElementType)) {
260
+ setFocusedRowIndex(targetElementInfo.rowIndex);
261
+ setFocusedElementType(targetElementInfo.type);
262
+ updateTabIndex(targetElementInfo.rowIndex, targetElementInfo.type);
263
+ }
264
+ }, [focusedRowIndex, focusedElementType, getFocusableElements, updateTabIndex]);
265
+ // Initialize tabindex when options first become available
266
+ useEffect(() => {
267
+ if (options && options.length > 0 && !isInitialized.current) {
268
+ // Find first non-blocked row
269
+ let firstValidRow = 0;
270
+ while (firstValidRow < options.length && options[firstValidRow].blocked) {
271
+ firstValidRow++;
272
+ }
273
+ if (firstValidRow < options.length) {
274
+ setFocusedRowIndex(firstValidRow);
275
+ setFocusedElementType("checkbox");
276
+ updateTabIndex(firstValidRow, "checkbox");
277
+ isInitialized.current = true;
278
+ }
279
+ }
280
+ }, [options, updateTabIndex]);
281
+ // Update tabindex when focused position changes (only after initialization)
282
+ useEffect(() => {
283
+ if (isInitialized.current) {
284
+ updateTabIndex(focusedRowIndex, focusedElementType);
285
+ }
286
+ }, [focusedRowIndex, focusedElementType, updateTabIndex]);
116
287
  const getLabelColorInfo = (color, withRightPadding, isHidden, extraArgs) => {
117
288
  if (!color) {
118
289
  return null;
@@ -170,7 +341,8 @@ export const CheckboxListWithAction = ({
170
341
  type: "button",
171
342
  onClick: handleClick,
172
343
  title: title,
173
- className: classN
344
+ className: classN,
345
+ tabIndex: -1
174
346
  }, otherArgs, {
175
347
  children: childEl
176
348
  })) : _jsx("div", {
@@ -200,6 +372,9 @@ export const CheckboxListWithAction = ({
200
372
  };
201
373
  return optionsArr ? _jsx("ul", {
202
374
  className: "zen-checkbox-list-with-action__list",
375
+ ref: listRef,
376
+ onKeyDown: handleKeyDown,
377
+ onFocus: handleFocus,
203
378
  children: optionsArr.map((opt, ind) => {
204
379
  const titleForRender = opt.title || opt.label;
205
380
  const labelForRender = selectSearchTerm(opt.label);
@@ -215,7 +390,9 @@ export const CheckboxListWithAction = ({
215
390
  checked: opt.checked,
216
391
  disabled: opt.blocked,
217
392
  title: translate("Toggle {groupName} filter").replace("{groupName}", titleForRender),
393
+ describedBy: opt.isWithAction ? navigationInstructionsId : undefined,
218
394
  "data-indeterminate": opt.partialChecked,
395
+ tabIndex: -1,
219
396
  children: opt.isWithAction ? null : labelElement
220
397
  }), opt.isWithAction && labelElement]
221
398
  }, `li_${ind}_${opt.property}`);
@@ -227,7 +404,12 @@ export const CheckboxListWithAction = ({
227
404
  children: [label ? _jsx("div", {
228
405
  className: "zen-checkbox-list-with-action__title",
229
406
  children: label
230
- }) : null, build(options)]
407
+ }) : null, build(options), _jsx("div", {
408
+ id: navigationInstructionsId,
409
+ className: "zen-checkbox-list-with-action__sr-only",
410
+ "aria-hidden": "true",
411
+ children: translate("Has additional actions. Use right arrow to access button, left arrow to return.")
412
+ })]
231
413
  });
232
414
  };
233
415
  export const TRANSLATIONS = ["View {groupName} children", "Toggle {groupName} filter", "Number of selected child groups - {count}"];
@@ -1,3 +1,3 @@
1
1
  import { IDropdownPopup as IDropdownPopupRaw } from "../dropdownRaw/dropdownPopup";
2
- export declare const DropdownPopup: ({ alignment, triggerRef, classNamePopup, isMobile, inputValue, dialogAriaLabel, disabled, filterName, handleApplyClick, handleClearClick, handleTriggerClick, mobileSheetStackingClassName, onInputChange, hasApplyButton, inputId, isClearButtonDisabled, isWithFooter, placeholder, searchField, isSearchInPopup, isApplyButtonDisabled, isOpenCombo, inputRef, contentRef, children, onReadyForFocus, handleCheckboxChange, checkboxLabel, isChecked, popupId }: IDropdownPopupRaw) => import("react/jsx-runtime").JSX.Element;
2
+ export declare const DropdownPopup: ({ alignment, triggerRef, classNamePopup, isMobile, inputValue, dialogAriaLabel, disabled, filterName, handleApplyClick, handleClearClick, handleTriggerClick, mobileSheetStackingClassName, onInputChange, hasApplyButton, inputId, isClearButtonDisabled, isWithFooter, placeholder, searchField, isSearchInPopup, isApplyButtonDisabled, isOpenCombo, inputRef, contentRef, children, onReadyForFocus, handleCheckboxChange, checkboxLabel, isChecked, popupId, handleKeydown, handleFocusOnSentinelItem }: IDropdownPopupRaw) => import("react/jsx-runtime").JSX.Element;
3
3
  export type IDropdownPopup = IDropdownPopupRaw;
@@ -1 +1 @@
1
- export declare const DropdownTrigger: ({ name, onBlur, isActive, id, triggerRef, searchField, placeholder, disabled, fullWidth, currentSelection, handleClick, inputValue, className, onSearchChange, width, title, triggerAriaLabel, count, inputRef, buttonType, isError, popupId, ...otherProps }: import("../dropdownRaw/dropdownTrigger").IDropdownTrigger) => import("react/jsx-runtime").JSX.Element;
1
+ export declare const DropdownTrigger: ({ name, onBlur, isActive, id, triggerRef, searchField, placeholder, disabled, fullWidth, currentSelection, handleClick, inputValue, className, onSearchChange, width, title, triggerAriaLabel, count, inputRef, buttonType, isError, popupId, handleFocusOnPopup, ...otherProps }: import("../dropdownRaw/dropdownTrigger").IDropdownTrigger) => import("react/jsx-runtime").JSX.Element;
@@ -32,11 +32,12 @@ export interface IDropdownList extends IZenComponentProps {
32
32
  handleCheckboxChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
33
33
  checkboxLabel?: string;
34
34
  isChecked?: boolean;
35
+ listElementRef?: React.RefObject<HTMLDivElement>;
35
36
  }
36
37
  interface IDropdownEmptyList extends Pick<IDropdownList, "className" | "width" | "onClearClick" | "onApplyClick" | "onCancelClick" | "hasApplyButton" | "isApplyDisabled" | "isClearButtonDisabled" | "isWithFooter"> {
37
38
  hasError: boolean;
38
39
  }
39
40
  export declare const EmptyList: ({ className, width, onClearClick, onApplyClick, onCancelClick, hasApplyButton, isApplyDisabled, isClearButtonDisabled, isWithFooter, hasError }: IDropdownEmptyList) => import("react/jsx-runtime").JSX.Element;
40
- export declare const DropdownList: ({ onBackButtonClick, onSelectAllClick, onClearClick, onApplyClick, onCancelClick, onChange, onSelect, onSingleSelect, listData, isAllSelected, backButtonName, width, minWidth, isSelectAllButtonDisable, hasSelectAllButton, filterName, isMultiselect, hasApplyButton, isApplyDisabled, isClearButtonDisabled, activeValue, forceSelection, isWithFooter, isMobile, handleCheckboxChange, checkboxLabel, isChecked }: IDropdownList) => import("react/jsx-runtime").JSX.Element;
41
+ export declare const DropdownList: ({ onBackButtonClick, onSelectAllClick, onClearClick, onApplyClick, onCancelClick, onChange, onSelect, onSingleSelect, listData, isAllSelected, backButtonName, width, minWidth, isSelectAllButtonDisable, hasSelectAllButton, filterName, isMultiselect, hasApplyButton, isApplyDisabled, isClearButtonDisabled, activeValue, forceSelection, isWithFooter, isMobile, handleCheckboxChange, checkboxLabel, isChecked, listElementRef }: IDropdownList) => import("react/jsx-runtime").JSX.Element;
41
42
  export declare const TRANSLATIONS: string[];
42
43
  export {};
@@ -1,6 +1,6 @@
1
1
  import { injectString } from "../utils/localization/translationsDictionary";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import React, { useCallback, useId, useMemo } from "react";
3
+ import React, { useCallback, useId, useMemo, useRef } from "react";
4
4
  import { CheckboxListWithAction } from "../checkboxListWithAction/checkboxListWithAction";
5
5
  import { classNames } from "../commonHelpers/classNames/classNames";
6
6
  import { IconBackArrow } from "../icons/deprecated/iconBackArrow";
@@ -309,7 +309,8 @@ export const DropdownList = ({
309
309
  isMobile,
310
310
  handleCheckboxChange,
311
311
  checkboxLabel,
312
- isChecked
312
+ isChecked,
313
+ listElementRef
313
314
  }) => {
314
315
  const {
315
316
  translate
@@ -320,6 +321,13 @@ export const DropdownList = ({
320
321
  const handleSelectAllClick = () => {
321
322
  onSelectAllClick(isAllSelected);
322
323
  };
324
+ const handleBackButtonKeyDown = useCallback(event => {
325
+ if (event.key === "ArrowLeft") {
326
+ event.preventDefault();
327
+ onBackButtonClick();
328
+ }
329
+ }, [onBackButtonClick]);
330
+ const componentRef = useRef(null);
323
331
  const handleChange = useCallback(value => {
324
332
  onChange(value);
325
333
  }, [onChange]);
@@ -340,10 +348,11 @@ export const DropdownList = ({
340
348
  forceSelection ? onSingleSelect(false, newActiveEl || activeValue) : onSingleSelect(false, newActiveEl);
341
349
  }, [activeValue, forceSelection, onSingleSelect]);
342
350
  const selectButtonLabel = isAllSelected ? translate("Deselect all") : translate("Select all");
343
- const getActionButton = (label, title, icon, isDisabled, clickHandler, size) => _jsx("button", {
351
+ const getActionButton = (label, title, icon, isDisabled, clickHandler, size, onKeyDown) => _jsx("button", {
344
352
  type: "button",
345
353
  disabled: isDisabled,
346
354
  onClick: clickHandler,
355
+ onKeyDown: onKeyDown,
347
356
  className: "zen-dropdown-list__item zen-dropdown-list__item--interactive zen-dropdown-list__action-button zen-dropdown-list__action-button-back",
348
357
  title: title,
349
358
  children: _jsxs(_Fragment, {
@@ -382,6 +391,7 @@ export const DropdownList = ({
382
391
  };
383
392
  return _jsx("div", {
384
393
  className: "zen-dropdown-list__elements",
394
+ ref: listElementRef,
385
395
  children: isMultiselect ? _jsx(CheckboxListWithAction, {
386
396
  options: listData,
387
397
  label: "",
@@ -399,10 +409,11 @@ export const DropdownList = ({
399
409
  value: createValue(listData, activeValue)
400
410
  })
401
411
  });
402
- }, [isMultiselect, listData, handleChange, handleClick, handleSingleChange, translate, listId, forceSelection, activeValue]);
412
+ }, [listElementRef, isMultiselect, listData, handleChange, handleClick, handleSingleChange, translate, listId, forceSelection, activeValue]);
403
413
  return _jsxs("div", Object.assign({
404
414
  className: classNames(["zen-dropdown-list", driveComponentClass || ""])
405
415
  }, styleWidth, {
416
+ ref: componentRef,
406
417
  children: [filterName ? _jsx("div", {
407
418
  className: "zen-dropdown-list__label",
408
419
  children: filterName
@@ -415,7 +426,7 @@ export const DropdownList = ({
415
426
  })
416
427
  }) : null, backButtonName ? _jsx("div", {
417
428
  className: "zen-dropdown-list__item-wrapper",
418
- children: getActionButton(backButtonName, translate("Back"), IconBackArrow, false, onBackButtonClick, "large")
429
+ children: getActionButton(backButtonName, translate("Back"), IconBackArrow, false, onBackButtonClick, "large", handleBackButtonKeyDown)
419
430
  }) : null, hasSelectAllButton ? getActionButton(selectButtonLabel, selectButtonLabel, IconCheckRadio, !!isSelectAllButtonDisable, handleSelectAllClick, isDrive ? "huger" : "huge") : null, listDataComponent, isWithFooter ? _jsxs("div", {
420
431
  className: "zen-dropdown-list__footer",
421
432
  children: [_jsx(Button, {
@@ -31,5 +31,7 @@ export interface IDropdownPopup extends IZenComponentProps {
31
31
  checkboxLabel?: string;
32
32
  isChecked?: boolean;
33
33
  isSearchInPopup?: boolean;
34
+ handleKeydown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
35
+ handleFocusOnSentinelItem?: () => void;
34
36
  }
35
- export declare const DropdownPopup: ({ alignment, triggerRef, classNamePopup, isMobile, inputValue, dialogAriaLabel, disabled, filterName, handleApplyClick, handleClearClick, handleTriggerClick, mobileSheetStackingClassName, onInputChange, hasApplyButton, inputId, isClearButtonDisabled, isWithFooter, placeholder, searchField, isSearchInPopup, isApplyButtonDisabled, isOpenCombo, inputRef, contentRef, children, onReadyForFocus, handleCheckboxChange, checkboxLabel, isChecked, popupId }: IDropdownPopup) => import("react/jsx-runtime").JSX.Element;
37
+ export declare const DropdownPopup: ({ alignment, triggerRef, classNamePopup, isMobile, inputValue, dialogAriaLabel, disabled, filterName, handleApplyClick, handleClearClick, handleTriggerClick, mobileSheetStackingClassName, onInputChange, hasApplyButton, inputId, isClearButtonDisabled, isWithFooter, placeholder, searchField, isSearchInPopup, isApplyButtonDisabled, isOpenCombo, inputRef, contentRef, children, onReadyForFocus, handleCheckboxChange, checkboxLabel, isChecked, popupId, handleKeydown, handleFocusOnSentinelItem }: IDropdownPopup) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { injectString } from "../utils/localization/translationsDictionary";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useId, useMemo } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useId, useMemo, useRef } from "react";
4
4
  import { MobileSheet } from "../mobileSheet/mobileSheet";
5
5
  import { Button } from "../button/button";
6
6
  import { ButtonType } from "../button/buttonType";
@@ -11,6 +11,7 @@ import { useLanguage } from "../utils/localization/useLanguage";
11
11
  import { FooterButtons } from "../footerButtons/footerButtons";
12
12
  import { Checkbox } from "../checkbox/checkbox";
13
13
  import { useScrollWithAndroidKeyboard } from "../utils/useScrollWithAndroidKeyboard";
14
+ import { getFirstFocusableItem } from "../utils/keyboardHelpers";
14
15
  injectString("cs", "Filter by group", "Filtrovat podle skupiny");
15
16
  injectString("da-DK", "Filter by group", "Filtrer efter gruppe");
16
17
  injectString("de", "Filter by group", "Nach Gruppe filtern");
@@ -119,12 +120,16 @@ export const DropdownPopup = ({
119
120
  handleCheckboxChange,
120
121
  checkboxLabel,
121
122
  isChecked,
122
- popupId
123
+ popupId,
124
+ handleKeydown,
125
+ handleFocusOnSentinelItem
123
126
  }) => {
124
127
  const {
125
128
  translate
126
129
  } = useLanguage();
127
130
  const triggerId = useId();
131
+ const footerButtonsRef = useRef(null);
132
+ const mobileContentRef = useRef(null);
128
133
  const {
129
134
  handleFocus,
130
135
  handleBlur,
@@ -133,7 +138,22 @@ export const DropdownPopup = ({
133
138
  const memoizedHandleHide = useCallback(() => {
134
139
  handleTriggerClick(false);
135
140
  }, [handleTriggerClick]);
136
- const memoizedMobileContent = useMemo(() => _jsxs(_Fragment, {
141
+ const handleMobileFocusOnSentinelItem = useCallback(() => {
142
+ if (!isMobile) {
143
+ return;
144
+ }
145
+ let nextFocusableElement = footerButtonsRef.current ? getFirstFocusableItem(footerButtonsRef.current) : null;
146
+ if (nextFocusableElement) {
147
+ nextFocusableElement.focus();
148
+ return;
149
+ }
150
+ nextFocusableElement = getFirstFocusableItem(mobileContentRef.current);
151
+ nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
152
+ return;
153
+ }, [isMobile, footerButtonsRef, mobileContentRef]);
154
+ const memoizedMobileContent = useMemo(() => _jsxs("div", {
155
+ onKeyDown: handleKeydown,
156
+ ref: mobileContentRef,
137
157
  children: [searchField ? _jsx("div", {
138
158
  className: "zen-dropdown-mobile-sheet__search-container",
139
159
  children: _jsx(SearchInput, {
@@ -157,13 +177,18 @@ export const DropdownPopup = ({
157
177
  }) : null, _jsx("div", {
158
178
  ref: contentRef,
159
179
  children: children
180
+ }), _jsx("div", {
181
+ tabIndex: 0,
182
+ onFocus: handleMobileFocusOnSentinelItem,
183
+ className: "zen-dropdown-list__sentinel-item"
160
184
  })]
161
- }), [searchField, onInputChange, handleBlur, handleFocus, inputValue, inputRef, inputId, triggerId, disabled, placeholder, translate, handleCheckboxChange, isChecked, checkboxLabel, contentRef, children]);
185
+ }), [handleKeydown, searchField, onInputChange, handleBlur, handleFocus, inputValue, inputRef, inputId, triggerId, disabled, placeholder, translate, handleCheckboxChange, isChecked, checkboxLabel, contentRef, children, handleMobileFocusOnSentinelItem]);
162
186
  const memoizedMobileFooter = useMemo(() => {
163
187
  if (!isWithFooter) {
164
188
  return null;
165
189
  }
166
190
  return _jsxs(FooterButtons, {
191
+ ref: footerButtonsRef,
167
192
  children: [hasApplyButton ? _jsx(Button, {
168
193
  onClick: handleApplyClick,
169
194
  type: ButtonType.Primary,
@@ -216,6 +241,7 @@ export const DropdownPopup = ({
216
241
  recalculateOnScroll: true,
217
242
  children: _jsxs("div", {
218
243
  ref: contentRef,
244
+ onKeyDown: handleKeydown,
219
245
  children: [searchField && isSearchInPopup ? _jsx("div", {
220
246
  className: "zen-dropdown-popup__search-container",
221
247
  children: _jsx(SearchInput, {
@@ -227,8 +253,12 @@ export const DropdownPopup = ({
227
253
  disabled: disabled,
228
254
  placeholder: placeholder || translate("Filter by group")
229
255
  })
230
- }) : null, children]
256
+ }) : null, children, _jsx("div", {
257
+ tabIndex: 0,
258
+ onFocus: handleFocusOnSentinelItem,
259
+ className: "zen-dropdown-list__sentinel-item"
260
+ })]
231
261
  })
232
- }), [alignment, children, classNamePopup, contentRef, dialogAriaLabel, disabled, handleTriggerClick, inputId, inputRef, inputValue, isOpenCombo, isSearchInPopup, onInputChange, placeholder, popupId, searchField, translate, triggerId, triggerRef]);
262
+ }), [alignment, children, classNamePopup, contentRef, dialogAriaLabel, disabled, handleFocusOnSentinelItem, handleKeydown, handleTriggerClick, inputId, inputRef, inputValue, isOpenCombo, isSearchInPopup, onInputChange, placeholder, popupId, searchField, translate, triggerId, triggerRef]);
233
263
  return isMobile ? memoizedMobileView : memoizedDesktopView;
234
264
  };
@@ -28,6 +28,8 @@ import { useFormFieldValues } from "../commonHelpers/hooks/useFormFieldValues/us
28
28
  import { FormSectionContext } from "../formSection/utils/formSectionProvider";
29
29
  import { LIST_LIMIT } from "../dropdown/dropdown";
30
30
  import { zen } from "../utils/zen";
31
+ import { isChildOf } from "../utils/isChildOf";
32
+ import { getFirstFocusableItem } from "../utils/keyboardHelpers";
31
33
  injectString("cs", "Filter by group", "Filtrovat podle skupiny");
32
34
  injectString("da-DK", "Filter by group", "Filtrer efter gruppe");
33
35
  injectString("de", "Filter by group", "Nach Gruppe filtern");
@@ -134,6 +136,8 @@ export const DropdownRaw = props => {
134
136
  const popupId = useId();
135
137
  const abortToken = useRef(new AbortController());
136
138
  const triggerRef = useRef(null);
139
+ const listElementRef = useRef(null);
140
+ const nextFocusableItemRef = useRef(false);
137
141
  const comboboxRef = useRef(null);
138
142
  const inputRef = useRef(null);
139
143
  const contentRef = useRef(null);
@@ -301,6 +305,9 @@ export const DropdownRaw = props => {
301
305
  type: StateActionType.SetCurrentId,
302
306
  payload: newCurrentId === state.rootId || state.inputValue ? undefined : newCurrentId
303
307
  });
308
+ // const focusableElements = contentRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
309
+ // focusableElements?.[0]?.focus();
310
+ nextFocusableItemRef.current = state.currentId ? `button[data-btn='${state.currentId}']` : true;
304
311
  }, [state.currentId, state.groupsMap, state.inputValue, state.rootId]);
305
312
  const handleSelect = useCallback(val => {
306
313
  if (isFullSelectionMode && state.isAllSelected) {
@@ -400,11 +407,14 @@ export const DropdownRaw = props => {
400
407
  });
401
408
  }, [isFullSelectionMode, state.isAllSelected, state.listData, state.isNestedList, state.selectedIds]);
402
409
  const handleClearClick = useCallback(() => {
410
+ var _a, _b;
403
411
  dispatchState({
404
412
  type: StateActionType.ResetSelection,
405
413
  payload: undefined
406
414
  });
407
- }, []);
415
+ const focusableElements = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(FOCUSABLE_SELECTOR);
416
+ (_b = focusableElements === null || focusableElements === void 0 ? void 0 : focusableElements[0]) === null || _b === void 0 ? void 0 : _b.focus();
417
+ }, [contentRef]);
408
418
  const handleCancelClick = useCallback(() => {
409
419
  dispatchState({
410
420
  type: StateActionType.ResetStateToGlobal,
@@ -664,6 +674,7 @@ export const DropdownRaw = props => {
664
674
  payload: isChecked
665
675
  });
666
676
  prevIsChecked.current = isChecked;
677
+ nextFocusableItemRef.current = `.zen-dropdown-list__checkbox-container input`;
667
678
  const currentAbort = abortToken.current;
668
679
  controlData(currentAbort.signal, state.inputValue, isChecked);
669
680
  }, [controlData, state.inputValue]);
@@ -700,18 +711,27 @@ export const DropdownRaw = props => {
700
711
  }
701
712
  }, [searchField, state.currentId, state.showList, contentRef]);
702
713
  useEffect(() => {
703
- var _a;
714
+ var _a, _b;
704
715
  if (!state.isOpenCombo || !state.showList) {
705
716
  return;
706
717
  }
707
- const firstFocusable = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(FOCUSABLE_SELECTOR);
708
- if (firstFocusable && (state.currentId || !searchField)) {
718
+ if (nextFocusableItemRef.current && typeof nextFocusableItemRef.current === "string") {
719
+ const nextFocusable = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(nextFocusableItemRef.current);
720
+ if (nextFocusable) {
721
+ nextFocusable.focus();
722
+ nextFocusableItemRef.current = false;
723
+ return;
724
+ }
725
+ }
726
+ const firstFocusable = (_b = contentRef.current) === null || _b === void 0 ? void 0 : _b.querySelector(FOCUSABLE_SELECTOR);
727
+ if (firstFocusable && (!searchField || state.currentId || nextFocusableItemRef.current)) {
728
+ nextFocusableItemRef.current = false;
709
729
  firstFocusable.focus();
710
730
  } else {
711
731
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
712
732
  (inputRef === null || inputRef === void 0 ? void 0 : inputRef.current) && inputRef.current.focus();
713
733
  }
714
- }, [state.isOpenCombo, state.currentId, state.showList, searchField]);
734
+ }, [state.isOpenCombo, state.currentId, state.showList, searchField, contentRef]);
715
735
  useEffect(() => {
716
736
  const shouldExit = !getNamedItems || Object.keys(state.groupsMap).length === 0 && Object.keys(state.groupsMapSelected).length === 0 || getNamedItemsHasBeenCalled.current || state.groupsMapSelected.size === 0;
717
737
  if (shouldExit) {
@@ -743,6 +763,57 @@ export const DropdownRaw = props => {
743
763
  return currentAllSelected !== undefined ? state.defaultValueIsAllSelected === currentAllSelected && isStringsTheSame && isCheckedEqualDefault : isStringsTheSame && isCheckedEqualDefault;
744
764
  }, [currentAllSelected, currentIsChecked, forceSelection, multiselect, state.defaultValue, state.defaultValueIsAllSelected, state.defaultValueIsChecked, state.selectedIds]);
745
765
  const memoizedIsApplyButtonDisabled = useMemo(() => compareStringsArrays(state.globalSelectedIds, state.selectedIds) && (isFullSelectionMode ? state.globalIsAllSelected === state.isAllSelected : true) && (isCheckboxMode ? state.globalIsChecked === state.isChecked : true), [isCheckboxMode, isFullSelectionMode, state.globalIsAllSelected, state.globalIsChecked, state.globalSelectedIds, state.isAllSelected, state.isChecked, state.selectedIds]);
766
+ const handleKeydown = useCallback(e => {
767
+ const isChildOfListElements = listElementRef.current && e.target instanceof Node ? isChildOf(e.target, listElementRef.current) : false;
768
+ if (isChildOfListElements) {
769
+ return;
770
+ }
771
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
772
+ if (!contentRef.current) {
773
+ return;
774
+ }
775
+ const focusableElements = contentRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
776
+ if (focusableElements.length === 0) {
777
+ return;
778
+ }
779
+ const currentIndex = Array.from(focusableElements).indexOf(e.target);
780
+ let nextIndex;
781
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
782
+ if (currentIndex === 0 && e.key === "ArrowUp" && inputRef.current) {
783
+ inputRef.current.focus();
784
+ return;
785
+ }
786
+ if (e.key === "ArrowDown") {
787
+ e.preventDefault();
788
+ nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % focusableElements.length;
789
+ } else {
790
+ e.preventDefault();
791
+ nextIndex = currentIndex === -1 ? focusableElements.length - 1 : (currentIndex - 1 + focusableElements.length) % focusableElements.length;
792
+ }
793
+ focusableElements[nextIndex].focus();
794
+ }
795
+ }, []);
796
+ const handleFocusOnSentinelItem = useCallback(() => {
797
+ var _a;
798
+ if (isMobile) {
799
+ return;
800
+ }
801
+ const firstFocusable = getFirstFocusableItem(contentRef.current);
802
+ if (firstFocusable && listElementRef.current && isChildOf(firstFocusable, listElementRef.current)) {
803
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
804
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
805
+ } else {
806
+ firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
807
+ }
808
+ }, [contentRef, isMobile, listElementRef]);
809
+ const handleFocusOnPopup = useCallback(() => {
810
+ var _a;
811
+ if (!state.showList) {
812
+ return;
813
+ }
814
+ const firstFocusable = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(FOCUSABLE_SELECTOR);
815
+ firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
816
+ }, [state.showList, contentRef]);
746
817
  const countSelected = useMemo(() => {
747
818
  var _a, _b;
748
819
  if (hasApplyButton && state.globalIsAllSelected || !hasApplyButton && state.isAllSelected) {
@@ -812,7 +883,8 @@ export const DropdownRaw = props => {
812
883
  isMobile: isMobile,
813
884
  handleCheckboxChange: isCheckboxMode && !isMobile ? handleCheckboxChange : undefined,
814
885
  checkboxLabel: checkboxLabel,
815
- isChecked: state.isChecked
886
+ isChecked: state.isChecked,
887
+ listElementRef: listElementRef
816
888
  });
817
889
  }
818
890
  return null;
@@ -862,7 +934,8 @@ export const DropdownRaw = props => {
862
934
  triggerRef: triggerRef,
863
935
  inputRef: inputRef,
864
936
  buttonType: buttonType,
865
- popupId: popupId
937
+ popupId: popupId,
938
+ handleFocusOnPopup: handleFocusOnPopup
866
939
  }, accessibilityProps)), _jsx(DropdownPopup, {
867
940
  alignment: alignment,
868
941
  triggerRef: triggerRef,
@@ -885,6 +958,7 @@ export const DropdownRaw = props => {
885
958
  handleClearClick: multiselect ? handleClearClick : handleSingleSelection.bind(this, true),
886
959
  handleTriggerClick: handleTriggerClick,
887
960
  onInputChange: onInputChange,
961
+ handleKeydown: handleKeydown,
888
962
  inputRef: inputRef,
889
963
  contentRef: contentRef,
890
964
  onReadyForFocus: handleReadyForFocus,
@@ -893,6 +967,7 @@ export const DropdownRaw = props => {
893
967
  isChecked: state.isChecked,
894
968
  isSearchInPopup: chip,
895
969
  mobileSheetStackingClassName: mobileSheetStackingClassName,
970
+ handleFocusOnSentinelItem: handleFocusOnSentinelItem,
896
971
  children: getPopupContent
897
972
  })]
898
973
  });
@@ -20,9 +20,10 @@ interface IDropdownSearchableTrigger {
20
20
  name?: string;
21
21
  onBlur?: () => void;
22
22
  popupId?: string;
23
+ handleFocusOnPopup?: () => void;
23
24
  }
24
25
  export declare const DropdownSearchableTrigger: {
25
- ({ value, className, onChange, placeholder, isOpenPopup, inputRef, handleClick, width, title, id, disabled, count, currentSelection, isError, fullWidth, ref, name, onBlur, popupId, ...otherProps }: IDropdownSearchableTrigger): import("react/jsx-runtime").JSX.Element;
26
+ ({ value, className, onChange, placeholder, isOpenPopup, inputRef, handleClick, width, title, id, disabled, count, currentSelection, isError, fullWidth, ref, name, onBlur, popupId, handleFocusOnPopup, ...otherProps }: IDropdownSearchableTrigger): import("react/jsx-runtime").JSX.Element;
26
27
  displayName: string;
27
28
  };
28
29
  export declare const TRANSLATIONS: string[];