@geotab/zenith 3.13.0-beta.1 → 3.13.1-beta.1

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 (40) hide show
  1. package/README.md +3 -1
  2. package/dist/commonHelpers/hooks/usePillSize.js +2 -2
  3. package/dist/commonStyles/popupMixin.less +2 -2
  4. package/dist/controlledPopup/controlledPopup.d.ts +4 -1
  5. package/dist/controlledPopup/controlledPopup.js +2 -2
  6. package/dist/filters/components/filtersContainer.js +64 -7
  7. package/dist/filters/components/filtersSearch.d.ts +7 -2
  8. package/dist/filters/components/filtersSearch.js +36 -72
  9. package/dist/filters/components/filtersSearchList.js +18 -35
  10. package/dist/filters/filters.d.ts +2 -0
  11. package/dist/filters/filters.js +66 -180
  12. package/dist/filters/filtersHelper.js +1 -1
  13. package/dist/groupsFilterRaw/groupsFilterRaw.js +7 -1
  14. package/dist/index.css +193 -113
  15. package/dist/inputAdornments/inputAdornments.d.ts +2 -1
  16. package/dist/inputAdornments/inputAdornments.js +2 -2
  17. package/dist/searchInputRaw/searchInputRaw.js +6 -2
  18. package/dist/searchInputRaw/types.d.ts +4 -0
  19. package/dist/utils/localization/translations/en.json +1 -0
  20. package/dist/utils/positioningUtils/calculatePosition.js +3 -1
  21. package/dist/utils/positioningUtils/normalizeVertically.js +1 -1
  22. package/esm/commonHelpers/hooks/usePillSize.js +2 -2
  23. package/esm/controlledPopup/controlledPopup.d.ts +4 -1
  24. package/esm/controlledPopup/controlledPopup.js +2 -2
  25. package/esm/filters/components/filtersContainer.js +65 -8
  26. package/esm/filters/components/filtersSearch.d.ts +7 -2
  27. package/esm/filters/components/filtersSearch.js +36 -49
  28. package/esm/filters/components/filtersSearchList.js +18 -35
  29. package/esm/filters/filters.d.ts +2 -0
  30. package/esm/filters/filters.js +67 -181
  31. package/esm/filters/filtersHelper.js +1 -1
  32. package/esm/groupsFilterRaw/groupsFilterRaw.js +7 -1
  33. package/esm/inputAdornments/inputAdornments.d.ts +2 -1
  34. package/esm/inputAdornments/inputAdornments.js +2 -2
  35. package/esm/searchInputRaw/searchInputRaw.js +6 -2
  36. package/esm/searchInputRaw/types.d.ts +4 -0
  37. package/esm/utils/localization/translations/en.json +1 -0
  38. package/esm/utils/positioningUtils/calculatePosition.js +3 -1
  39. package/esm/utils/positioningUtils/normalizeVertically.js +1 -1
  40. package/package.json +1 -1
@@ -7,7 +7,7 @@ const classNames_1 = require("../commonHelpers/classNames/classNames");
7
7
  const textInputRaw_1 = require("../textInputRaw/textInputRaw");
8
8
  const useDriveClassName_1 = require("../utils/theme/useDriveClassName");
9
9
  const useMobileClassName_1 = require("../utils/theme/useMobileClassName");
10
- const InputAdornments = ({ id, value, className = "", disabled, isError, maxLength, onChange, onKeyDown, onFocus, startAdornment, endAdornment, placeholder, inputType = "text", autoFocus, name, onBlur, ref, ariaLabel, ariaDescribedby, role, ariaAutocomplete, ariaControls, ariaHaspopup, ariaExpanded }) => {
10
+ const InputAdornments = ({ id, value, className = "", disabled, isError, maxLength, onChange, onKeyDown, onFocus, startAdornment, endAdornment, placeholder, inputType = "text", autoFocus, name, onBlur, ref, ariaLabel, ariaDescribedby, role, ariaAutocomplete, ariaControls, ariaHaspopup, ariaExpanded, autocomplete }) => {
11
11
  const disabledClassName = disabled ? "zen-input-adornments--disabled" : "";
12
12
  const driveComponentClass = (0, useDriveClassName_1.useDriveClassName)("zen-input-adornments");
13
13
  const mobileComponentClass = (0, useMobileClassName_1.useMobileClassName)("zen-input-adornments");
@@ -25,7 +25,7 @@ const InputAdornments = ({ id, value, className = "", disabled, isError, maxLeng
25
25
  !value ? "zen-input-adornments__wrapper--empty" : "",
26
26
  className
27
27
  ]), [className, endAdornment, value]);
28
- return ((0, jsx_runtime_1.jsxs)("div", { className: classes, children: [!!startAdornment && (0, jsx_runtime_1.jsx)("div", { className: "zen-input-adornments__adornment zen-input-adornments__adornment--start", children: startAdornment }), (0, jsx_runtime_1.jsx)("div", { className: wrapperClasses, children: (0, jsx_runtime_1.jsx)(textInputRaw_1.TextInputRaw, { name: name, onBlur: onBlur, ref: ref, id: id, value: value, maxLength: maxLength, isError: isError, placeholder: placeholder, onChange: onChange, onKeyDown: onKeyDown, onFocus: onFocus, disabled: disabled, type: inputType, autoFocus: autoFocus, ariaLabel: ariaLabel, ariaDescribedby: ariaDescribedby, role: role, ariaAutocomplete: ariaAutocomplete, ariaControls: ariaControls, ariaHaspopup: ariaHaspopup, ariaExpanded: ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true, className: "zen-input-adornments__input zen-input-adornments__input--border-none" }) }), !!endAdornment && (0, jsx_runtime_1.jsx)("div", { className: "zen-input-adornments__adornment zen-input-adornments__adornment--end", children: endAdornment })] }));
28
+ return ((0, jsx_runtime_1.jsxs)("div", { className: classes, children: [!!startAdornment && (0, jsx_runtime_1.jsx)("div", { className: "zen-input-adornments__adornment zen-input-adornments__adornment--start", children: startAdornment }), (0, jsx_runtime_1.jsx)("div", { className: wrapperClasses, children: (0, jsx_runtime_1.jsx)(textInputRaw_1.TextInputRaw, { name: name, onBlur: onBlur, ref: ref, id: id, value: value, maxLength: maxLength, isError: isError, placeholder: placeholder, onChange: onChange, onKeyDown: onKeyDown, onFocus: onFocus, disabled: disabled, type: inputType, autoFocus: autoFocus, ariaLabel: ariaLabel, ariaDescribedby: ariaDescribedby, role: role, ariaAutocomplete: ariaAutocomplete, ariaControls: ariaControls, ariaHaspopup: ariaHaspopup, ariaExpanded: ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true, autocomplete: autocomplete, className: "zen-input-adornments__input zen-input-adornments__input--border-none" }) }), !!endAdornment && (0, jsx_runtime_1.jsx)("div", { className: "zen-input-adornments__adornment zen-input-adornments__adornment--end", children: endAdornment })] }));
29
29
  };
