@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,27 +1,5 @@
1
- import { MIN_DATE_STRING_LENGTH } from "../../FilterInputMenu/FilterInputDateValueMenu/constants.js";
2
- import { chipIdToConditionIndex, getFieldValues, hasStaticAllowlist, isDatePreset } from "../../lib/index.js";
3
- const findMatchingFieldValue = (fieldValues, text)=>fieldValues.find((v)=>v.label.toLowerCase() === text.toLowerCase() || String(v.value).toLowerCase() === text.toLowerCase());
4
- const isValidFieldValue = (fieldValues, v)=>fieldValues.some((opt)=>opt.value === v || String(opt.value).toLowerCase() === String(v).toLowerCase());
5
- const getInvalidValueIndices = (field, values)=>{
6
- if (field.validate) return values.reduce((acc, v, idx)=>{
7
- if (field.validate(v)) acc.push(idx);
8
- return acc;
9
- }, []);
10
- if (!hasStaticAllowlist(field)) return [];
11
- const fv = getFieldValues(field);
12
- if (0 === fv.length) return [];
13
- return values.reduce((acc, v, idx)=>{
14
- if (!isValidFieldValue(fv, v)) acc.push(idx);
15
- return acc;
16
- }, []);
17
- };
18
- const validateValueForField = (field, value)=>{
19
- if (null == value) return false;
20
- const values = Array.isArray(value) ? value : [
21
- value
22
- ];
23
- return getInvalidValueIndices(field, values).length > 0;
24
- };
1
+ import { MIN_DATE_STRING_LENGTH } from "../../../FilterInputMenu/FilterInputDateValueMenu/constants.js";
2
+ import { chipIdToConditionIndex, findMatchingFieldValue, getFieldValues, getInvalidValueIndices, hasStaticAllowlist, isDatePreset } from "../../../lib/index.js";
25
3
  const resolveFieldValue = (field, text)=>{
26
4
  const match = findMatchingFieldValue(getFieldValues(field), text);
27
5
  return match ? match.value : text;
@@ -90,4 +68,4 @@ const resolveDateValue = (trimmed, editingChipId, conditions)=>{
90
68
  dateOrigin
91
69
  };
92
70
  };
