@wallarm-org/design-system 0.41.0 → 0.42.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 (93) hide show
  1. package/dist/components/FilterInput/FilterInput.d.ts +7 -8
  2. package/dist/components/FilterInput/FilterInputContext/types.d.ts +12 -9
  3. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +4 -2
  4. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -0
  5. package/dist/components/FilterInput/FilterInputErrors/parseFilterInputErrors.js +1 -2
  6. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.d.ts +1 -1
  7. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.js +1 -1
  8. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.d.ts +1 -1
  9. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +2 -2
  10. package/dist/components/FilterInput/FilterInputField/FilterInputField.js +11 -40
  11. package/dist/components/FilterInput/FilterInputField/hooks/useSegmentEditKeyboard.d.ts +37 -0
  12. package/dist/components/FilterInput/FilterInputField/hooks/useSegmentEditKeyboard.js +78 -0
  13. package/dist/components/FilterInput/FilterInputMenu/FilterInputDateValueMenu/constants.d.ts +2 -2
  14. package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.d.ts +1 -1
  15. package/dist/components/FilterInput/FilterInputMenu/FilterInputMenu.d.ts +1 -1
  16. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.d.ts +5 -6
  17. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useValueMenuDisplayValues.d.ts +4 -14
  18. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useValueMenuState.d.ts +3 -5
  19. package/dist/components/FilterInput/FilterInputMenu/hooks/useKeyboardNav.d.ts +1 -1
  20. package/dist/components/FilterInput/FilterInputMenu/hooks/useKeyboardNav.js +5 -2
  21. package/dist/components/FilterInput/hooks/useAutoCleanupDetachedElement.d.ts +13 -0
  22. package/dist/components/FilterInput/hooks/useAutoCleanupDetachedElement.js +22 -0
  23. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/{deriveAutocompleteValues.d.ts → lib/deriveAutocompleteValues.d.ts} +3 -3
  24. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/{deriveAutocompleteValues.js → lib/deriveAutocompleteValues.js} +1 -1
  25. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/getInitialSegmentText.d.ts +9 -0
  26. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/getInitialSegmentText.js +8 -0
  27. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/index.d.ts +3 -0
  28. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/index.js +4 -0
  29. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/{valueCommitHelpers.d.ts → lib/valueResolution.d.ts} +1 -7
  30. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/{valueCommitHelpers.js → lib/valueResolution.js} +3 -25
  31. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useAutocompleteState.d.ts +36 -0
  32. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useAutocompleteState.js +53 -0
  33. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.d.ts +5 -16
  34. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.js +12 -5
  35. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipActions.d.ts +2 -2
  36. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipActions.js +5 -5
  37. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipCascade.d.ts +44 -0
  38. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipCascade.js +99 -0
  39. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.d.ts +7 -8
  40. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.js +21 -16
  41. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +9 -10
  42. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +50 -83
  43. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.d.ts +9 -11
  44. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.js +47 -20
  45. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.d.ts +6 -4
  46. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.js +32 -15
  47. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/index.d.ts +1 -0
  48. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/index.js +2 -0
  49. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/{useMenuFlow.d.ts → useMenuFlow/types.d.ts} +15 -18
  50. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/types.js +0 -0
  51. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useFieldFlow.d.ts +11 -0
  52. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useFieldFlow.js +95 -0
  53. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useMenuFlow.d.ts +20 -0
  54. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useMenuFlow.js +47 -0
  55. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useOperatorFlow.d.ts +11 -0
  56. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useOperatorFlow.js +87 -0
  57. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useValueFlow.d.ts +14 -0
  58. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useValueFlow.js +107 -0
  59. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuPositioning.d.ts +25 -8
  60. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuPositioning.js +38 -22
  61. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResetState.d.ts +10 -19
  62. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResetState.js +3 -3
  63. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useSegmentEditFlow.d.ts +36 -0
  64. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useSegmentEditFlow.js +69 -0
  65. package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +1 -2
  66. package/dist/components/FilterInput/hooks/useFilterInputPositioning.d.ts +9 -4
  67. package/dist/components/FilterInput/hooks/useFilterInputPositioning.js +7 -6
  68. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.d.ts +4 -4
  69. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.d.ts +2 -2
  70. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.d.ts +2 -5
  71. package/dist/components/FilterInput/hooks/useFloatingRecomputeOn.d.ts +14 -0
  72. package/dist/components/FilterInput/hooks/useFloatingRecomputeOn.js +18 -0
  73. package/dist/components/FilterInput/hooks/useResizeTracker.d.ts +10 -0
  74. package/dist/components/FilterInput/hooks/useResizeTracker.js +21 -0
  75. package/dist/components/FilterInput/lib/applyAcceptChar.d.ts +3 -4
  76. package/dist/components/FilterInput/lib/applyKnownFieldHelpers.d.ts +13 -25
  77. package/dist/components/FilterInput/lib/constants.d.ts +16 -34
  78. package/dist/components/FilterInput/lib/constants.js +3 -1
  79. package/dist/components/FilterInput/lib/dom.d.ts +15 -11
  80. package/dist/components/FilterInput/lib/dom.js +14 -9
  81. package/dist/components/FilterInput/lib/fields.d.ts +10 -19
  82. package/dist/components/FilterInput/lib/index.d.ts +4 -2
  83. package/dist/components/FilterInput/lib/index.js +5 -3
  84. package/dist/components/FilterInput/lib/menuFilterText.d.ts +8 -18
  85. package/dist/components/FilterInput/lib/operators.d.ts +11 -21
  86. package/dist/components/FilterInput/lib/segmentMenu.d.ts +4 -0
  87. package/dist/components/FilterInput/lib/segmentMenu.js +7 -0
  88. package/dist/components/FilterInput/lib/serializeExpression.d.ts +5 -12
  89. package/dist/components/FilterInput/lib/validation.d.ts +9 -0
  90. package/dist/components/FilterInput/lib/validation.js +24 -0
  91. package/dist/metadata/components.json +8 -8
  92. package/package.json +1 -1
  93. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.js +0 -260