30
30
  exports.InputAdornments = InputAdornments;
31
31
  exports.InputAdornments.displayName = "InputAdornments";
@@ -120,12 +120,14 @@ const SearchInputRaw = props => {
120
120
  autoFocus,
121
121
  id,
122
122
  onFocus,
123
+ onClear,
123
124
  ref,
124
125
  role,
125
126
  ariaAutocomplete,
126
127
  ariaControls,
127
128
  ariaHaspopup,
128
- ariaExpanded
129
+ ariaExpanded,
130
+ autocomplete = "off"
129
131
  } = props;
130
132
  const {
131
133
  name,
@@ -159,6 +161,7 @@ const SearchInputRaw = props => {
159
161
  const onClick = () => {
160
162
  var _a;
161
163
  onChange === null || onChange === void 0 ? void 0 : onChange("");
164
+ onClear === null || onClear === void 0 ? void 0 : onClear();
162
165
  // Return focus to the input after clearing so keyboard users don't lose their place (WCAG 2.1 SC 2.1.1)
163
166
  (_a = ref ? ref.current : innerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
164
167
  };
@@ -217,7 +220,8 @@ const SearchInputRaw = props => {
217
220
  ariaAutocomplete: ariaAutocomplete,
218
221
  ariaControls: ariaControls,
219
222
  ariaHaspopup: ariaHaspopup,
220
- ariaExpanded: ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true
223
+ ariaExpanded: ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true,
224
+ autocomplete: autocomplete
221
225
  })
222
226
  }));
223
227
  };
@@ -8,6 +8,8 @@ export interface ISearchInputRawBase {
8
8
  placeholder?: string;
9
9
  ariaLabel?: string;
10
10
  closeButtonTitle?: string;
11
+ /** Called when the clear (X) button is clicked, in addition to clearing the value via onChange */
12
+ onClear?: () => void;
11
13
  isError?: boolean;
12
14
  isReadOnly?: boolean;
13
15
  maxLength?: number;
@@ -26,6 +28,8 @@ export interface ISearchInputRawBase {
26
28
  ariaControls?: string;
27
29
  ariaHaspopup?: boolean | "dialog" | "menu" | "grid" | "true" | "false" | "listbox" | "tree";
28
30
  ariaExpanded?: boolean | "true" | "false";
31
+ /** HTML autocomplete attribute for the underlying input. Defaults to "off" to suppress browser autofill. */
32
+ autocomplete?: string;
29
33
  }
30
34
  export interface ISearchInputRawDefault extends ISearchInputRawBase {
31
35
  value: string;
@@ -252,6 +252,7 @@
252
252
  "value changes in the range from {MIN} to {MAX}.": "value changes in the range from {MIN} to {MAX}.",
253
253
  "Error generating chart narrative for dataset": "Error generating chart narrative for dataset",
254
254
  "Chart data table and visualization": "Chart data table and visualization",
255
+ "Find in page": "Find in page",
255
256
  "Search page": "Search page",
256
257
  "Search by": "Search by",
257
258
  "Save": "Save",
@@ -93,7 +93,9 @@ const calculatePosition = (triggerRef, popupRef, paddingX, paddingY, inline, ali
93
93
  }
94
94
  }
95
95
  }
