@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
@@ -0,0 +1,20 @@
1
+ import type { MenuFlowDeps } from './types';
2
+ /**
3
+ * Aggregates the segment-specific flow hooks (`useFieldFlow`,
4
+ * `useOperatorFlow`, `useValueFlow`) and adds the cross-cutting
5
+ * `handleMenuClose` handler. Sub-hooks share a single fresh-conditions ref so
6
+ * none of them re-create callbacks on every keystroke that mutates conditions.
7
+ */
8
+ export declare const useMenuFlow: (deps: MenuFlowDeps) => {
9
+ handleMenuClose: () => void;
10
+ handleFieldSelect: (field: import("../../..").FieldMetadata) => void;
11
+ handleOperatorSelect: (operator: import("../../..").FilterOperator) => void;
12
+ handleValueSelect: (val: string | number | boolean) => void;
13
+ handleMultiCommit: (values: Array<string | number | boolean>) => void;
14
+ handleBuildingValueChange: (preview: string | undefined) => void;
15
+ handleMultiSelectToggle: () => void;
16
+ handleRangeSelect: (from: string, to: string) => void;
17
+ handleCustomValueCommit: (customText: string) => void;
18
+ handleCustomOperatorCommit: (customText: string) => void;
19
+ handleCustomAttributeCommit: (customText: string) => void;
20
+ };
@@ -0,0 +1,47 @@
1
+ import { useCallback, useLayoutEffect, useRef } from "react";
2
+ import { useFieldFlow } from "./useFieldFlow.js";
3
+ import { useOperatorFlow } from "./useOperatorFlow.js";
4
+ import { useValueFlow } from "./useValueFlow.js";
5
+ const useMenuFlow = (deps)=>{
6
+ const { editing, selectedField, inputRef, conditions, resetState, commitBuildingOnBlur, setMenuState } = deps;
7
+ const conditionsRef = useRef(conditions);
8
+ useLayoutEffect(()=>{
9
+ conditionsRef.current = conditions;
10
+ });
11
+ const internalDeps = {
12
+ ...deps,
13
+ conditionsRef
14
+ };
15
+ const { handleFieldSelect, handleCustomAttributeCommit } = useFieldFlow(internalDeps);
16
+ const { handleOperatorSelect, handleCustomOperatorCommit } = useOperatorFlow(internalDeps);
17
+ const { handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleMultiSelectToggle, handleRangeSelect, handleCustomValueCommit } = useValueFlow(internalDeps);
18
+ const handleMenuClose = useCallback(()=>{
19
+ if (document.activeElement === inputRef.current) return;
20
+ if (document.activeElement?.closest?.('[data-slot^="segment-"]')) return;
21
+ if (commitBuildingOnBlur()) return;
22
+ const hasIncompleteBuilding = null !== selectedField && !editing.editingChipId;
23
+ if (hasIncompleteBuilding) return void setMenuState('closed');
24
+ resetState();
25
+ }, [
26
+ commitBuildingOnBlur,
27
+ resetState,
28
+ inputRef,
29
+ selectedField,
30
+ editing.editingChipId,
31
+ setMenuState
32
+ ]);
33
+ return {
34
+ handleMenuClose,
35
+ handleFieldSelect,
36
+ handleOperatorSelect,
37
+ handleValueSelect,
38
+ handleMultiCommit,
39
+ handleBuildingValueChange,
40
+ handleMultiSelectToggle,
41
+ handleRangeSelect,
42
+ handleCustomValueCommit,
43
+ handleCustomOperatorCommit,
44
+ handleCustomAttributeCommit
45
+ };
46
+ };
47
+ export { useMenuFlow };
@@ -0,0 +1,11 @@
1
+ import type { FilterOperator } from '../../../types';
2
+ import type { MenuFlowInternalDeps } from './types';
3
+ /**
4
+ * Operator-segment handlers. `handleOperatorSelect` is the menu-click path,
5
+ * `handleCustomOperatorCommit` is the keyboard-typed path — it resolves the
6
+ * typed text to a known operator (label / raw key / symbol) and delegates.
7
+ */
8
+ export declare const useOperatorFlow: ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText, }: MenuFlowInternalDeps) => {
9
+ handleOperatorSelect: (operator: FilterOperator) => void;
10
+ handleCustomOperatorCommit: (customText: string) => void;
11
+ };
@@ -0,0 +1,87 @@
1
+ import { useCallback } from "react";
2
+ import { SEGMENT_VARIANT } from "../../../FilterInputField/FilterInputChip/index.js";
3
+ import { OPERATOR_SYMBOLS, chipIdToConditionIndex, getFieldOperators, getOperatorFromLabel, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible } from "../../../lib/index.js";
4
+ const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText })=>{
5
+ const { editingChipId, editingSegment, isBuildingEdit, setEditingSegment, setSegmentFilterText, clearEditing } = editing;
6
+ const handleOperatorSelect = useCallback((operator)=>{
7
+ if (!selectedField) return;
8
+ if (isBuildingEdit && editingSegment === SEGMENT_VARIANT.operator) {
9
+ const shapeCompatible = isValueShapeCompatible(selectedOperator, operator);
10
+ if (!shapeCompatible) setBuildingMultiValue(void 0);
11
+ setSelectedOperator(operator);
12
+ clearEditing();
13
+ if (isNoValueOperator(operator)) {
14
+ upsertCondition(selectedField, operator, null, null, insertIndex);
15
+ resetState(true);
16
+ return;
17
+ }
18
+ setMenuState('value');
19
+ return;
20
+ }
21
+ if (isNoValueOperator(operator)) {
22
+ const isEditing = !!editingChipId;
23
+ upsertCondition(selectedField, operator, null, editingChipId, isEditing ? void 0 : insertIndex);
24
+ resetState(!isEditing);
25
+ return;
26
+ }
27
+ if (editingChipId && editingSegment === SEGMENT_VARIANT.operator) {
28
+ const idx = chipIdToConditionIndex(editingChipId);
29
+ const condition = null !== idx ? conditionsRef.current[idx] : null;
30
+ if (condition) {
31
+ const hasValue = null !== condition.value && '' !== condition.value;
32
+ if (hasValue) {
33
+ upsertCondition(selectedField, operator, condition.value, editingChipId, void 0, void 0, condition.dateOrigin);
34
+ resetState();
35
+ return;
36
+ }
37
+ upsertCondition(selectedField, operator, null, editingChipId);
38
+ setEditingSegment(SEGMENT_VARIANT.value);
39
+ setSegmentFilterText('');
40
+ }
41
+ }
42
+ setSelectedOperator(operator);
43
+ setInputText('');
44
+ setMenuState('value');
45
+ }, [
46
+ editingChipId,
47
+ editingSegment,
48
+ isBuildingEdit,
49
+ setEditingSegment,
50
+ setSegmentFilterText,
51
+ clearEditing,
52
+ selectedField,
53
+ selectedOperator,
54
+ insertIndex,
55
+ conditionsRef,
56
+ upsertCondition,
57
+ resetState,
58
+ setSelectedOperator,
59
+ setMenuState,
60
+ setBuildingMultiValue,
61
+ setInputText
62
+ ]);
63
+ const handleCustomOperatorCommit = useCallback((customText)=>{
64
+ if (!selectedField || !customText.trim()) return;
65
+ const trimmed = customText.trim();
66
+ const allowed = getFieldOperators(selectedField);
67
+ let matched = getOperatorFromLabel(trimmed, selectedField.type);
68
+ if (!matched) {
69
+ const symbolMatch = allowed.find((op)=>OPERATOR_SYMBOLS[op].toLowerCase() === trimmed.toLowerCase());
70
+ if (symbolMatch) matched = symbolMatch;
71
+ }
72
+ if (!matched) {
73
+ const rawMatch = allowed.find((op)=>op.toLowerCase() === trimmed.toLowerCase());
74
+ if (rawMatch) matched = rawMatch;
75
+ }
76
+ if (!matched || !isOperatorAllowedForField(selectedField, matched)) return;
77
+ handleOperatorSelect(matched);
78
+ }, [
79
+ selectedField,
80
+ handleOperatorSelect
81
+ ]);
82
+ return {
83
+ handleOperatorSelect,
84
+ handleCustomOperatorCommit
85
+ };
86
+ };
87
+ export { useOperatorFlow };
@@ -0,0 +1,14 @@
1
+ import type { MenuFlowInternalDeps } from './types';
2
+ /**
3
+ * Value-segment handlers: single/multi/range select from the menu and the
4
+ * keyboard-typed custom-value commit (which routes through the field-type
5
+ * specific resolver). Also handles multi-select preview/toggle plumbing.
6
+ */
7
+ export declare const useValueFlow: ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, dateRange, setBuildingMultiValue, setInputText, }: MenuFlowInternalDeps) => {
8
+ handleValueSelect: (val: string | number | boolean) => void;
9
+ handleMultiCommit: (values: Array<string | number | boolean>) => void;
10
+ handleBuildingValueChange: (preview: string | undefined) => void;
11
+ handleMultiSelectToggle: () => void;
12
+ handleRangeSelect: (from: string, to: string) => void;
13
+ handleCustomValueCommit: (customText: string) => void;
14
+ };
@@ -0,0 +1,107 @@
1
+ import { useCallback } from "react";
2
+ import { SEGMENT_VARIANT } from "../../../FilterInputField/FilterInputChip/index.js";
3
+ import { isBetweenOperator, isMultiSelectOperator } from "../../../lib/index.js";
4
+ import { resolveDateRangeValue, resolveDateValue, resolveMultiValues, resolveSingleValue } from "../lib/index.js";
5
+ const useValueFlow = ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, dateRange, setBuildingMultiValue, setInputText })=>{
6
+ const { editingChipId, editingSegment, resetSegmentTyping } = editing;
7
+ const handleValueSelect = useCallback((val)=>{
8
+ if (!selectedField || !selectedOperator) return;
9
+ if (isBetweenOperator(selectedOperator) && 'date' === selectedField.type) {
10
+ const result = dateRange.selectValue(String(val));
11
+ if (!result) return;
12
+ upsertCondition(selectedField, selectedOperator, result, editingChipId);
13
+ resetState(!editingChipId);
14
+ return;
15
+ }
16
+ const isEditing = !!editingChipId;
17
+ upsertCondition(selectedField, selectedOperator, val, editingChipId, isEditing ? void 0 : insertIndex);
18
+ resetState(!isEditing);
19
+ }, [
20
+ selectedField,
21
+ selectedOperator,
22
+ editingChipId,
23
+ dateRange,
24
+ insertIndex,
25
+ upsertCondition,
26
+ resetState
27
+ ]);
28
+ const handleMultiCommit = useCallback((values)=>{
29
+ if (!selectedField || !selectedOperator || 0 === values.length) return;
30
+ const isEditing = !!editingChipId;
31
+ upsertCondition(selectedField, selectedOperator, values, editingChipId, isEditing ? void 0 : insertIndex);
32
+ resetState(!isEditing);
33
+ }, [
34
+ selectedField,
35
+ selectedOperator,
36
+ editingChipId,
37
+ insertIndex,
38
+ upsertCondition,
39
+ resetState
40
+ ]);
41
+ const handleBuildingValueChange = useCallback((preview)=>{
42
+ setBuildingMultiValue(preview);
43
+ }, [
44
+ setBuildingMultiValue
45
+ ]);
46
+ const handleMultiSelectToggle = useCallback(()=>{
47
+ if (editingSegment === SEGMENT_VARIANT.value) resetSegmentTyping();
48
+ else setInputText('');
49
+ }, [
50
+ editingSegment,
51
+ resetSegmentTyping,
52
+ setInputText
53
+ ]);
54
+ const handleRangeSelect = useCallback((from, to)=>{
55
+ if (!selectedField || !selectedOperator) return;
56
+ const isEditing = !!editingChipId;
57
+ upsertCondition(selectedField, selectedOperator, [
58
+ from,
59
+ to
60
+ ], editingChipId, isEditing ? void 0 : insertIndex);
61
+ resetState(!isEditing);
62
+ }, [
63
+ selectedField,
64
+ selectedOperator,
65
+ editingChipId,
66
+ insertIndex,
67
+ upsertCondition,
68
+ resetState
69
+ ]);
70
+ const handleCustomValueCommit = useCallback((customText)=>{
71
+ if (!selectedField || !selectedOperator || !customText.trim()) return;
72
+ const trimmed = customText.trim();
73
+ const isEditing = !!editingChipId;
74
+ if (isMultiSelectOperator(selectedOperator)) {
75
+ const { resolved, error } = resolveMultiValues(selectedField, trimmed);
76
+ upsertCondition(selectedField, selectedOperator, resolved, editingChipId, isEditing ? void 0 : insertIndex, error ? SEGMENT_VARIANT.value : void 0);
77
+ } else if ('date' === selectedField.type) if (isBetweenOperator(selectedOperator)) {
78
+ const rangeValue = resolveDateRangeValue(trimmed);
79
+ upsertCondition(selectedField, selectedOperator, rangeValue ?? trimmed, editingChipId, isEditing ? void 0 : insertIndex, rangeValue ? void 0 : SEGMENT_VARIANT.value, 'absolute');
80
+ } else {
81
+ const { error, dateOrigin } = resolveDateValue(trimmed, editingChipId, conditionsRef.current);
82
+ upsertCondition(selectedField, selectedOperator, trimmed, editingChipId, isEditing ? void 0 : insertIndex, error ? SEGMENT_VARIANT.value : void 0, dateOrigin);
83
+ }
84
+ else {
85
+ const { resolved, error } = resolveSingleValue(selectedField, trimmed);
86
+ upsertCondition(selectedField, selectedOperator, resolved, editingChipId, isEditing ? void 0 : insertIndex, error ? SEGMENT_VARIANT.value : void 0);
87
+ }
88
+ resetState(!isEditing);
89
+ }, [
90
+ selectedField,
91
+ selectedOperator,
92
+ editingChipId,
93
+ insertIndex,
94
+ conditionsRef,
95
+ upsertCondition,
96
+ resetState
97
+ ]);
98
+ return {
99
+ handleValueSelect,
100
+ handleMultiCommit,
101
+ handleBuildingValueChange,
102
+ handleMultiSelectToggle,
103
+ handleRangeSelect,
104
+ handleCustomValueCommit
105
+ };
106
+ };
107
+ export { useValueFlow };
@@ -5,15 +5,32 @@ interface UseMenuPositioningOptions {
5
5
  inputRef?: RefObject<HTMLElement | null>;
6
6
  isBuilding: boolean;
7
7
  insertIndex: number;
8
+ /** Committed chip count — recompute trigger for sibling reflow (ResizeObserver
9
+ * misses it; without this the dropdown sticks at the old position). */
10
+ chipsCount: number;
11
+ /** Menu is currently visible — gates the synthetic resize so a chip add/remove
12
+ * while the menu is closed doesn't wake page-wide resize listeners. */
13
+ isMenuOpen: boolean;
8
14
  }