@@ -1,50 +1,36 @@
1
- import { useCallback, useRef, useState } from "react";
2
1
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
2
  import { useDateRange } from "../../FilterInputMenu/FilterInputDateValueMenu/hooks.js";
4
- import { applyAcceptChar, getOperatorLabel } from "../../lib/index.js";
5
- import { deriveAutocompleteValues } from "./deriveAutocompleteValues.js";
3
+ import { deriveAutocompleteValues } from "./lib/index.js";
4
+ import { useAutocompleteState } from "./useAutocompleteState.js";
6
5
  import { useBlurCommit } from "./useBlurCommit.js";
7
6
  import { useChipActions } from "./useChipActions.js";
7
+ import { useChipCascade } from "./useChipCascade.js";
8
8
  import { useChipEditing } from "./useChipEditing.js";
9
9
  import { useFocusManagement } from "./useFocusManagement.js";
10
10
  import { useInputHandlers } from "./useInputHandlers.js";
11
- import { useMenuFlow } from "./useMenuFlow.js";
11
+ import { useMenuFlow } from "./useMenuFlow/index.js";
12
12
  import { useMenuPositioning } from "./useMenuPositioning.js";
13
13
  import { useResetState } from "./useResetState.js";
14
+ import { useSegmentEditFlow } from "./useSegmentEditFlow.js";
14
15
  const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition, removeCondition, removeConditionAtIndex, clearAll, setConnectorValue, containerRef, buildingChipRef, inputRef })=>{
15
- const [inputText, setInputText] = useState('');
16
- const [menuState, setMenuState] = useState('closed');
17
- const [selectedField, setSelectedField] = useState(null);
18
- const [selectedOperator, setSelectedOperator] = useState(null);
19
- const [isFocused, setIsFocused] = useState(false);
20
- const [buildingMultiValue, setBuildingMultiValue] = useState(void 0);
21
- const [insertIndex, setInsertIndex] = useState(null);
22
- const [insertAfterConnector, setInsertAfterConnector] = useState(false);
23
- const effectiveInsertIndex = insertIndex ?? conditions.length;
24
- const effectiveInsertIndexRef = useRef(effectiveInsertIndex);
25
- effectiveInsertIndexRef.current = effectiveInsertIndex;
26
- const conditionsRef = useRef(conditions);
27
- conditionsRef.current = conditions;
28
- const conditionsLengthRef = useRef(conditions.length);
29
- conditionsLengthRef.current = conditions.length;
30
- const blurCommitRef = useRef(null);
31
- const segmentAttributeInputRef = useRef(null);
32
- const segmentOperatorInputRef = useRef(null);
33
- const segmentValueInputRef = useRef(null);
34
- const commitBuildingOnBlurRef = useRef(()=>false);
35
- const { menuPositioning, setMenuOffset, resetMenuOffset } = useMenuPositioning({
16
+ const state = useAutocompleteState({
17
+ conditions
18
+ });
19
+ const { inputText, setInputText, menuState, setMenuState, selectedField, setSelectedField, selectedOperator, setSelectedOperator, isFocused, setIsFocused, buildingMultiValue, setBuildingMultiValue, insertIndex, setInsertIndex, insertAfterConnector, setInsertAfterConnector, effectiveInsertIndex, effectiveInsertIndexRef, conditionsRef, conditionsLengthRef, blurCommitRef, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, commitBuildingOnBlurRef } = state;
20
+ const { menuPositioning, setMenuAnchor, resetMenuAnchor } = useMenuPositioning({
36
21
  containerRef,
37
22
  buildingChipRef,
38
23
  inputRef,
39
24
  isBuilding: null !== selectedField,
40
- insertIndex: effectiveInsertIndex
25
+ insertIndex: effectiveInsertIndex,
26
+ chipsCount: chips.length,
27
+ isMenuOpen: 'closed' !== menuState
41
28
  });
42
29
  const editing = useChipEditing({
43
30
  conditions,
44
31
  chips,
45
32
  fields,
46
- containerRef,
47
- setMenuOffset,
33
+ setMenuAnchor,
48
34
  setSelectedField,
49
35
  setSelectedOperator,
50
36
  setMenuState,
@@ -56,7 +42,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
56
42
  dateRange,
57
43
  containerRef,
58
44
  inputRef,
59
- resetMenuOffset,
45
+ resetMenuAnchor,
60
46
  setInputText,
61
47
  setSelectedField,
62
48
  setSelectedOperator,
@@ -83,6 +69,22 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
83
69
  setMenuState,
84
70
  setBuildingMultiValue
85
71
  });
72
+ const { switchEditSegment, removeEditingChip, stepBackBuildingMenu } = useChipCascade({
73
+ editing,
74
+ chips,
75
+ fields,
76
+ conditionsRef,
77
+ selectedField,
78
+ selectedOperator,
79
+ buildingMultiValue,
80
+ upsertCondition,
81
+ removeCondition,
82
+ resetState,
83
+ setInputText,
84
+ setMenuState,
85
+ setSelectedOperator,
86
+ setBuildingMultiValue
87
+ });
86
88
  const { handleInputChange, handleInputClick, handleKeyDown, menuRef } = useInputHandlers({
87
89
  inputText,
88
90
  menuState,
@@ -97,11 +99,12 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
97
99
  setInputText,
98
100
  setMenuState,
99
101
  setInsertIndex,
100
- resetMenuOffset,
102
+ resetMenuAnchor,
101
103
  removeConditionAtIndex,
102
104
  handleFieldSelect,
103
105
  handleOperatorSelect,
104
- handleCustomValueCommit
106
+ handleCustomValueCommit,
107
+ stepBackBuildingMenu
105
108
  });
106
109
  const { commitBuildingOnBlur, hasIncompleteBuilding } = useBlurCommit({
107
110
  selectedField,
@@ -132,7 +135,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
132
135
  hasIncompleteBuilding,
133
136
  setIsFocused,
134
137
  setMenuState,
135
- resetMenuOffset,
138
+ resetMenuAnchor,
136
139
  resetState
137
140
  });
138
141
  const { handleChipRemove, handleClear, handleGapClick, closeAutocompleteMenu } = useChipActions({
@@ -140,12 +143,24 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
140
143
  inputRef,
141
144
  removeCondition,
142
145
  clearAll,
143
- resetMenuOffset,
146
+ resetMenuAnchor,
144
147
  resetState,
145
148
  setInsertIndex,
146
149
  setInsertAfterConnector,
147
150
  setMenuState
148
151
  });
152
+ const { handleSegmentFilterChange, cancelSegmentEdit, handleMenuDiscard, handleBuildingChipClick } = useSegmentEditFlow({
153
+ editing,
154
+ selectedField,
155
+ selectedOperator,
156
+ buildingMultiValue,
157
+ resetState,
158
+ setSelectedField,
159
+ setSelectedOperator,
160
+ setInputText,
161
+ setMenuState,
162
+ setMenuAnchor
163
+ });
149
164
  const { isBuilding, buildingChipData, editingMultiValues, editingSingleValue, editingDateRange } = deriveAutocompleteValues({
150
165
  editingChipId: editing.editingChipId,
151
166
  selectedField,
@@ -155,56 +170,6 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
155
170
  dateRangeFromValue: dateRange.fromValue,
156
171
  segmentFilterText: editing.editingSegment === SEGMENT_VARIANT.value ? editing.segmentMenuFilterText : void 0
157
172
  });
158
- const handleSegmentFilterChange = useCallback((text)=>{
159
- const accept = selectedField?.acceptChar;
160
- const next = editing.editingSegment === SEGMENT_VARIANT.value && accept ? applyAcceptChar(text, accept) : text;
161
- editing.setSegmentFilterText(next);
162
- }, [
163
- selectedField,
164
- editing
165
- ]);
166
- const cancelSegmentEdit = useCallback(()=>{
167
- const isBuildingEdit = !editing.editingChipId && null !== editing.editingSegment;
168
- if (isBuildingEdit) {
169
- editing.clearEditing();
170
- setMenuState('closed');
171
- return;
172
- }
173
- setSelectedField(null);
174
- setSelectedOperator(null);
175
- editing.clearEditing();
176
- setMenuState('closed');
177
- }, [
178
- editing
179
- ]);
180
- const handleMenuDiscard = useCallback(()=>{
181
- const isBuildingEdit = !editing.editingChipId && null !== editing.editingSegment;
182
- if (isBuildingEdit) {
183
- editing.clearEditing();
184
- setMenuState('closed');
185
- return;
186
- }
187
- resetState();
188
- }, [
189
- editing,
190
- resetState
191
- ]);
192
- const handleBuildingChipClick = useCallback((segment, anchorRect)=>{
193
- if (!selectedField) return;
194
- const containerRect = containerRef.current?.getBoundingClientRect();
195
- setMenuOffset(containerRect ? anchorRect.left - containerRect.left : 0);
196
- const initialText = segment === SEGMENT_VARIANT.attribute ? selectedField.label : segment === SEGMENT_VARIANT.operator ? selectedOperator ? getOperatorLabel(selectedOperator, selectedField.type) : '' : buildingMultiValue ?? '';
197
- editing.startBuildingEdit(segment, initialText);
198
- setInputText('');
199
- setMenuState(segment === SEGMENT_VARIANT.attribute ? 'field' : segment === SEGMENT_VARIANT.operator ? 'operator' : 'value');
200
- }, [
201
- selectedField,
202
- selectedOperator,
203
- buildingMultiValue,
204
- containerRef,
205
- setMenuOffset,
206
- editing
207
- ]);
208
173
  return {
209
174
  inputText,
210
175
  menuState,
@@ -229,6 +194,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
229
194
  resetAutocompleteState: resetState,
230
195
  handleChipClick: editing.handleChipClick,
231
196
  handleBuildingChipClick,
197
+ switchEditSegment,
198
+ removeEditingChip,
232
199
  handleConnectorChange: setConnectorValue,
233
200
  handleChipRemove,
234
201
  handleClear,
@@ -6,32 +6,30 @@ interface UseFocusManagementDeps {
6
6
  isFocused: boolean;
7
7
  conditionsLength: number;
8
8
  inputText: string;
9
- /** In-progress building-chip state — needed so refocus resumes at the next
10
- * missing segment instead of (incorrectly) reopening the field menu. */
9
+ /** In-progress building-chip state — refocus resumes at next missing segment. */
11
10
  selectedField: FieldMetadata | null;
12
11
  selectedOperator: FilterOperator | null;
13
12
  containerRef: RefObject<HTMLElement | null>;
14
13
  inputRef: RefObject<HTMLInputElement | null>;
15
14
  editingSegment: ChipSegment | null;
16
- /** Direct ref to the attribute segment <input> when editing — avoids querySelector. */
15
+ /** Direct ref to the attribute segment input when editing. */
17
16
  segmentAttributeInputRef: RefObject<HTMLInputElement | null>;
18
- /** Direct ref to the operator segment <input> when editing — avoids querySelector. */
17
+ /** Direct ref to the operator segment input when editing. */
19
18
  segmentOperatorInputRef: RefObject<HTMLInputElement | null>;
20
- /** Direct ref to the value segment <input> when editing — avoids querySelector. */
19
+ /** Direct ref to the value segment input when editing. */
21
20
  segmentValueInputRef: RefObject<HTMLInputElement | null>;
22
- /** Ref set by FilterInputValueMenu — calls commitChecked() for multi-select before blur reset. Returns true if committed. */
21
+ /** Set by FilterInputValueMenu — commits multi-select checked values on blur. */
23
22
  blurCommitRef: RefObject<(() => boolean) | null>;
24
- /** Tries to commit a building chip's freeform value on blur. Returns true if committed. */
23
+ /** Commits a building chip's freeform value on blur. Returns true if committed. */
25
24
  commitBuildingOnBlur: () => boolean;
26
- /** True if there's an in-progress building chip that the blur/close path
27
- * must preserve (skip resetState). */
25
+ /** In-progress building chip exists blur/close must preserve it (skip resetState). */
28
26
  hasIncompleteBuilding: () => boolean;
29
27
  setIsFocused: (focused: boolean) => void;
30
28
  setMenuState: (state: MenuState) => void;
31
- resetMenuOffset: () => void;
29
+ resetMenuAnchor: () => void;
32
30
  resetState: (continueBuilding?: boolean) => void;
33
31
  }
34
- export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuOffset, resetState, }: UseFocusManagementDeps) => {
32
+ export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuAnchor, resetState, }: UseFocusManagementDeps) => {
35
33
  handleFocus: (e: FocusEvent) => void;
36
34
  handleBlur: (e: FocusEvent) => void;
37
35
  };
@@ -1,7 +1,7 @@
1
- import { useCallback, useEffect, useRef } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
3
  import { isMenuRelated, nextBuildingMenu } from "../../lib/index.js";
4
- const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuOffset, resetState })=>{
4
+ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuAnchor, resetState })=>{
5
5
  const handleFocus = useCallback((e)=>{
6
6
  if (e.target?.closest?.('[data-slot="filter-input-connector-chip"]')) return;
7
7
  setIsFocused(true);
@@ -46,10 +46,10 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
46
46
  return;
47
47
  }
48
48
  if (selectedField) {
49
- resetMenuOffset();
49
+ resetMenuAnchor();
50
50
  setMenuState(nextBuildingMenu(selectedField, selectedOperator));
51
51
  } else if (0 === conditionsLength && '' === inputText) {
52
- resetMenuOffset();
52
+ resetMenuAnchor();
53
53
  setMenuState('field');
54
54
  }
55
55
  prevFocusedRef.current = isFocused;
@@ -60,9 +60,18 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
60
60
  selectedField,
61
61
  selectedOperator,
62
62
  editingSegment,
63
- resetMenuOffset,
63
+ resetMenuAnchor,
64
64
  setMenuState
65
65
  ]);