93
- export { displayDateToIso, getInvalidValueIndices, isValidFieldValue, resolveDateRangeValue, resolveDateValue, resolveFieldValue, resolveMultiValues, resolveSingleValue, validateValueForField };
71
+ export { displayDateToIso, resolveDateRangeValue, resolveDateValue, resolveFieldValue, resolveMultiValues, resolveSingleValue };
@@ -0,0 +1,36 @@
1
+ import type { Condition, FieldMetadata, FilterOperator, MenuState } from '../../types';
2
+ interface UseAutocompleteStateOptions {
3
+ conditions: Condition[];
4
+ }
5
+ /**
6
+ * Owns the useState/useRef surface of the autocomplete hook. Refs mirror
7
+ * state so sub-hooks read synchronously without recreating callbacks.
8
+ */
9
+ export declare const useAutocompleteState: ({ conditions }: UseAutocompleteStateOptions) => {
10
+ inputText: string;
11
+ setInputText: import("react").Dispatch<import("react").SetStateAction<string>>;
12
+ menuState: MenuState;
13
+ setMenuState: import("react").Dispatch<import("react").SetStateAction<MenuState>>;
14
+ selectedField: FieldMetadata | null;
15
+ setSelectedField: import("react").Dispatch<import("react").SetStateAction<FieldMetadata | null>>;
16
+ selectedOperator: FilterOperator | null;
17
+ setSelectedOperator: import("react").Dispatch<import("react").SetStateAction<FilterOperator | null>>;
18
+ isFocused: boolean;
19
+ setIsFocused: import("react").Dispatch<import("react").SetStateAction<boolean>>;
20
+ buildingMultiValue: string | undefined;
21
+ setBuildingMultiValue: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
22
+ insertIndex: number | null;
23
+ setInsertIndex: import("react").Dispatch<import("react").SetStateAction<number | null>>;
24
+ insertAfterConnector: boolean;
25
+ setInsertAfterConnector: import("react").Dispatch<import("react").SetStateAction<boolean>>;
26
+ effectiveInsertIndex: number;
27
+ effectiveInsertIndexRef: import("react").RefObject<number>;
28
+ conditionsRef: import("react").RefObject<Condition[]>;
29
+ conditionsLengthRef: import("react").RefObject<number>;
30
+ blurCommitRef: import("react").RefObject<(() => boolean) | null>;
31
+ segmentAttributeInputRef: import("react").RefObject<HTMLInputElement | null>;
32
+ segmentOperatorInputRef: import("react").RefObject<HTMLInputElement | null>;
33
+ segmentValueInputRef: import("react").RefObject<HTMLInputElement | null>;
34
+ commitBuildingOnBlurRef: import("react").RefObject<() => boolean>;
35
+ };
36
+ export {};
@@ -0,0 +1,53 @@
1
+ import { useLayoutEffect, useRef, useState } from "react";
2
+ const useAutocompleteState = ({ conditions })=>{
3
+ const [inputText, setInputText] = useState('');
4
+ const [menuState, setMenuState] = useState('closed');
5
+ const [selectedField, setSelectedField] = useState(null);
6
+ const [selectedOperator, setSelectedOperator] = useState(null);
7
+ const [isFocused, setIsFocused] = useState(false);
8
+ const [buildingMultiValue, setBuildingMultiValue] = useState(void 0);
9
+ const [insertIndex, setInsertIndex] = useState(null);
10
+ const [insertAfterConnector, setInsertAfterConnector] = useState(false);
11
+ const effectiveInsertIndex = insertIndex ?? conditions.length;
12
+ const effectiveInsertIndexRef = useRef(effectiveInsertIndex);
13
+ const conditionsRef = useRef(conditions);
14
+ const conditionsLengthRef = useRef(conditions.length);
15
+ useLayoutEffect(()=>{
16
+ effectiveInsertIndexRef.current = effectiveInsertIndex;
17
+ conditionsRef.current = conditions;
18
+ conditionsLengthRef.current = conditions.length;
19
+ });
20
+ const blurCommitRef = useRef(null);
21
+ const segmentAttributeInputRef = useRef(null);
22
+ const segmentOperatorInputRef = useRef(null);
23
+ const segmentValueInputRef = useRef(null);
24
+ const commitBuildingOnBlurRef = useRef(()=>false);
25
+ return {
26
+ inputText,
27
+ setInputText,
28
+ menuState,
29
+ setMenuState,
30
+ selectedField,
31
+ setSelectedField,
32
+ selectedOperator,
33
+ setSelectedOperator,
34
+ isFocused,
35
+ setIsFocused,
36
+ buildingMultiValue,
37
+ setBuildingMultiValue,
38
+ insertIndex,
39
+ setInsertIndex,
40
+ insertAfterConnector,
41
+ setInsertAfterConnector,
42
+ effectiveInsertIndex,
43
+ effectiveInsertIndexRef,
44
+ conditionsRef,
45
+ conditionsLengthRef,
46
+ blurCommitRef,
47
+ segmentAttributeInputRef,
48
+ segmentOperatorInputRef,
49
+ segmentValueInputRef,
50
+ commitBuildingOnBlurRef
51
+ };
52
+ };
53
+ export { useAutocompleteState };
@@ -9,25 +9,14 @@ interface UseBlurCommitDeps {
9
9
  handleCustomValueCommit: (text: string) => void;
10
10
  upsertCondition: UpsertCondition;
11
11
  resetState: () => void;
12
- /**
13
- * Indirection ref consumed by useMenuFlow to break the circular dependency
14
- * useMenuFlow ↔ useBlurCommit. The hook writes the latest commit fn here
15
- * each render; useMenuFlow calls through it via `() => ref.current()`.
16
- */
12
+ /** Indirection ref breaking the useMenuFlow ↔ useBlurCommit cycle. */
17
13
  commitBuildingOnBlurRef: RefObject<() => boolean>;
18
14
  }