9
15
  /**
10
- * Manages dropdown menu positioning relative to the active chip or input.
11
- * Computes a horizontal offset lazily in getAnchorRect so the dropdown aligns with:
12
- * - The input element (when selecting a field, before building chip exists)
13
- * - The building chip (when selecting operator/value during chip creation)
14
- * - The clicked chip segment (when editing — offset set externally via setMenuOffset)
16
+ * Manages dropdown menu positioning relative to the active chip / segment / input.
17
+ *
18
+ * Anchor resolution (highest priority first):
19
+ * 1. Explicit element passed via `setMenuAnchor` set when a chip segment is
20
+ * clicked, including the Backspace cascade.
21
+ * 2. Building chip ref (for operator/value menus while a chip is being built).
22
+ * 3. Input ref — only when chips are already committed (the input has shifted
23
+ * past them, so the dropdown follows the input's live position).
24
+ * 4. Container rect — the empty/initial state, flush with the field's border.
25
+ *
26
+ * Three signal sources keep Ark UI in sync with the live anchor:
27
+ * - `useResizeTracker` — width changes inside a segment as the user types.
28
+ * - `useFloatingRecomputeOn(chipsCount)` — sibling reflow when chips are
29
+ * added/removed (no element resized, so the resize observer is silent).
30
+ * - `useAutoCleanupDetachedElement` — drops the captured anchor as soon as
31
+ * its DOM node leaves the tree (e.g. controlled-value prop removed it).
15
32
  */