66
+ const segmentInputRefsMap = useMemo(()=>({
67
+ [SEGMENT_VARIANT.attribute]: segmentAttributeInputRef,
68
+ [SEGMENT_VARIANT.operator]: segmentOperatorInputRef,
69
+ [SEGMENT_VARIANT.value]: segmentValueInputRef
70
+ }), [
71
+ segmentAttributeInputRef,
72
+ segmentOperatorInputRef,
73
+ segmentValueInputRef
74
+ ]);
66
75
  useEffect(()=>{
67
76
  if ('closed' === menuState) return;
68
77
  if (!isFocused) return;
@@ -74,18 +83,11 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
74
83
  if (!container) return;
75
84
  const active = document.activeElement;
76
85
  if (active && !container.contains(active) && !isMenuRelated(active)) return;
77
- if (editingSegment) {
78
- const segmentInputRefs = {
79
- [SEGMENT_VARIANT.attribute]: segmentAttributeInputRef,
80
- [SEGMENT_VARIANT.operator]: segmentOperatorInputRef,
81
- [SEGMENT_VARIANT.value]: segmentValueInputRef
82
- };
83
- const segmentInput = segmentInputRefs[editingSegment]?.current ?? null;
84
- if (segmentInput && document.activeElement !== segmentInput) {
85
- segmentInput.focus();
86
- segmentInput.select();
87
- }
88
- } else if (document.activeElement !== inputRef.current) inputRef.current?.focus();
86
+ const dest = editingSegment ? segmentInputRefsMap[editingSegment]?.current ?? null : inputRef.current;
87
+ if (!dest) return;
88
+ if (document.activeElement === dest) return;
89
+ dest.focus();
90
+ if (editingSegment) dest.select();
89
91
  });