96
- else if (!isTopsideAlignment && triggerRect.y + triggerRect.height + popupRect.height > (zen_1.zen.innerHeight || 0)) {
96
+ else if (!isTopsideAlignment &&
97
+ triggerRect.y + triggerRect.height + popupRect.height > (zen_1.zen.innerHeight || 0) &&
98
+ position.height === undefined) {
97
99
  const newTopPosition = (zen_1.zen.innerHeight || 0) + scrollTop - popupRect.height;
98
100
  position.top = newTopPosition < 0 ? 0 : newTopPosition;
99
101
  }
@@ -7,7 +7,7 @@ const normalizeVertically = (position, popupRect, parentHeight) => {
7
7
  position.bottom = 0;
8
8
  return;
9
9
  }
10
- if (position.top) {
10
+ if (position.top !== undefined) {
11
11
  if (position.top < 0) {
12
12
  position.top = 0;
13
13
  }
@@ -49,7 +49,7 @@ export const usePillSize = ({ size, iconPosition, onClose, hasContent, initialTi
49
49
  }
50
50
  }, []);
51
51
  useResize(() => {
52
- if (isFixedSize || (hasIcon && !isClosable) || (!hasIcon && isClosable)) {
52
+ if (isFixedSize || !hasIcon || !isClosable) {
53
53
  return;
54
54
  }
55
55
  hidePillContent();
@@ -58,7 +58,7 @@ export const usePillSize = ({ size, iconPosition, onClose, hasContent, initialTi
58
58
  if (initialTitle) {
59
59
  return;
60
60
  }
61
- if ((hasIcon && !isClosable) || (!hasIcon && isClosable)) {
61
+ if (!hasIcon || !isClosable) {
62
62
  return;
63
63
  }
64
64
  hidePillContent();
@@ -3,6 +3,7 @@ import { TTrapFocus } from "../absolute/absolute";
3
3
  import "./controlledPopup.less";
4
4
  import { IZenComponentProps } from "../commonHelpers/zenComponent";
5
5
  import { TAlignment as TAlignmentAbsolute } from "../absolute/absolute";
6
+ import { IAlignment } from "../utils/positioningUtils/alignments";
6
7
  /**
7
8
  * @deprecated - please use {@link TAlignmentAbsolute|TAlignment} interface from the absolute component
8
9
  */
@@ -28,5 +29,7 @@ export interface IControlledPopup extends IZenComponentProps {
28
29
  preventAttributesAutoSet?: boolean;
29
30
  /** Whether to focus the first focusable element when popup opens. Default: true */
30
31
  focusOnOpen?: boolean;
32
+ /** Override the default alignment calculation functions. */
33
+ alignmentsFn?: IAlignment;
31
34
  }
32
- export declare const ControlledPopup: ({ isOpen, id, paddingX, paddingY, triggerRef, className, children, alignment, inline, onOpenChange, useTrapFocusWithTrigger, shouldHoldScroll, ariaLabel, ariaLabelledby, recalculateOnScroll, closeOnScroll, preventAttributesAutoSet, focusOnOpen, role }: IControlledPopup) => import("react/jsx-runtime").JSX.Element;
35
+ export declare const ControlledPopup: ({ isOpen, id, paddingX, paddingY, triggerRef, className, children, alignment, inline, onOpenChange, useTrapFocusWithTrigger, shouldHoldScroll, ariaLabel, ariaLabelledby, recalculateOnScroll, closeOnScroll, preventAttributesAutoSet, focusOnOpen, alignmentsFn, role }: IControlledPopup) => import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ import { Absolute } from "../absolute/absolute";
4
4
  import { classNames } from "../commonHelpers/classNames/classNames";
5
5
  import { useClientReady } from "../commonHelpers/hooks/useClientReady";
6
6
  import { useRTLAlignment } from "../utils/localization/useRTLAlignment";
7
- export const ControlledPopup = ({ isOpen, id, paddingX, paddingY, triggerRef, className, children, alignment, inline, onOpenChange, useTrapFocusWithTrigger, shouldHoldScroll, ariaLabel, ariaLabelledby, recalculateOnScroll, closeOnScroll, preventAttributesAutoSet, focusOnOpen, role = "dialog" }) => {
7
+ export const ControlledPopup = ({ isOpen, id, paddingX, paddingY, triggerRef, className, children, alignment, inline, onOpenChange, useTrapFocusWithTrigger, shouldHoldScroll, ariaLabel, ariaLabelledby, recalculateOnScroll, closeOnScroll, preventAttributesAutoSet, focusOnOpen, alignmentsFn, role = "dialog" }) => {
8
8
  const autoId = useId();
9
9
  const popupId = id || autoId;
10
10
  const isClientReady = useClientReady();
@@ -23,5 +23,5 @@ export const ControlledPopup = ({ isOpen, id, paddingX, paddingY, triggerRef, cl
23
23
  triggerRef.current.setAttribute("aria-expanded", isOpen ? "true" : "false");
24
24
  triggerRef.current.setAttribute("aria-controls", popupId);
25
25
  }, [triggerRef, isOpen, popupId, preventAttributesAutoSet, isClientReady]);
26
- return (_jsx(Absolute, Object.assign({ triggerRef: triggerRef, alignment: rtlAlignment, id: popupId, isOpen: isOpen, className: classNames(["zen-popup", className ? className : ""]), paddingX: paddingX, paddingY: paddingY, inline: inline, onOpenChange: onOpenChange, useTrapFocusWithTrigger: useTrapFocusWithTrigger, shouldHoldScroll: shouldHoldScroll }, (role !== false && typeof role === "string" ? { role } : {}), (role !== false && ariaLabel ? { ariaLabel } : {}), (role !== false && ariaLabelledby ? { ariaLabelledby } : {}), { recalculateOnScroll: recalculateOnScroll, closeOnScroll: closeOnScroll, focusOnOpen: focusOnOpen, children: children })));
26
+ return (_jsx(Absolute, Object.assign({ triggerRef: triggerRef, alignment: rtlAlignment, id: popupId, isOpen: isOpen, className: classNames(["zen-popup", className ? className : ""]), paddingX: paddingX, paddingY: paddingY, inline: inline, onOpenChange: onOpenChange, useTrapFocusWithTrigger: useTrapFocusWithTrigger, shouldHoldScroll: shouldHoldScroll }, (role !== false && typeof role === "string" ? { role } : {}), (role !== false && ariaLabel ? { ariaLabel } : {}), (role !== false && ariaLabelledby ? { ariaLabelledby } : {}), { recalculateOnScroll: recalculateOnScroll, closeOnScroll: closeOnScroll, focusOnOpen: focusOnOpen, alignmentsFn: alignmentsFn, children: children })));
27
27
  };
@@ -1,17 +1,20 @@
1
1
  import { injectString } from "../../utils/localization/translationsDictionary";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useMemo } from "react";
3
+ import { useCallback, useMemo, useRef, useState } from "react";
4
4
  import { classNames } from "../../commonHelpers/classNames/classNames";
5
5
  import { Chip } from "../../chip/chip";
6
6
  import React from "react";
7
7
  import { IconFilter } from "../../icons/iconFilter";
8
8
  import { createPropsHash, createStateHash, createStateObject, getFiltersState, getNumberOfChangedFilters, stabilizeChildrenWithId } from "../filtersHelper";
9
9
  import { FiltersSavedChipComponent } from "./filtersSavedChipComponent";
10
- import { IconClose } from "../../icons/iconClose";
10
+ import { IconCloseCircle } from "../../icons/iconCloseCircle";
11
+ import { IconDisk } from "../../icons/iconDisk";
11
12
  import { useChipStatus } from "../../chip/chipStatusProvider";
12
13
  import { useLanguage } from "../../utils/localization/useLanguage";
13
14
  import { useMobile } from "../../commonHelpers/hooks/useMobile";
14
15
  import { getNewFocusableItem } from "../../utils/keyboardHelpers";
16
+ import { useFiltersSavedItems } from "./filtersSavedItemsProvider";
17
+ import { FiltersSaveModal } from "./filtersSaveModal";
15
18
  injectString("cs", "All Filters", "V\u0161echny filtry");
16
19
  injectString("da-DK", "All Filters", "Alle filtre");
17
20
  injectString("de", "All Filters", "Alle Filter");
@@ -66,6 +69,33 @@ injectString("zh-Hans", "Clear", "\u6E05\u9664");
66
69
  injectString("zh-TW", "Clear", "\u6E05\u9664");
67
70
  injectString("ro-RO", "Clear", "\u0218terge\u021Bi");
68
71
  injectString("ar", "Clear", "\u0645\u0633\u062D");
72
+ injectString("cs", "Save", "Ulo\u017Eit");
73
+ injectString("da-DK", "Save", "Spare");
74
+ injectString("de", "Save", "Speichern");
75
+ injectString("en", "Save", "Save");
76
+ injectString("es", "Save", "Guardar");
77
+ injectString("fi-FI", "Save", "S\xE4\xE4st\xE4\xE4");
78
+ injectString("fr", "Save", "Enregistrer");
79
+ injectString("fr-FR", "Save", "Enregistrer");
80
+ injectString("hu-HU", "Save", "Ment\xE9s");
81
+ injectString("id", "Save", "Simpan");
82
+ injectString("it", "Save", "Salvare");
83
+ injectString("ja", "Save", "\u4FDD\u5B58");
84
+ injectString("ko-KR", "Save", "\uC800\uC7A5");
85
+ injectString("ms", "Save", "Simpan");
86
+ injectString("nb-NO", "Save", "Lagre");
87
+ injectString("nl", "Save", "Opslaan");
88
+ injectString("pl", "Save", "Zapisz");
89
+ injectString("pt-BR", "Save", "Salvar");
90
+ injectString("pt-PT", "Save", "Guardar");
91
+ injectString("sk-SK", "Save", "Ulo\u017Ei\u0165");
92
+ injectString("sv", "Save", "Spara");
93
+ injectString("th", "Save", "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01");
94
+ injectString("tr", "Save", "Kaydet");
95
+ injectString("zh-Hans", "Save", "\u4FDD\u5B58");
96
+ injectString("zh-TW", "Save", "\u5132\u5B58");
97
+ injectString("ro-RO", "Save", "Salva\u021Bi");
98
+ injectString("ar", "Save", "\u062D\u0641\u0638");
69
99
  export const FiltersContainer = ({
70
100
  className,
71
101
  isPinned = false,
@@ -81,6 +111,11 @@ export const FiltersContainer = ({
81
111
  translate
82
112
  } = useLanguage();
83
113
  const isMobile = useMobile();
114
+ const {
115
+ onSave
116
+ } = useFiltersSavedItems();
117
+ const [saveModalOpen, setSaveModalOpen] = useState(false);
118
+ const saveChipRef = useRef(null);
84
119
  const filtersChipContainerRef = React.useRef(null);
85
120
  const numberOfChangedFilters = useMemo(() => {
86
121
  const suppressedFilters = new Set();
@@ -103,6 +138,7 @@ export const FiltersContainer = ({
103
138
  title: translate("All Filters"),
104
139
  dataRole: "dialog",
105
140
  triggerRef: filtersChipRef,
141
+ className: "zen-filters-container__all-filters-chip",
106
142
  children: "+"
107
143
  }), [numberOfChangedFilters, chipStatus, filtersClickHandler, translate, filtersChipRef]);
108
144
  const childrenSignature = useMemo(() => {
@@ -160,18 +196,39 @@ export const FiltersContainer = ({
160
196
  active: false,
161
197
  status: isPinned ? "accent" : undefined,
162
198
  onChange: handleRemovalClick,
163
- icon: IconClose
199
+ icon: IconCloseCircle
164
200
  }) : null, [isPinned, handleRemovalClick, translate, numberOfChangedFilters]);
201
+ const handleSaveChipClick = useCallback(() => {
202
+ setSaveModalOpen(true);
203
+ }, []);
204
+ const chipSaveComponent = useMemo(() => onSave ? _jsx(Chip, {
205
+ title: translate("Save"),
206
+ onClick: handleSaveChipClick,
207
+ icon: IconDisk,
208
+ isOpen: saveModalOpen,
209
+ triggerRef: saveChipRef,
210
+ className: "zen-filters-container__save-chip"
211
+ }) : null, [onSave, translate, handleSaveChipClick, saveModalOpen]);
212
+ // Order:
213
+ // Default: [filters] [+]
214
+ // Selected: [filters] [+badge] [save] [clear]
215
+ // w/ Saved: [filters] [+] [save]
216
+ const showInlineSave = Boolean(onSave && (numberOfChangedFilters > 0 || hasSavedFilters));
165
217
  const memoizedFiltersElements = useMemo(() => _jsxs(_Fragment, {
166
- children: [memoizedChildren, hasSavedFilters ? _jsx(FiltersSavedChipComponent, {}) : null, filtersChipComponent, chipRemovalComponent]
167
- }), [chipRemovalComponent, filtersChipComponent, hasSavedFilters, memoizedChildren]);
168
- return _jsx("div", {
218
+ children: [memoizedChildren, hasSavedFilters ? _jsx(FiltersSavedChipComponent, {}) : null, filtersChipComponent, showInlineSave ? chipSaveComponent : null, chipRemovalComponent]
219
+ }), [chipRemovalComponent, chipSaveComponent, filtersChipComponent, hasSavedFilters, memoizedChildren, showInlineSave]);
220
+ return _jsxs("div", {
169
221
  className: classNames(["zen-filters-container", isMobile ? "zen-filters-container--mobile" : "", className || ""]),
170
222
  onKeyDown: handleKeyDown,
171
223
  ref: filtersChipContainerRef,
172
- children: isMobile ? _jsx("div", {
224
+ children: [isMobile ? _jsx("div", {
173
225
  className: "zen-filters-container__mobile-scroll-wrapper",
174
226
  children: memoizedFiltersElements
175
- }) : memoizedFiltersElements
227
+ }) : memoizedFiltersElements, onSave ? _jsx(FiltersSaveModal, {
228
+ prioritize: isMobile,
229
+ isOpen: saveModalOpen,
230
+ onClose: setSaveModalOpen,
231
+ onSave: onSave
232
+ }) : null]
176
233
  });
177
234
  };
@@ -8,19 +8,24 @@ interface IFiltersSearch {
8
8
  onChange: (newValue: string) => void;
9
9
  onSearchKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
10
10
  onOpenPopup?: () => void;
11
+ /** Called when the clear (X) button inside the search input is clicked */
12
+ onClear?: () => void;
11
13
  searchSelection?: ISearchItem[];
12
- handleRemove?: (id: ISearchItem) => void;
13
14
  className?: string;
14
15
  ref?: RefObject<HTMLInputElement | null>;
16
+ /** Ref forwarded to the root wrapper div — used to anchor the search popup */
17
+ containerRef?: RefObject<HTMLDivElement | null>;
15
18
  ariaLabel?: string;
16
19
  role?: string | undefined;
17
20
  ariaAutocomplete?: "list" | "none" | "inline" | "both";
18
21
  ariaControls?: string;
19
22
  ariaHaspopup?: boolean | "dialog" | "menu" | "grid" | "true" | "false" | "listbox" | "tree";
20
23
  ariaExpanded?: boolean | "true" | "false";
24
+ /** When true, renders only the search icon button (input lives in the popup) */
25
+ isMobile?: boolean;
21
26
  }
22
27
  export declare const FiltersSearch: {
23
- ({ id, name, value, onChange, onSearchKeyDown, onOpenPopup, searchSelection, handleRemove, className, ariaLabel, role, ariaAutocomplete, ariaControls, ariaHaspopup, ariaExpanded, ref }: IFiltersSearch): import("react/jsx-runtime").JSX.Element;
28
+ ({ id, name, value, onChange, onSearchKeyDown, onOpenPopup, onClear, searchSelection, className, containerRef, ariaLabel, role, ariaAutocomplete, ariaControls, ariaHaspopup, ariaExpanded, isMobile, ref }: IFiltersSearch): import("react/jsx-runtime").JSX.Element;
24
29
  displayName: string;
25
30
  };
26
31
  export {};
@@ -1,62 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useCallback, useMemo } from "react";
2
+ import { useCallback, useMemo } from "react";
3
3
  import { classNames } from "../../commonHelpers/classNames/classNames";
4
- import { NO_TYPED_SEARCH_ITEM } from "./filtersSearchList";
5
4
  import { SearchInputRaw } from "../../searchInputRaw/searchInputRaw";
6
5
  import { Pill } from "../../pill/pill";
7
- import { useMobile } from "../../commonHelpers/hooks/useMobile";
8
- export const FiltersSearch = ({ id, name, value, onChange, onSearchKeyDown, onOpenPopup, searchSelection, handleRemove, className, ariaLabel, role, ariaAutocomplete, ariaControls, ariaHaspopup, ariaExpanded, ref }) => {
9
- const isWithPills = Boolean(searchSelection && handleRemove);
10
- const divider = "_divider_";
11
- const isMobile = useMobile();
12
- const memoizedPillsArray = React.useMemo(() => {
13
- if (!isWithPills || !searchSelection || searchSelection.length === 0) {
14
- return [];
15
- }
16
- const pillsArray = [];
17
- searchSelection.forEach(item => {
18
- pillsArray.push({
19
- id: `${item.type || NO_TYPED_SEARCH_ITEM}${divider}${item.id}`,
20
- name: item.name
21
- });
22
- });
23
- return pillsArray;
24
- }, [isWithPills, searchSelection]);
25
- const onRemove = useCallback((removedId) => {
26
- const [type, itemId] = removedId.split(divider);
27
- const item = searchSelection === null || searchSelection === void 0 ? void 0 : searchSelection.find(el => el.id === itemId);
28
- if (item) {
29
- handleRemove === null || handleRemove === void 0 ? void 0 : handleRemove(Object.assign(Object.assign({}, item), { type: type === NO_TYPED_SEARCH_ITEM ? undefined : type }));
30
- }
31
- }, [handleRemove, searchSelection]);
32
- const handleSearchClick = useCallback(e => {
33
- var _a, _b, _c;
34
- if (((_a = e.target) === null || _a === void 0 ? void 0 : _a.dataset.name) === "IconClose" ||
35
- e.target.classList.contains("zen-pill-non-actionable__close-button") ||
36
- ((_b = e.target) === null || _b === void 0 ? void 0 : _b.parentElement.classList.contains("zen-pill-non-actionable__close-button")) ||
37
- ((_c = e.target) === null || _c === void 0 ? void 0 : _c.parentElement.dataset.name) === "IconClose") {
6
+ import { IconSearch } from "../../icons/iconSearch";
7
+ export const FiltersSearch = ({ id, name, value, onChange, onSearchKeyDown, onOpenPopup, onClear, searchSelection, className, containerRef, ariaLabel, role, ariaAutocomplete, ariaControls, ariaHaspopup, ariaExpanded, isMobile, ref }) => {
8
+ var _a;
9
+ const selectionCount = (_a = searchSelection === null || searchSelection === void 0 ? void 0 : searchSelection.length) !== null && _a !== void 0 ? _a : 0;
10
+ const handleSearchClick = useCallback((e) => {
11
+ var _a, _b;
12
+ const target = e.target;
13
+ if (target.classList.contains("zen-search-input__close-button") ||
14
+ ((_a = target.parentElement) === null || _a === void 0 ? void 0 : _a.classList.contains("zen-search-input__close-button")) ||
15
+ target.dataset.name === "IconClose" ||
16
+ ((_b = target.parentElement) === null || _b === void 0 ? void 0 : _b.dataset.name) === "IconClose") {
38
17
  return;
39
18
  }
40
19
  onOpenPopup === null || onOpenPopup === void 0 ? void 0 : onOpenPopup();
41
20
  }, [onOpenPopup]);
21
+ const selectionLabel = selectionCount > 99 ? "+99" : String(selectionCount);
42
22
  const selectedPills = useMemo(() => {
43
- if (memoizedPillsArray.length === 0) {
23
+ if (selectionCount === 0) {
44
24
  return [];
45
25
  }
46
- if (isMobile) {
47
- return memoizedPillsArray.length
48
- ? [
49
- _jsx(Pill, { type: "info", className: "zen-filters-search__selected-item zen-filters-search__selected-item--more", children: `+${memoizedPillsArray.length}` }, `${memoizedPillsArray.length}_${memoizedPillsArray.map(pill => pill.name).join("_")}`)
50
- ]
51
- : [];
52
- }
53
- const pillsForRender = [
54
- { id: memoizedPillsArray[0].id, name: memoizedPillsArray[0].name },
55
- memoizedPillsArray.length > 1 ? { id: "more_pills", name: `+${memoizedPillsArray.length - 1}` } : undefined
56
- ].filter(Boolean);
57
- return pillsForRender.map((pill, ind) => (_jsx(Pill, { type: "info", onClose: ind === 0 ? onRemove.bind(null, pill.id) : undefined, className: classNames(["zen-filters-search__selected-item", ind !== 0 ? "zen-filters-search__selected-item--more" : ""]), children: pill.name }, `${pill.id}_${pill.name}`)));
58
- }, [memoizedPillsArray, onRemove, isMobile]);
59
- const searchComponent = useMemo(() => (_jsx(SearchInputRaw, { id: id, ref: ref, placeholder: name, onChange: onChange, onKeyDown: onSearchKeyDown, value: value, role: role, ariaLabel: ariaLabel, ariaExpanded: ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true, ariaHaspopup: ariaHaspopup, ariaControls: ariaControls, ariaAutocomplete: ariaAutocomplete, className: classNames(["zen-filters-search__search-input", "zen-filters__search", className || ""]) })), [id, ref, name, onChange, onSearchKeyDown, value, className, role, ariaLabel, ariaExpanded, ariaHaspopup, ariaControls, ariaAutocomplete]);
60
- return (_jsxs("div", { className: classNames(["zen-filters-search", className || "", isMobile ? "zen-filters-search--mobile" : ""]), onClick: handleSearchClick, children: [searchComponent, selectedPills.length > 0 && _jsx("div", { className: "zen-filters-search__selected", children: selectedPills })] }));
26
+ return [
27
+ _jsx(Pill, { type: "info", className: "zen-filters-search__selected-item zen-filters-search__selected-item--more", children: selectionLabel }, selectionCount)
28
+ ];
29
+ }, [selectionCount, selectionLabel]);
30
+ const searchComponent = useMemo(() => (_jsx(SearchInputRaw, { id: id, ref: ref, placeholder: name, onChange: onChange, onClear: onClear, onKeyDown: onSearchKeyDown, value: value, role: role, ariaLabel: ariaLabel, ariaExpanded: ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true, ariaHaspopup: ariaHaspopup, ariaControls: ariaControls, ariaAutocomplete: ariaAutocomplete, className: classNames(["zen-filters-search__search-input", "zen-filters__search", className || ""]) })), [
31
+ id,
32
+ ref,
33
+ name,
34
+ onChange,
35
+ onClear,
36
+ onSearchKeyDown,
37
+ value,
38
+ className,
39
+ role,
40
+ ariaLabel,
41
+ ariaExpanded,
42
+ ariaHaspopup,
43
+ ariaControls,
44
+ ariaAutocomplete
45
+ ]);
46
+ const mobileTrigger = useMemo(() => (_jsx("button", { type: "button", className: "zen-filters-search__mobile-trigger", onClick: handleSearchClick, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded === undefined ? undefined : ariaExpanded === "true" || ariaExpanded === true, "aria-label": ariaLabel || name, children: _jsx(IconSearch, { className: "zen-filters-search__mobile-trigger-icon", size: "large" }) })), [handleSearchClick, ariaHaspopup, ariaControls, ariaExpanded, ariaLabel, name]);
47
+ return (_jsxs("div", { ref: containerRef, className: classNames(["zen-filters-search", isMobile ? "zen-filters-search--mobile" : "", className || ""]), onClick: isMobile ? undefined : handleSearchClick, children: [isMobile ? mobileTrigger : searchComponent, selectedPills.length > 0 && _jsx("div", { className: "zen-filters-search__selected", children: selectedPills })] }));
61
48
  };
62
49
  FiltersSearch.displayName = "FiltersSearch";
@@ -126,33 +126,6 @@ injectString("zh-Hans", "Currently selected", "Currently selected");
126
126
  injectString("zh-TW", "Currently selected", "Currently selected");
127
127
  injectString("ro-RO", "Currently selected", "Selec\u021Bie curent\u0103");
128
128
  injectString("ar", "Currently selected", "\u0627\u0644\u0645\u062D\u062F\u062F \u062D\u0627\u0644\u064A\u0627\u064B");
129
- injectString("cs", "View history", "View history");
130
- injectString("da-DK", "View history", "View history");
131
- injectString("de", "View history", "View history");
132
- injectString("en", "View history", "View history");
133
- injectString("es", "View history", "View history");
134
- injectString("fi-FI", "View history", "View history");
135
- injectString("fr", "View history", "View history");
136
- injectString("fr-FR", "View history", "View history");
137
- injectString("hu-HU", "View history", "View history");
138
- injectString("id", "View history", "View history");
139
- injectString("it", "View history", "View history");
140
- injectString("ja", "View history", "View history");
141
- injectString("ko-KR", "View history", "View history");
142
- injectString("ms", "View history", "View history");
143
- injectString("nb-NO", "View history", "View history");
144
- injectString("nl", "View history", "View history");
145
- injectString("pl", "View history", "View history");
146
- injectString("pt-BR", "View history", "View history");
147
- injectString("pt-PT", "View history", "Ver hist\xF3rico");
148
- injectString("sk-SK", "View history", "View history");
149
- injectString("sv", "View history", "View history");
150
- injectString("th", "View history", "View history");
151
- injectString("tr", "View history", "View history");
152
- injectString("zh-Hans", "View history", "View history");
153
- injectString("zh-TW", "View history", "View history");
154
- injectString("ro-RO", "View history", "Vede\u021Bi istoricul");
155
- injectString("ar", "View history", "\u0639\u0631\u0636 \u0627\u0644\u0633\u062C\u0644");
156
129
  injectString("cs", "Clear", "Vymazat");
157
130
  injectString("da-DK", "Clear", "Ryd");
158
131
  injectString("de", "Clear", "L\xF6schen");
@@ -385,15 +358,25 @@ export const FiltersSearchList = ({
385
358
  var _a;
386
359
  setRemovedItems(prev => [...prev, itemId]);
387
360
  onRemoveRecentSearchItem && onRemoveRecentSearchItem([itemId]);
361
+ const remaining = recentItems.filter(item => item.id !== itemId && !removedItems.includes(item.id));
362
+ if (remaining.length === 0) {
363
+ changeMode("");
364
+ return;
365
+ }
388
366
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
389
367
  const firstItem = (_a = searchListRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(FOCUSABLE_SELECTOR);
390
368
  firstItem === null || firstItem === void 0 ? void 0 : firstItem.focus();
391
- }, [onRemoveRecentSearchItem, searchListRef]);
369
+ }, [onRemoveRecentSearchItem, searchListRef, recentItems, removedItems, changeMode]);
392
370
  const handleRemoveAll = useCallback(() => {
393
371
  setRemovedItems(recentItems.map(item => item.id));
394
372
  onRemoveRecentSearchItem && onRemoveRecentSearchItem(recentItems.map(item => item.id));
395
373
  changeMode(getCurrentMode());
396
374
  }, [recentItems, onRemoveRecentSearchItem, changeMode, getCurrentMode]);
375
+ const handleViewAll = useCallback(() => {
376
+ setRemovedItems(recentItems.map(item => item.id));
377
+ onRemoveRecentSearchItem && onRemoveRecentSearchItem(recentItems.map(item => item.id));
378
+ changeMode("");
379
+ }, [recentItems, onRemoveRecentSearchItem, changeMode]);
397
380
  const memoizedSearchItemClick = useCallback((itemId, itemType) => () => {
398
381
  const currentSearchItem = onlyOneType && !itemType ? searchItems.find(item => item.id === itemId) : searchItems.find(item => item.id === itemId && item.type === itemType);
399
382
  currentSearchItem && handleSearchItemClick && handleSearchItemClick(currentSearchItem);
@@ -522,10 +505,10 @@ export const FiltersSearchList = ({
522
505
  children: [_jsx("div", {
523
506
  className: "zen-filters-search-list__header-title",
524
507
  children: currentMode === "recent" ? translate("Recent searches") : translate("Currently selected")
525
- }), currentMode === "recent" && recentItems.length > resultsToShow.length && _jsx(Button, {
508
+ }), currentMode === "recent" && _jsx(Button, {
526
509
  type: "tertiary",
527
- onClick: changeMode.bind(null, "history"),
528
- children: translate("View history")
510
+ onClick: handleViewAll,
511
+ children: translate("Clear all")
529
512
  }), currentMode === "selected" && resultsToShow.length > 0 && _jsx(Button, {
530
513
  type: "tertiary",
531
514
  onClick: memoizedClearSelectedItems,
@@ -570,17 +553,17 @@ export const FiltersSearchList = ({
570
553
  type: "tertiary",
571
554
  onClick: handleRemoveAll,
572
555
  children: translate("Clear all")
573
- }) : null, currentMode === "recent" && recentItems.length > resultsToShow.length ? _jsx(Button, {
556
+ }) : null, currentMode === "recent" ? _jsx(Button, {
574
557
  type: "tertiary",
575
- onClick: changeMode.bind(null, "history"),
576
- children: translate("View history")
558
+ onClick: handleViewAll,
559
+ children: translate("Clear all")
577
560
  }) : null, currentMode === "selected" && resultsToShow.length > 0 && (searchSelection || []).length > 0 ? _jsx(Button, {
578
561
  type: "tertiary",
579
562
  onClick: memoizedClearSelectedItemsForApplyMode,
580
563
  children: translate("Clear")
581
564
  }) : null]
582
565
  });
583
- }, [changeMode, currentMode, handleRemoveAll, applyButtonHandler, memoizedClearSelectedItemsForApplyMode, recentItems.length, resultsToShow.length, translate, isInitialSearchSelectionState, searchSelection]);
566
+ }, [currentMode, handleRemoveAll, handleViewAll, applyButtonHandler, memoizedClearSelectedItemsForApplyMode, resultsToShow.length, translate, isInitialSearchSelectionState, searchSelection]);
584
567
  const searchHeaderContent = applyButtonHandler ? getHeaderTitles() : getSearchHeaderContent();
585
568
  return _jsxs(_Fragment, {
586
569
  children: [_jsxs("div", {
@@ -40,6 +40,8 @@ export interface IFiltersSearchTypesItem {
40
40
  export interface IFiltersBase extends IZenComponentProps {
41
41
  searchTypes?: IFiltersSearchTypesItem[] | IFiltersSearchTypesItem;
42
42
  onToggleView?: (isOpen: boolean) => void;
43
+ /** Placeholder text for the search input. Defaults to "Find in page" when empty or undefined. */
44
+ searchPlaceholder?: string;
43
45
  }
44
46
  export interface IFiltersWithKeyword {
45
47
  keyword: string;