16
- export declare const useMenuPositioning: ({ containerRef, buildingChipRef, inputRef, isBuilding, insertIndex, }: UseMenuPositioningOptions) => {
33
+ export declare const useMenuPositioning: ({ containerRef, buildingChipRef, inputRef, isBuilding, insertIndex, chipsCount, isMenuOpen, }: UseMenuPositioningOptions) => {
17
34
  menuPositioning: {
18
35
  placement: "bottom-start";
19
36
  gutter: number;
@@ -28,7 +45,7 @@ export declare const useMenuPositioning: ({ containerRef, buildingChipRef, input
28
45
  right: number;
29
46
  } | null;
30
47
  };
31
- setMenuOffset: (offset: number) => void;
32
- resetMenuOffset: () => void;
48
+ setMenuAnchor: import("react").Dispatch<import("react").SetStateAction<HTMLElement | null>>;
49
+ resetMenuAnchor: () => void;
33
50
  };
34
51
  export {};
@@ -1,36 +1,52 @@
1
- import { useCallback, useRef } from "react";
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { MENU_BASE_GUTTER, MENU_CHIP_GUTTER_OFFSET, QUERY_BAR_SELECTOR, toAnchorBounds } from "../../lib/index.js";
3
+ import { useAutoCleanupDetachedElement } from "../useAutoCleanupDetachedElement.js";
2
4
  import { useFilterInputPositioning } from "../useFilterInputPositioning.js";
3
- const useMenuPositioning = ({ containerRef, buildingChipRef, inputRef, isBuilding, insertIndex })=>{
4
- const editingOffsetRef = useRef(null);
5
- const setMenuOffset = useCallback((offset)=>{
6
- editingOffsetRef.current = offset;
7
- }, []);
8
- const resetMenuOffset = useCallback(()=>{
9
- editingOffsetRef.current = null;
10
- }, []);
11
- const getAnchorLeft = useCallback((containerRect)=>{
12
- if (null !== editingOffsetRef.current) return containerRect.left + editingOffsetRef.current;
13
- if (isBuilding) {
14
- const chipRect = buildingChipRef.current?.getBoundingClientRect();
15
- if (chipRect) return chipRect.left;
16
- }
17
- if (inputRef?.current) return inputRef.current.getBoundingClientRect().left;
18
- return containerRect.left;
19
- }, [
5
+ import { useFloatingRecomputeOn } from "../useFloatingRecomputeOn.js";
6
+ import { useResizeTracker } from "../useResizeTracker.js";
7
+ const useMenuPositioning = ({ containerRef, buildingChipRef, inputRef, isBuilding, insertIndex, chipsCount, isMenuOpen })=>{
8
+ const [editingEl, setMenuAnchor] = useState(null);
9
+ const resetMenuAnchor = useCallback(()=>setMenuAnchor(null), []);
10
+ useAutoCleanupDetachedElement(editingEl, resetMenuAnchor);
11
+ useFloatingRecomputeOn(chipsCount, isMenuOpen);
12
+ const tick = useResizeTracker(editingEl, buildingChipRef.current, containerRef.current);
13
+ const anchorStateRef = useRef({
14
+ editingEl,
15
+ isBuilding,
16
+ chipsCount
17
+ });
18
+ anchorStateRef.current = {
19
+ editingEl,
20
20
  isBuilding,
21
+ chipsCount
22
+ };
23
+ const getAnchorBounds = useCallback((containerRect)=>{
24
+ const { editingEl: el, isBuilding: building, chipsCount: count } = anchorStateRef.current;
25
+ const chipAnchor = el?.isConnected ? toAnchorBounds(el.getBoundingClientRect()) : building && buildingChipRef.current ? toAnchorBounds(buildingChipRef.current.getBoundingClientRect()) : count > 0 && inputRef?.current ? toAnchorBounds(inputRef.current.getBoundingClientRect()) : null;
26
+ if (chipAnchor) return {
27
+ ...chipAnchor,
28
+ bottom: chipAnchor.bottom + MENU_CHIP_GUTTER_OFFSET
29
+ };
30
+ const fieldEl = containerRef.current?.querySelector(QUERY_BAR_SELECTOR) ?? null;
31
+ return toAnchorBounds(fieldEl?.getBoundingClientRect() ?? containerRect);
32
+ }, [
33
+ containerRef,
21
34
  buildingChipRef,
22
35
  inputRef
23
36
  ]);
24
37
  const menuPositioning = useFilterInputPositioning({
25
38
  containerRef,
26
- getAnchorLeft
39
+ getAnchorBounds,
40
+ gutter: MENU_BASE_GUTTER
27
41
  }, [
28
- insertIndex
42
+ insertIndex,
43
+ chipsCount,
44
+ tick
29
45
  ]);
30
46
  return {
31
47
  menuPositioning,
32
- setMenuOffset,
33
- resetMenuOffset
48
+ setMenuAnchor,
49
+ resetMenuAnchor
34
50
  };
35
51
  };
36
52
  export { useMenuPositioning };
@@ -9,7 +9,7 @@ interface UseResetStateDeps {
9
9
  };
10
10
  containerRef: RefObject<HTMLElement | null>;
11
11
  inputRef: RefObject<HTMLInputElement | null>;
12
- resetMenuOffset: () => void;
12
+ resetMenuAnchor: () => void;
13
13
  setInputText: Dispatch<SetStateAction<string>>;
14
14
  setSelectedField: Dispatch<SetStateAction<FieldMetadata | null>>;
15
15
  setSelectedOperator: Dispatch<SetStateAction<FilterOperator | null>>;
@@ -19,24 +19,15 @@ interface UseResetStateDeps {
19
19
  setMenuState: Dispatch<SetStateAction<MenuState>>;
20
20
  }
21
21
  /**
22
- * Resets all autocomplete state and conditionally returns focus to the main input.
22
+ * Resets all autocomplete state and refocuses the input.
23
23
  *
24
- * ── Body-focus policy (READ THIS) ─────────────────────────────────────────────
25
- * `document.activeElement === document.body` is treated **as "stayed inside"** here
26
- * i.e. we DO refocus the main input. The reasoning: state mutations in
27
- * `doReset()` (e.g. `editing.clearEditing()`) can synchronously unmount the
28
- * element that previously had focus (a chip's inline input, a menu item), and
29
- * the browser drops focus to body in that case. We want to honor "user is still
30
- * working in the FilterInput" intent and put the caret back in the input.
31
- *
32
- * The opposite policy lives in `useFocusManagement.ts`'s rAF effect: there,
33
- * body-focus means "user clicked outside" (e.g. tenant switcher) and we MUST
34
- * NOT recapture. The two policies coexist because the triggers are different
35
- * (state-driven re-render vs. genuine outside click).
36
- *
37
- * If you find yourself unifying these — make sure the "DOM dropped focus on
38
- * re-render" case still refocuses, otherwise resetState during commit chains
39
- * breaks. AS-882.
24
+ * Body-focus policy: here, activeElement===body is treated as "stayed inside"
25
+ * (refocus the input) because doReset()'s state mutations can synchronously
26
+ * unmount the element that had focus, and the browser drops to body. The
27
+ * opposite policy in useFocusManagement treats body-focus as "user clicked
28
+ * outside" both coexist because the triggers differ (state-driven re-render
29
+ * vs. genuine outside click). If unifying these, preserve the re-render
30
+ * refocus case or commit chains break. AS-882.
40
31
  */
41
- export declare const useResetState: ({ editing, dateRange, containerRef, inputRef, resetMenuOffset, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseResetStateDeps) => (continueBuilding?: boolean) => void;
32
+ export declare const useResetState: ({ editing, dateRange, containerRef, inputRef, resetMenuAnchor, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseResetStateDeps) => (continueBuilding?: boolean) => void;
42
33
  export {};
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from "react";
2
2
  import { flushSync } from "react-dom";
3
3
  import { isMenuRelated } from "../../lib/index.js";
4
- const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuOffset, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
4
+ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuAnchor, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
5
5
  const resetState = useCallback((continueBuilding = false)=>{
6
6
  const doReset = ()=>{
7
7
  setInputText('');
@@ -12,7 +12,7 @@ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuOf
12
12
  setBuildingMultiValue(void 0);
13
13
  setInsertIndex(null);
14
14
  setInsertAfterConnector(false);
15
- resetMenuOffset();
15
+ resetMenuAnchor();
16
16
  setMenuState('closed');
17
17
  };
18
18
  if (continueBuilding) {
@@ -27,7 +27,7 @@ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuOf
27
27
  dateRange,
28
28
  inputRef,
29
29
  containerRef,
30
- resetMenuOffset,
30
+ resetMenuAnchor,
31
31
  setInputText,
32
32
  setSelectedField,
33
33
  setSelectedOperator,
@@ -0,0 +1,36 @@
1
+ import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
2
+ import type { FieldMetadata, FilterOperator, MenuState } from '../../types';
3
+ interface UseSegmentEditFlowOptions {
4
+ editing: {
5
+ editingChipId: string | null;
6
+ editingSegment: ChipSegment | null;
7
+ isBuildingEdit: boolean;
8
+ setSegmentFilterText: (text: string) => void;
9
+ startBuildingEdit: (segment: ChipSegment, currentText: string) => void;
10
+ clearEditing: () => void;
11
+ };
12
+ selectedField: FieldMetadata | null;
13
+ selectedOperator: FilterOperator | null;
14
+ buildingMultiValue: string | undefined;
15
+ resetState: (continueBuilding?: boolean) => void;
16
+ setSelectedField: (f: FieldMetadata | null) => void;
17
+ setSelectedOperator: (op: FilterOperator | null) => void;
18
+ setInputText: (text: string) => void;
19
+ setMenuState: (state: MenuState) => void;
20
+ setMenuAnchor: (el: HTMLElement | null) => void;
21
+ }
22
+ /**
23
+ * Cross-cutting inline-segment handlers that don't fit any single segment flow:
24
+ * - `handleSegmentFilterChange` — apply field-level acceptChar to value-segment text
25
+ * - `cancelSegmentEdit` — full state reset on blur-cancel of a committed-chip edit;
26
+ * preserves selectedField/Operator when bailing out of a building-chip edit (AS-929)
27
+ * - `handleMenuDiscard` — Escape from a menu (preserve building state, reset otherwise)
28
+ * - `handleBuildingChipClick` — enter inline-edit on a building-chip segment
29
+ */
30
+ export declare const useSegmentEditFlow: ({ editing, selectedField, selectedOperator, buildingMultiValue, resetState, setSelectedField, setSelectedOperator, setInputText, setMenuState, setMenuAnchor, }: UseSegmentEditFlowOptions) => {
31
+ handleSegmentFilterChange: (text: string) => void;
32
+ cancelSegmentEdit: () => void;
33
+ handleMenuDiscard: () => void;
34
+ handleBuildingChipClick: (segment: ChipSegment, anchorEl: HTMLElement) => void;
35
+ };
36
+ export {};
@@ -0,0 +1,69 @@
1
+ import { useCallback } from "react";
2
+ import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
+ import { SEGMENT_TO_MENU, applyAcceptChar } from "../../lib/index.js";
4
+ import { getInitialSegmentText } from "./lib/index.js";
5
+ const useSegmentEditFlow = ({ editing, selectedField, selectedOperator, buildingMultiValue, resetState, setSelectedField, setSelectedOperator, setInputText, setMenuState, setMenuAnchor })=>{
6
+ const { editingSegment, isBuildingEdit, setSegmentFilterText, startBuildingEdit, clearEditing } = editing;
7
+ const handleSegmentFilterChange = useCallback((text)=>{
8
+ const accept = selectedField?.acceptChar;
9
+ const next = editingSegment === SEGMENT_VARIANT.value && accept ? applyAcceptChar(text, accept) : text;
10
+ setSegmentFilterText(next);
11
+ }, [
12
+ selectedField,
13
+ editingSegment,
14
+ setSegmentFilterText
15
+ ]);
16
+ const cancelSegmentEdit = useCallback(()=>{
17
+ if (isBuildingEdit) {
18
+ clearEditing();
19
+ setMenuState('closed');
20
+ return;
21
+ }
22
+ setSelectedField(null);
23
+ setSelectedOperator(null);
24
+ clearEditing();
25
+ setMenuState('closed');
26
+ }, [
27
+ isBuildingEdit,
28
+ clearEditing,
29
+ setSelectedField,
30
+ setSelectedOperator,
31
+ setMenuState
32
+ ]);
33
+ const handleMenuDiscard = useCallback(()=>{
34
+ if (isBuildingEdit) {
35
+ clearEditing();
36
+ setMenuState('closed');
37
+ return;
38
+ }
39
+ resetState();
40
+ }, [
41
+ isBuildingEdit,
42
+ clearEditing,
43
+ resetState,
44
+ setMenuState
45
+ ]);
46
+ const handleBuildingChipClick = useCallback((segment, anchorEl)=>{
47
+ if (!selectedField) return;
48
+ setMenuAnchor(anchorEl);
49
+ const initialText = getInitialSegmentText(segment, selectedField, selectedOperator, buildingMultiValue);
50
+ startBuildingEdit(segment, initialText);
51
+ setInputText('');
52
+ setMenuState(SEGMENT_TO_MENU[segment]);
53
+ }, [
54
+ selectedField,
55
+ selectedOperator,
56
+ buildingMultiValue,
57
+ setMenuAnchor,
58
+ startBuildingEdit,
59
+ setInputText,
60
+ setMenuState
61
+ ]);
62
+ return {
63
+ handleSegmentFilterChange,
64
+ cancelSegmentEdit,
65
+ handleMenuDiscard,
66
+ handleBuildingChipClick
67
+ };
68
+ };
69
+ export { useSegmentEditFlow };
@@ -1,6 +1,5 @@
1
1
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
- import { NO_VALUE_PLACEHOLDER, findOptionByValue, getDateDisplayLabel, getOperatorLabel, isNoValueOperator } from "../../lib/index.js";
3
- import { getInvalidValueIndices } from "../useFilterInputAutocomplete/valueCommitHelpers.js";
2
+ import { NO_VALUE_PLACEHOLDER, findOptionByValue, getDateDisplayLabel, getInvalidValueIndices, getOperatorLabel, isNoValueOperator } from "../../lib/index.js";
4
3
  const INVALID_DATE = 'Invalid Date';
5
4
  const DATE_RANGE_SEPARATOR = ' – ';
6
5
  const MULTI_VALUE_SEPARATOR = ', ';
@@ -1,18 +1,23 @@
1
1
  import type { RefObject } from 'react';
2
+ import { type AnchorBounds } from '../lib';
2
3
  interface UseFilterInputPositioningProps {
3
4
  /** Ref to the trigger element (used for X anchor and container lookup via closest) */
4
5
  anchorRef?: RefObject<HTMLElement | null>;
5
6
  /** Explicit container ref (skips closest lookup) */
6
7
  containerRef?: RefObject<HTMLElement | null>;
7
- /** Override horizontal anchor calculation (receives containerRect, returns left px) */
8
- getAnchorLeft?: (containerRect: DOMRect) => number;
8
+ /** Override anchor calculation (receives containerRect, returns vertical/left bounds) */
9
+ getAnchorBounds?: (containerRect: DOMRect) => AnchorBounds;
10
+ /** Vertical gap below the anchor. Defaults to 12. */
11
+ gutter?: number;
9
12
  }
10
13
  /**
11
14
  * Shared positioning hook for all FilterInput dropdowns.
12
- * Anchors vertically to the query bar container and horizontally to the trigger element.
15
+ * Anchors the dropdown to the active element (chip segment, building chip, input
16
+ * or trigger) so it sits flush below the element being interacted with. Falls
17
+ * back to the trigger element's rect, then to the query bar container.
13
18
  * @param deps - extra values that force positioning recalculation (e.g. insertIndex)
14
19
  */
15
- export declare const useFilterInputPositioning: ({ anchorRef, containerRef, getAnchorLeft }: UseFilterInputPositioningProps, deps?: unknown[]) => {
20
+ export declare const useFilterInputPositioning: ({ anchorRef, containerRef, getAnchorBounds, gutter }: UseFilterInputPositioningProps, deps?: unknown[]) => {
16
21
  placement: "bottom-start";
17
22
  gutter: number;
18
23
  getAnchorRect: () => {