90
92
  });
91
93
  return ()=>{
@@ -98,9 +100,34 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
98
100
  inputRef,
99
101
  editingSegment,
100
102
  containerRef,
101
- segmentAttributeInputRef,
102
- segmentOperatorInputRef,
103
- segmentValueInputRef
103
+ segmentInputRefsMap
104
+ ]);
105
+ useEffect(()=>{
106
+ if ('closed' === menuState) return;
107
+ if (!isFocused) return;
108
+ const handleFocusIn = (e)=>{
109
+ const targetEl = e.target;
110
+ if (!targetEl) return;
111
+ const menu = targetEl.closest('[data-filter-input-menu]');
112
+ if (!menu) return;
113
+ if (menu.querySelector('[data-scope="date-picker"]')) return;
114
+ const tag = targetEl.tagName;
115
+ if ('BUTTON' === tag || 'INPUT' === tag || 'TEXTAREA' === tag || 'SELECT' === tag) return;
116
+ const dest = editingSegment ? segmentInputRefsMap[editingSegment]?.current : inputRef.current;
117
+ if (!dest) return;
118
+ if (document.activeElement === dest) return;
119
+ dest.focus({
120
+ preventScroll: true
121
+ });
122
+ };
123
+ document.addEventListener('focusin', handleFocusIn);
124
+ return ()=>document.removeEventListener('focusin', handleFocusIn);
125
+ }, [
126
+ menuState,
127
+ isFocused,
128
+ editingSegment,
129
+ inputRef,
130
+ segmentInputRefsMap
104
131
  ]);