19
15
  /**
20
- * Commit logic for an incomplete "building" chip on blur or menu close.
21
- *
22
- * Instead of discarding the in-progress chip when the user blurs or the menu
23
- * closes, this either commits the typed text as a custom value (when there's an
24
- * operator + text) or persists it as an error chip (so it remains visible and
25
- * editable). Uses fresh refs to avoid stale closures.
26
- *
27
- * Re-entry guard: snapshots and clears the refs synchronously so concurrent
28
- * callers (two `onOpenChange(false)` from Ark UI menus during a state transition,
29
- * plus a blur in the same tick) short-circuit on re-entry instead of creating
30
- * duplicate error chips.
16
+ * Commit an incomplete building chip on blur/menu-close instead of discarding:
17
+ * commit typed text as custom value, or persist as an error chip so it stays
18
+ * editable. Re-entry-guarded: refs cleared synchronously so concurrent callers
19
+ * (multiple onOpenChange + blur in one tick) don't create duplicate chips.
31
20
  */
32
21
  export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, }: UseBlurCommitDeps) => {
33
22
  commitBuildingOnBlur: () => boolean;
@@ -1,12 +1,14 @@
1
- import { useCallback, useRef } from "react";
1
+ import { useCallback, useLayoutEffect, useRef } from "react";
2
2
  import { isBuildingComplete, isNoValueOperator } from "../../lib/index.js";
3
3
  const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef })=>{
4
4
  const selectedFieldRef = useRef(selectedField);
5
- selectedFieldRef.current = selectedField;
6
5
  const selectedOperatorRef = useRef(selectedOperator);
7
- selectedOperatorRef.current = selectedOperator;
8
6
  const inputTextRef = useRef(inputText);
9
- inputTextRef.current = inputText;
7
+ useLayoutEffect(()=>{
8
+ selectedFieldRef.current = selectedField;
9
+ selectedOperatorRef.current = selectedOperator;
10
+ inputTextRef.current = inputText;
11
+ });
10
12
  const committingRef = useRef(false);
11
13
  const commitBuildingOnBlur = useCallback(()=>{
12
14
  if (committingRef.current) return false;
@@ -39,7 +41,12 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
39
41
  resetState,
40
42
  effectiveInsertIndexRef
41
43
  ]);
42
- commitBuildingOnBlurRef.current = commitBuildingOnBlur;
44
+ useLayoutEffect(()=>{
45
+ commitBuildingOnBlurRef.current = commitBuildingOnBlur;
46
+ }, [
47
+ commitBuildingOnBlur,
48
+ commitBuildingOnBlurRef
49
+ ]);
43
50
  const hasIncompleteBuilding = useCallback(()=>null !== selectedFieldRef.current && !editingChipId, [
44
51
  editingChipId
45
52
  ]);
@@ -5,7 +5,7 @@ interface UseChipActionsDeps {
5
5
  inputRef: RefObject<HTMLInputElement | null>;
6
6
  removeCondition: (chipId: string) => void;
7
7
  clearAll: () => void;
8
- resetMenuOffset: () => void;
8
+ resetMenuAnchor: () => void;
9
9
  resetState: () => void;
10
10
  setInsertIndex: Dispatch<SetStateAction<number | null>>;
11
11
  setInsertAfterConnector: Dispatch<SetStateAction<boolean>>;
@@ -18,7 +18,7 @@ interface UseChipActionsDeps {
18
18
  * - clicking a gap between chips to insert a new condition there
19
19
  * - closing the menu (used by external consumers like the connector chip)
20
20
  */
21
- export declare const useChipActions: ({ effectiveInsertIndexRef, inputRef, removeCondition, clearAll, resetMenuOffset, resetState, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseChipActionsDeps) => {
21
+ export declare const useChipActions: ({ effectiveInsertIndexRef, inputRef, removeCondition, clearAll, resetMenuAnchor, resetState, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseChipActionsDeps) => {
22
22
  handleChipRemove: (chipId: string) => void;
23
23
  handleClear: () => void;
24
24
  handleGapClick: (conditionIndex: number, afterConnector: boolean) => void;
@@ -1,17 +1,17 @@
1
1
  import { useCallback } from "react";
2
2
  import { flushSync } from "react-dom";
3
3
  import { chipIdToConditionIndex } from "../../lib/index.js";
4
- const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, clearAll, resetMenuOffset, resetState, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
4
+ const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, clearAll, resetMenuAnchor, resetState, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
5
5
  const handleChipRemove = useCallback((chipId)=>{
6
6
  const chipCondIdx = chipIdToConditionIndex(chipId);
7
7
  if (null !== chipCondIdx && chipCondIdx < effectiveInsertIndexRef.current) setInsertIndex((prev)=>null != prev ? prev - 1 : prev);
8
8
  removeCondition(chipId);
9
- resetMenuOffset();
9
+ resetMenuAnchor();
10
10
  setMenuState('closed');
11
11
  inputRef.current?.focus();
12
12
  }, [
13
13
  removeCondition,
14
- resetMenuOffset,
14
+ resetMenuAnchor,
15
15
  inputRef,
16
16
  effectiveInsertIndexRef,
17
17
  setInsertIndex,
@@ -29,14 +29,14 @@ const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, cl
29
29
  flushSync(()=>{
30
30
  setInsertIndex(conditionIndex);
31
31
  setInsertAfterConnector(afterConnector);
32
- resetMenuOffset();
32
+ resetMenuAnchor();
33
33
  setMenuState('closed');
34
34
  });
35
35
  setMenuState('field');
36
36
  inputRef.current?.focus();
37
37
  }, [
38
38
  resetState,
39
- resetMenuOffset,
39
+ resetMenuAnchor,
40
40
  inputRef,
41
41
  setInsertIndex,
42
42
  setInsertAfterConnector,
@@ -0,0 +1,44 @@
1
+ import type { MutableRefObject } from 'react';
2
+ import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
+ import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
4
+ interface UseChipCascadeOptions {
5
+ editing: {
6
+ editingChipId: string | null;
7
+ editingSegment: ChipSegment | null;
8
+ isBuildingEdit: boolean;
9
+ startBuildingEdit: (segment: ChipSegment, currentText: string) => void;
10
+ switchEditSegment: (segment: ChipSegment, currentText: string) => void;
11
+ };
12
+ chips: FilterInputChipData[];
13
+ fields: FieldMetadata[];
14
+ conditionsRef: MutableRefObject<Condition[]>;
15
+ selectedField: FieldMetadata | null;
16
+ selectedOperator: FilterOperator | null;
17
+ buildingMultiValue: string | undefined;
18
+ upsertCondition: UpsertCondition;
19
+ removeCondition: (chipId: string) => void;
20
+ resetState: (continueBuilding?: boolean) => void;
21
+ setInputText: (text: string) => void;
22
+ setMenuState: (state: MenuState) => void;
23
+ setSelectedOperator: (op: FilterOperator | null) => void;
24
+ setBuildingMultiValue: (val: string | undefined) => void;
25
+ }
26
+ /**
27
+ * Backspace-driven cascade for chip segments. Bundles three closely related
28
+ * actions so consumers don't have to wire them piecemeal:
29
+ *
30
+ * - `switchEditSegment` — walks inline-edit one segment to the left (value
31
+ * → operator → attribute). The source segment's chip data is cleared so
32
+ * the just-emptied text does not re-render after the switch.
33
+ * - `removeEditingChip` — drops the chip currently being edited (building
34
+ * reset or committed `removeCondition` + insertIndex bookkeeping).
35
+ * - `stepBackBuildingMenu` — from the main input during building, enters
36
+ * inline-edit on the previous segment with its text pre-selected, mirroring
37
+ * the segment-click + cascade UX.
38
+ */
39
+ export declare const useChipCascade: ({ editing, chips, fields, conditionsRef, selectedField, selectedOperator, buildingMultiValue, upsertCondition, removeCondition, resetState, setInputText, setMenuState, setSelectedOperator, setBuildingMultiValue, }: UseChipCascadeOptions) => {
40
+ switchEditSegment: (targetSegment: ChipSegment) => boolean;
41
+ removeEditingChip: () => void;
42
+ stepBackBuildingMenu: (current: "field" | "operator" | "value") => void;
43
+ };
44
+ export {};
@@ -0,0 +1,99 @@
1
+ import { useCallback } from "react";
2
+ import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
+ import { SEGMENT_TO_MENU, chipIdToConditionIndex } from "../../lib/index.js";
4
+ import { getInitialSegmentText } from "./lib/index.js";
5
+ const useChipCascade = ({ editing, chips, fields, conditionsRef, selectedField, selectedOperator, buildingMultiValue, upsertCondition, removeCondition, resetState, setInputText, setMenuState, setSelectedOperator, setBuildingMultiValue })=>{
6
+ const { editingChipId, editingSegment, isBuildingEdit, startBuildingEdit, switchEditSegment: editingSwitchSegment } = editing;
7
+ const switchEditSegment = useCallback((targetSegment)=>{
8
+ const sourceSegment = editingSegment;
9
+ if (null === sourceSegment) return false;
10
+ if (isBuildingEdit) {
11
+ if (!selectedField) return false;
12
+ if (sourceSegment === SEGMENT_VARIANT.value) setBuildingMultiValue(void 0);
13
+ else if (sourceSegment === SEGMENT_VARIANT.operator) setSelectedOperator(null);
14
+ const initialText = getInitialSegmentText(targetSegment, selectedField, selectedOperator, buildingMultiValue);
15
+ startBuildingEdit(targetSegment, initialText);
16
+ setInputText('');
17
+ setMenuState(SEGMENT_TO_MENU[targetSegment]);
18
+ return true;
19
+ }
20
+ const editingId = editingChipId;
21
+ if (!editingId) return false;
22
+ const chip = chips.find((c)=>c.id === editingId);
23
+ if (!chip || 'chip' !== chip.variant) return false;
24
+ const idx = chipIdToConditionIndex(editingId);
25
+ const condition = null !== idx ? conditionsRef.current[idx] : null;
26
+ const field = condition ? fields.find((f)=>f.name === condition.field) : null;
27
+ if (condition && field) {
28
+ if (sourceSegment === SEGMENT_VARIANT.value) upsertCondition(field, condition.operator, null, editingId, void 0, void 0, condition.dateOrigin);
29
+ else if (sourceSegment === SEGMENT_VARIANT.operator) upsertCondition(field, void 0, condition.value, editingId, void 0, void 0, condition.dateOrigin);
30
+ }
31
+ const targetText = targetSegment === SEGMENT_VARIANT.attribute ? chip.attribute ?? '' : targetSegment === SEGMENT_VARIANT.operator ? chip.operator ?? '' : '';
32
+ editingSwitchSegment(targetSegment, targetText);
33
+ setMenuState(SEGMENT_TO_MENU[targetSegment]);
34
+ return true;
35
+ }, [
36
+ editingChipId,
37
+ editingSegment,
38
+ isBuildingEdit,
39
+ startBuildingEdit,
40
+ editingSwitchSegment,
41
+ selectedField,
42
+ selectedOperator,
43
+ buildingMultiValue,
44
+ chips,
45
+ fields,
46
+ conditionsRef,
47
+ upsertCondition,
48
+ setBuildingMultiValue,
49
+ setSelectedOperator,
50
+ setInputText,
51
+ setMenuState
52
+ ]);
53
+ const removeEditingChip = useCallback(()=>{
54
+ if (null === editingSegment) return;
55
+ if (!editingChipId) return void resetState();
56
+ removeCondition(editingChipId);
57
+ resetState();
58
+ }, [
59
+ editingChipId,
60
+ editingSegment,
61
+ removeCondition,
62
+ resetState
63
+ ]);
64
+ const stepBackBuildingMenu = useCallback((current)=>{
65
+ if (!selectedField) return void resetState();
66
+ if ('value' === current) {
67
+ setBuildingMultiValue(void 0);
68
+ setInputText('');
69
+ const operatorText = getInitialSegmentText(SEGMENT_VARIANT.operator, selectedField, selectedOperator, buildingMultiValue);
70
+ startBuildingEdit(SEGMENT_VARIANT.operator, operatorText);
71
+ setMenuState('operator');
72
+ return;
73
+ }
74
+ if ('operator' === current) {
75
+ setSelectedOperator(null);
76
+ setInputText('');
77
+ startBuildingEdit(SEGMENT_VARIANT.attribute, selectedField.label);
78
+ setMenuState('field');
79
+ return;
80
+ }
81
+ resetState();
82
+ }, [
83
+ selectedField,
84
+ selectedOperator,
85
+ buildingMultiValue,
86
+ startBuildingEdit,
87
+ resetState,
88
+ setBuildingMultiValue,
89
+ setInputText,
90
+ setMenuState,
91
+ setSelectedOperator
92
+ ]);
93
+ return {
94
+ switchEditSegment,
95
+ removeEditingChip,
96
+ stepBackBuildingMenu
97
+ };
98
+ };
99
+ export { useChipCascade };
@@ -1,32 +1,31 @@
1
- import type { RefObject } from 'react';
2
1
  import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
2
  import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
4
3
  interface UseChipEditingOptions {
5
4
  conditions: Condition[];
6
5
  chips: FilterInputChipData[];
7
6
  fields: FieldMetadata[];
8
- containerRef: RefObject<HTMLElement | null>;
9
- setMenuOffset: (offset: number) => void;
7
+ setMenuAnchor: (el: HTMLElement | null) => void;
10
8
  setSelectedField: (field: FieldMetadata | null) => void;
11
9
  setSelectedOperator: (op: FilterOperator | null) => void;
12
10
  setMenuState: (state: MenuState) => void;
13
11
  upsertCondition: UpsertCondition;
14
12
  }
15
13
  /**
16
- * Manages editing of existing filter chips.
17
- * Handles chip click open appropriate menu based on segment,
18
- * then advances through the full flow (field → operator → value).
14
+ * Manages editing of existing filter chips. Chip click opens the appropriate
15
+ * menu based on segment, then advances through field operator → value.
19
16
  */
20
- export declare const useChipEditing: ({ conditions, chips, fields, containerRef, setMenuOffset, setSelectedField, setSelectedOperator, setMenuState, upsertCondition, }: UseChipEditingOptions) => {
17
+ export declare const useChipEditing: ({ conditions, chips, fields, setMenuAnchor, setSelectedField, setSelectedOperator, setMenuState, upsertCondition, }: UseChipEditingOptions) => {
21
18
  editingChipId: string | null;
22
19
  editingSegment: import("../../FilterInputField/FilterInputChip").SegmentVariant | null;
20
+ isBuildingEdit: boolean;
23
21
  setEditingSegment: import("react").Dispatch<import("react").SetStateAction<import("../../FilterInputField/FilterInputChip").SegmentVariant | null>>;
24
22
  segmentFilterText: string;
25
23
  segmentMenuFilterText: string;
26
24
  setSegmentFilterText: (text: string) => void;
27
25
  resetSegmentTyping: () => void;
28
- handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
26
+ handleChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
29
27
  startBuildingEdit: (segment: ChipSegment, currentText: string) => void;
28
+ switchEditSegment: (segment: ChipSegment, currentText: string) => void;
30
29
  clearEditing: () => void;
31
30
  };
32
31
  export {};
@@ -1,15 +1,10 @@
1
- import { useCallback, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
- import { chipIdToConditionIndex, getOperatorFromLabel, isNoValueOperator } from "../../lib/index.js";
3
+ import { SEGMENT_TO_MENU, chipIdToConditionIndex, getOperatorFromLabel, isNoValueOperator } from "../../lib/index.js";
4
4
  const getConditionByChipId = (chipId, conditions)=>{
5
5
  const idx = chipIdToConditionIndex(chipId);
6
6
  return null !== idx ? conditions[idx] ?? null : null;
7
7
  };
8
- const SEGMENT_TO_MENU = {
9
- attribute: 'field',
10
- operator: 'operator',
11
- value: 'value'
12
- };
13
8
  const getFirstIncompleteSegment = (condition, fields)=>{
14
9
  const field = fields.find((f)=>f.name === condition.field);
15
10
  if (!field || condition.error === SEGMENT_VARIANT.attribute) return SEGMENT_VARIANT.attribute;
@@ -17,18 +12,20 @@ const getFirstIncompleteSegment = (condition, fields)=>{
17
12
  if (null === condition.value || '' === condition.value || condition.error === SEGMENT_VARIANT.value) return SEGMENT_VARIANT.value;
18
13
  return null;
19
14
  };
20
- const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset, setSelectedField, setSelectedOperator, setMenuState, upsertCondition })=>{
15
+ const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedField, setSelectedOperator, setMenuState, upsertCondition })=>{
21
16
  const [editingChipId, setEditingChipId] = useState(null);
22
17
  const [editingSegment, setEditingSegment] = useState(null);
23
18
  const [segmentFilterText, setSegmentFilterText] = useState('');
24
19
  const [userHasTyped, setUserHasTyped] = useState(false);
25
20
  const conditionsRef = useRef(conditions);
26
- conditionsRef.current = conditions;
27
21
  const chipsRef = useRef(chips);
28
- chipsRef.current = chips;
29
22
  const fieldsRef = useRef(fields);
30
- fieldsRef.current = fields;
31
- const handleChipClick = useCallback((chipId, segment, anchorRect)=>{
23
+ useLayoutEffect(()=>{
24
+ conditionsRef.current = conditions;
25
+ chipsRef.current = chips;
26
+ fieldsRef.current = fields;
27
+ });
28
+ const handleChipClick = useCallback((chipId, segment, anchorEl)=>{
32
29
  const condition = getConditionByChipId(chipId, conditionsRef.current);
33
30
  if (!condition) return;
34
31
  const field = fieldsRef.current.find((f)=>f.name === condition.field);
@@ -39,8 +36,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
39
36
  const targetSegment = incompleteSegment ?? (isPlaceholderValueClick ? SEGMENT_VARIANT.operator : segment);
40
37
  if (!field && targetSegment !== SEGMENT_VARIANT.attribute) return;
41
38
  if (incompleteSegment && field) upsertCondition(field, condition.operator, condition.value, chipId);
42
- const containerRect = containerRef.current?.getBoundingClientRect();
43
- setMenuOffset(containerRect ? anchorRect.left - containerRect.left : 0);
39
+ setMenuAnchor(anchorEl);
44
40
  setEditingChipId(chipId);
45
41
  setSelectedField(field ?? null);
46
42
  const rawOperator = targetSegment === SEGMENT_VARIANT.value || targetSegment === SEGMENT_VARIANT.operator ? getOperatorFromLabel(chip.operator || '', field?.type ?? 'string') ?? condition.operator : null;
@@ -55,8 +51,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
55
51
  setUserHasTyped(false);
56
52
  setMenuState(SEGMENT_TO_MENU[targetSegment]);
57
53
  }, [
58
- containerRef,
59
- setMenuOffset,
54
+ setMenuAnchor,
60
55
  setSelectedField,
61
56
  setSelectedOperator,
62
57
  setMenuState,
@@ -74,6 +69,11 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
74
69
  setSegmentFilterText(currentText);
75
70
  setUserHasTyped(false);
76
71
  }, []);
72
+ const switchEditSegment = useCallback((segment, currentText)=>{
73
+ setEditingSegment(segment);
74
+ setSegmentFilterText(currentText);
75
+ setUserHasTyped(false);
76
+ }, []);
77
77
  const handleSegmentFilterChange = useCallback((text)=>{
78
78
  setSegmentFilterText(text);
79
79
  setUserHasTyped(true);
@@ -83,9 +83,11 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
83
83
  }, []);
84
84
  const segmentDisplayText = segmentFilterText;
85
85
  const segmentMenuFilterText = userHasTyped ? segmentFilterText : '';
86
+ const isBuildingEdit = null === editingChipId && null !== editingSegment;
86
87
  return useMemo(()=>({
87
88
  editingChipId,
88
89
  editingSegment,
90
+ isBuildingEdit,
89
91
  setEditingSegment,
90
92
  segmentFilterText: segmentDisplayText,
91
93
  segmentMenuFilterText,
@@ -93,16 +95,19 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
93
95
  resetSegmentTyping,
94
96
  handleChipClick,
95
97
  startBuildingEdit,
98
+ switchEditSegment,
96
99
  clearEditing
97
100
  }), [
98
101
  editingChipId,
99
102
  editingSegment,
103
+ isBuildingEdit,
100
104
  segmentDisplayText,
101
105
  segmentMenuFilterText,
102
106
  handleSegmentFilterChange,
103
107
  resetSegmentTyping,
104
108
  handleChipClick,
105
109
  startBuildingEdit,
110
+ switchEditSegment,
106
111
  clearEditing
107
112
  ]);
108
113
  };
@@ -1,6 +1,5 @@
1
1
  import type { RefObject } from 'react';
2
- import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
- import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
2
+ import type { Condition, FieldMetadata, FilterInputChipData, UpsertCondition } from '../../types';
4
3
  interface UseFilterInputAutocompleteOptions {
5
4
  fields: FieldMetadata[];
6
5
  conditions: Condition[];
@@ -16,9 +15,9 @@ interface UseFilterInputAutocompleteOptions {
16
15
  }
17
16
  export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, upsertCondition, removeCondition, removeConditionAtIndex, clearAll, setConnectorValue, containerRef, buildingChipRef, inputRef, }: UseFilterInputAutocompleteOptions) => {
18
17
  inputText: string;
19
- menuState: MenuState;
18
+ menuState: import("../../types").MenuState;
20
19
  selectedField: FieldMetadata | null;
21
- selectedOperator: FilterOperator | null;
20
+ selectedOperator: import("../..").FilterOperator | null;
22
21
  isBuilding: boolean;
23
22
  buildingChipData: import("../../FilterInputContext").BuildingChipData | null;
24
23
  menuPositioning: {
@@ -41,19 +40,19 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
41
40
  inputRef: RefObject<HTMLInputElement | null>;
42
41
  handleInputChange: (e: import("react").ChangeEvent<HTMLInputElement>) => void;
43
42
  handleFieldSelect: (field: FieldMetadata) => void;
44
- handleOperatorSelect: (operator: FilterOperator) => void;
43
+ handleOperatorSelect: (operator: import("../..").FilterOperator) => void;
45
44
  handleValueSelect: (val: string | number | boolean) => void;
46
45
  handleMultiCommit: (values: Array<string | number | boolean>) => void;
47
46
  handleBuildingValueChange: (preview: string | undefined) => void;
48
47
  handleMultiSelectToggle: () => void;
49
48
  handleMenuClose: () => void;
50
49
  handleMenuDiscard: () => void;
51
- /** Hard reset of autocomplete stateused by paste/clipboard flows where
52
- * the conditions array is replaced and any in-progress building must be
53
- * scrapped, regardless of inline-edit mode. */
50
+ /** Hard reset for paste/clipboard flowsscraps in-progress building. */
54
51
  resetAutocompleteState: (continueBuilding?: boolean) => void;
55
- handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
56
- handleBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
52
+ handleChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
53
+ handleBuildingChipClick: (segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
54
+ switchEditSegment: (targetSegment: import("../../FilterInputField").ChipSegment) => boolean;
55
+ removeEditingChip: () => void;
57
56
  handleConnectorChange: (connectorId: string, value: "and" | "or") => void;
58
57
  handleChipRemove: (chipId: string) => void;
59
58
  handleClear: () => void;