105
132
  return {
106
133
  handleFocus,
@@ -1,11 +1,11 @@
1
1
  import type { ChangeEvent, KeyboardEvent, MutableRefObject, RefObject } from 'react';
2
2
  import type { Condition, FieldMetadata, FilterOperator, MenuState } from '../../types';
3
+ type BuildingStep = 'field' | 'operator' | 'value';
3
4
  interface UseInputHandlersDeps {
4
5
  inputText: string;
5
6
  menuState: MenuState;
6
7
  selectedField: FieldMetadata | null;
7
- /** Needed so a click into the main input while a building chip is alive
8
- * resumes at the next missing segment instead of doing nothing. */
8
+ /** Lets click resume at the next missing segment of a live building chip. */
9
9
  selectedOperator: FilterOperator | null;
10
10
  isFocused: boolean;
11
11
  fields: FieldMetadata[];
@@ -16,13 +16,15 @@ interface UseInputHandlersDeps {
16
16
  setInputText: (text: string) => void;
17
17
  setMenuState: (state: MenuState) => void;
18
18
  setInsertIndex: (fn: (prev: number | null) => number) => void;
19
- resetMenuOffset: () => void;
19
+ resetMenuAnchor: () => void;
20
20
  removeConditionAtIndex: (index: number) => void;
21
21
  handleFieldSelect: (field: FieldMetadata) => void;
22
22
  handleOperatorSelect: (operator: FilterOperator) => void;
23
23
  handleCustomValueCommit: (text: string) => void;
24
+ /** Step building chip back one segment; tears it down at the field step. */
25
+ stepBackBuildingMenu: (current: BuildingStep) => void;
24
26
  }
25
- export declare const useInputHandlers: ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, }: UseInputHandlersDeps) => {
27
+ export declare const useInputHandlers: ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuAnchor, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, stepBackBuildingMenu, }: UseInputHandlersDeps) => {
26
28
  handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
27
29
  handleInputClick: () => void;
28
30
  handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasFieldValues, nextBuildingMenu } from "../../lib/index.js";
3
- const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
3
+ const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuAnchor, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, stepBackBuildingMenu })=>{
4
4
  const menuRef = useRef(null);
5
5
  const handleInputChange = useCallback((e)=>{
6
6
  let text = e.target.value;
@@ -22,18 +22,24 @@ const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperato
22
22
  const handleInputClick = useCallback(()=>{
23
23
  inputRef.current?.focus();
24
24
  if ('closed' !== menuState) return;
25
- resetMenuOffset();
25
+ resetMenuAnchor();
26
26
  setMenuState(nextBuildingMenu(selectedField, selectedOperator));
27
27
  }, [
28
28
  menuState,
29
29
  selectedField,
30
30
  selectedOperator,
31
- resetMenuOffset,
31
+ resetMenuAnchor,
32
32
  inputRef,
33
33
  setMenuState
34
34
  ]);
35
35
  const handleKeyDown = useCallback((e)=>{
36
- if ('ArrowDown' === e.key && 'closed' !== menuState) {
36
+ if ('ArrowDown' === e.key) {
37
+ if ('closed' === menuState) {
38
+ e.preventDefault();
39
+ resetMenuAnchor();
40
+ setMenuState(nextBuildingMenu(selectedField, selectedOperator));
41
+ return;
42
+ }
37
43
  e.preventDefault();
38
44
  menuRef.current?.focus();
39
45
  return;
@@ -72,23 +78,32 @@ const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperato
72
78
  handleCustomValueCommit(inputText);
73
79
  return;
74
80
  }
75
- if ('Backspace' === e.key && !e.repeat && '' === inputText && conditionsLengthRef.current > 0) {
76
- e.preventDefault();
77
- const removeIdx = effectiveInsertIndexRef.current - 1;
78
- if (removeIdx >= 0 && !conditionsRef.current[removeIdx]?.disabled) {
79
- removeConditionAtIndex(removeIdx);
80
- setInsertIndex((prev)=>{
81
- const eff = prev ?? conditionsLengthRef.current;
82
- return eff > 0 ? eff - 1 : 0;
83
- });
81
+ if ('Backspace' === e.key && !e.repeat && '' === inputText) {
82
+ if (selectedField && ('value' === menuState || 'operator' === menuState || 'field' === menuState)) {
83
+ e.preventDefault();
84
+ stepBackBuildingMenu(menuState);
85
+ return;
86
+ }
87
+ if (conditionsLengthRef.current > 0) {
88
+ e.preventDefault();
89
+ const removeIdx = effectiveInsertIndexRef.current - 1;
90
+ if (removeIdx >= 0 && !conditionsRef.current[removeIdx]?.disabled) {
91
+ removeConditionAtIndex(removeIdx);
92
+ setInsertIndex((prev)=>{
93
+ const eff = prev ?? conditionsLengthRef.current;
94
+ return eff > 0 ? eff - 1 : 0;
95
+ });
96
+ }
97
+ resetMenuAnchor();
98
+ setMenuState('field');
84
99
  }
85
- setMenuState('closed');
86
100
  }
87
101
  }, [
88
102
  inputText,
89
103
  removeConditionAtIndex,
90
104
  menuState,
91
105
  selectedField,
106
+ selectedOperator,
92
107
  fields,
93
108
  handleFieldSelect,
94
109
  handleOperatorSelect,
@@ -98,7 +113,9 @@ const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperato
98
113
  setInsertIndex,
99
114
  conditionsRef,
100
115
  conditionsLengthRef,
101
- effectiveInsertIndexRef
116
+ effectiveInsertIndexRef,
117
+ stepBackBuildingMenu,
118
+ resetMenuAnchor
102
119
  ]);
103
120
  return {
104
121
  handleInputChange,
@@ -0,0 +1 @@
1
+ export { useMenuFlow } from './useMenuFlow';
@@ -0,0 +1,2 @@
1
+ import { useMenuFlow } from "./useMenuFlow.js";
2
+ export { useMenuFlow };
@@ -1,13 +1,19 @@
1
1
  import type { RefObject } from 'react';
2
- import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
- import type { Condition, FieldMetadata, FilterOperator, MenuState, UpsertCondition } from '../../types';
4
- interface MenuFlowDeps {
2
+ import type { ChipSegment } from '../../../FilterInputField/FilterInputChip';
3
+ import type { Condition, FieldMetadata, FilterOperator, MenuState, UpsertCondition } from '../../../types';
4
+ export interface MenuFlowDeps {
5
5
  editing: {
6
6
  editingChipId: string | null;
7
- editingSegment: string | null;
7
+ editingSegment: ChipSegment | null;
8
+ /** Pre-derived chipId === null && segment !== null marker — single source
9
+ * of truth, avoids re-deriving in every consumer. */
10
+ isBuildingEdit: boolean;
8
11
  setEditingSegment: (segment: ChipSegment | null) => void;
9
12
  setSegmentFilterText: (text: string) => void;
10
13
  resetSegmentTyping: () => void;
14
+ /** Switch inline-edit to another segment of the same committed chip,
15
+ * loading its text and resetting the user-typed flag in one step. */
16
+ switchEditSegment: (segment: ChipSegment, currentText: string) => void;
11
17
  /** Exit inline-edit and the building-edit marker. Called when switching
12
18
  * filter/operator in the building chip lands on the next menu. */
13
19
  clearEditing: () => void;
@@ -31,17 +37,8 @@ interface MenuFlowDeps {
31
37
  setMenuState: (state: MenuState) => void;
32
38
  setBuildingMultiValue: (val: string | undefined) => void;
33
39
  }
34
- export declare const useMenuFlow: ({ editing, selectedField, selectedOperator, fields, inputRef, insertIndex, upsertCondition, conditions, resetState, commitBuildingOnBlur, dateRange, setSelectedField, setSelectedOperator, setInputText, setMenuState, setBuildingMultiValue, }: MenuFlowDeps) => {
35
- handleMenuClose: () => void;
36
- handleFieldSelect: (field: FieldMetadata) => void;
37
- handleOperatorSelect: (operator: FilterOperator) => void;
38
- handleValueSelect: (val: string | number | boolean) => void;
39
- handleMultiCommit: (values: Array<string | number | boolean>) => void;
40
- handleBuildingValueChange: (preview: string | undefined) => void;
41
- handleMultiSelectToggle: () => void;
42
- handleRangeSelect: (from: string, to: string) => void;
43
- handleCustomValueCommit: (customText: string) => void;
44
- handleCustomOperatorCommit: (customText: string) => void;
45
- handleCustomAttributeCommit: (customText: string) => void;
46
- };
47
- export {};
40
+ /** Shared shape: each sub-hook receives the same deps plus a ref to `conditions`
41
+ * that stays fresh without forcing callbacks to recreate on every keystroke. */
42
+ export interface MenuFlowInternalDeps extends MenuFlowDeps {
43
+ conditionsRef: RefObject<Condition[]>;
44
+ }
@@ -0,0 +1,11 @@
1
+ import type { FieldMetadata } from '../../../types';
2
+ import type { MenuFlowInternalDeps } from './types';
3
+ /**
4
+ * Attribute-segment handlers. `handleFieldSelect` is the menu-click path,
5
+ * `handleCustomAttributeCommit` is the keyboard-typed path — they share
6
+ * post-cascade and shape-preservation logic via mutual delegation.
7
+ */
8
+ export declare const useFieldFlow: ({ editing, selectedOperator, fields, upsertCondition, conditionsRef, resetState, setSelectedField, setSelectedOperator, setInputText, setMenuState, }: MenuFlowInternalDeps) => {
9
+ handleFieldSelect: (field: FieldMetadata) => void;
10
+ handleCustomAttributeCommit: (customText: string) => void;
11
+ };
@@ -0,0 +1,95 @@
1
+ import { useCallback } from "react";
2
+ import { SEGMENT_VARIANT } from "../../../FilterInputField/FilterInputChip/index.js";
3
+ import { chipIdToConditionIndex, isOperatorAllowedForField, validateValueForField } from "../../../lib/index.js";
4
+ const useFieldFlow = ({ editing, selectedOperator, fields, upsertCondition, conditionsRef, resetState, setSelectedField, setSelectedOperator, setInputText, setMenuState })=>{
5
+ const { editingChipId, editingSegment, isBuildingEdit, switchEditSegment, clearEditing } = editing;
6
+ const handleFieldSelect = useCallback((field)=>{
7
+ if (editingChipId && editingSegment === SEGMENT_VARIANT.attribute) {
8
+ const idx = chipIdToConditionIndex(editingChipId);
9
+ const condition = null !== idx ? conditionsRef.current[idx] : null;
10
+ if (condition) {
11
+ if (!condition.operator) {
12
+ upsertCondition(field, void 0, condition.value, editingChipId, void 0, void 0, condition.dateOrigin);
13
+ setSelectedField(field);
14
+ setSelectedOperator(null);
15
+ switchEditSegment(SEGMENT_VARIANT.operator, '');
16
+ setMenuState('operator');
17
+ return;
18
+ }
19
+ const hasValueError = validateValueForField(field, condition.value);
20
+ upsertCondition(field, condition.operator, condition.value, editingChipId, void 0, hasValueError ? SEGMENT_VARIANT.value : void 0, condition.dateOrigin);
21
+ }
22
+ resetState();
23
+ return;
24
+ }
25
+ if (isBuildingEdit && editingSegment === SEGMENT_VARIANT.attribute) {
26
+ setSelectedField(field);
27
+ const keepOperator = selectedOperator ? isOperatorAllowedForField(field, selectedOperator) : false;
28
+ if (!keepOperator) setSelectedOperator(null);
29
+ clearEditing();
30
+ setMenuState(keepOperator ? 'value' : 'operator');
31
+ return;
32
+ }
33
+ setSelectedField(field);
34
+ setInputText('');
35
+ setMenuState('operator');
36
+ }, [
37
+ editingChipId,
38
+ editingSegment,
39
+ isBuildingEdit,
40
+ switchEditSegment,
41
+ clearEditing,
42
+ selectedOperator,
43
+ conditionsRef,
44
+ upsertCondition,
45
+ resetState,
46
+ setSelectedField,
47
+ setSelectedOperator,
48
+ setInputText,
49
+ setMenuState
50
+ ]);
51
+ const handleCustomAttributeCommit = useCallback((customText)=>{
52
+ if (!customText.trim()) return;
53
+ const trimmed = customText.trim();
54
+ const matchedField = fields.find((f)=>f.label.toLowerCase() === trimmed.toLowerCase() || f.name.toLowerCase() === trimmed.toLowerCase());
55
+ if (!editingChipId) {
56
+ if (matchedField) handleFieldSelect(matchedField);
57
+ return;
58
+ }
59
+ const idx = chipIdToConditionIndex(editingChipId);
60
+ const condition = null !== idx ? conditionsRef.current[idx] : null;
61
+ if (!condition) return;
62
+ if (matchedField) return void handleFieldSelect(matchedField);
63
+ const syntheticField = {
64
+ name: trimmed,
65
+ label: trimmed,
66
+ type: 'string'
67
+ };
68
+ if (!condition.operator) {
69
+ upsertCondition(syntheticField, void 0, condition.value, editingChipId, void 0, SEGMENT_VARIANT.attribute, condition.dateOrigin);
70
+ setSelectedField(syntheticField);
71
+ setSelectedOperator(null);
72
+ switchEditSegment(SEGMENT_VARIANT.operator, '');
73
+ setMenuState('operator');
74
+ return;
75
+ }
76
+ upsertCondition(syntheticField, condition.operator, condition.value, editingChipId, void 0, SEGMENT_VARIANT.attribute, condition.dateOrigin);
77
+ resetState();
78
+ }, [
79
+ editingChipId,
80
+ switchEditSegment,
81
+ fields,
82
+ conditionsRef,
83
+ upsertCondition,
84
+ resetState,
85
+ handleFieldSelect,
86
+ setSelectedField,
87
+ setSelectedOperator,
88
+ setMenuState
89
+ ]);
90
+ return {
91
+ handleFieldSelect,
92
+ handleCustomAttributeCommit
93
+ };
94
+ };
95
+ export { useFieldFlow };