@wallarm-org/design-system 0.9.0 → 0.10.0-rc-feature-filter-attacks-components-oks.2

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 (34) hide show
  1. package/dist/components/QueryBar/QueryBarContext/types.d.ts +2 -0
  2. package/dist/components/QueryBar/QueryBarContext/useQueryBarContextValue.d.ts +1 -0
  3. package/dist/components/QueryBar/QueryBarContext/useQueryBarContextValue.js +3 -1
  4. package/dist/components/QueryBar/QueryBarInput/QueryBarConnectorChip/QueryBarConnectorChip.d.ts +1 -1
  5. package/dist/components/QueryBar/QueryBarInput/QueryBarConnectorChip/QueryBarConnectorChip.js +12 -0
  6. package/dist/components/QueryBar/QueryBarInput/QueryBarFilterInput.d.ts +1 -0
  7. package/dist/components/QueryBar/QueryBarInput/QueryBarFilterInput.js +3 -2
  8. package/dist/components/QueryBar/QueryBarInput/QueryBarInput.js +16 -11
  9. package/dist/components/QueryBar/QueryBarInput/classes.d.ts +2 -0
  10. package/dist/components/QueryBar/QueryBarInput/classes.js +2 -1
  11. package/dist/components/QueryBar/QueryBarMenu/QueryBarMenu.js +8 -7
  12. package/dist/components/QueryBar/QueryBarMenu/QueryBarOperatorMenu.d.ts +6 -0
  13. package/dist/components/QueryBar/QueryBarMenu/QueryBarOperatorMenu.js +31 -9
  14. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/deriveAutocompleteValues.js +5 -2
  15. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useFocusManagement.d.ts +20 -0
  16. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useFocusManagement.js +65 -0
  17. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useInputHandlers.d.ts +27 -0
  18. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useInputHandlers.js +104 -0
  19. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuFlow.d.ts +1 -1
  20. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuFlow.js +22 -79
  21. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuPositioning.d.ts +2 -3
  22. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuPositioning.js +25 -14
  23. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useQueryBarAutocomplete.d.ts +7 -6
  24. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useQueryBarAutocomplete.js +59 -109
  25. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/valueCommitHelpers.d.ts +20 -0
  26. package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/valueCommitHelpers.js +57 -0
  27. package/dist/components/QueryBar/lib/constants.js +26 -6
  28. package/dist/components/QueryBar/lib/fields.d.ts +12 -0
  29. package/dist/components/QueryBar/lib/fields.js +10 -0
  30. package/dist/components/QueryBar/lib/index.d.ts +1 -0
  31. package/dist/components/QueryBar/lib/index.js +2 -1
  32. package/dist/components/QueryBar/types.d.ts +6 -0
  33. package/dist/metadata/components.json +8 -2
  34. package/package.json +1 -1
@@ -35,4 +35,6 @@ export interface QueryBarContextValue {
35
35
  onCustomAttributeCommit: (customText: string) => void;
36
36
  /** Ref to the currently open menu content element */
37
37
  menuRef: RefObject<HTMLDivElement | null>;
38
+ /** Close autocomplete menu (used by connector chip to enforce single-dropdown constraint) */
39
+ closeAutocompleteMenu: () => void;
38
40
  }
@@ -24,6 +24,7 @@ interface AutocompleteForContext {
24
24
  handleCustomValueCommit: (customText: string) => void;
25
25
  handleCustomAttributeCommit: (customText: string) => void;
26
26
  menuRef: RefObject<HTMLDivElement | null>;
27
+ closeAutocompleteMenu: () => void;
27
28
  }
28
29
  interface UseQueryBarContextValueOptions {
29
30
  chips: QueryBarChipData[];
@@ -26,7 +26,8 @@ const useQueryBarContextValue = ({ chips, autocomplete, buildingChipRef, inputRe
26
26
  onCancelSegmentEdit: autocomplete.cancelSegmentEdit,
27
27
  onCustomValueCommit: autocomplete.handleCustomValueCommit,
28
28
  onCustomAttributeCommit: autocomplete.handleCustomAttributeCommit,
29
- menuRef: autocomplete.menuRef
29
+ menuRef: autocomplete.menuRef,
30
+ closeAutocompleteMenu: autocomplete.closeAutocompleteMenu
30
31
  }), [
31
32
  chips,
32
33
  autocomplete.buildingChipData,
@@ -50,6 +51,7 @@ const useQueryBarContextValue = ({ chips, autocomplete, buildingChipRef, inputRe
50
51
  autocomplete.handleCustomValueCommit,
51
52
  autocomplete.handleCustomAttributeCommit,
52
53
  autocomplete.menuRef,
54
+ autocomplete.closeAutocompleteMenu,
53
55
  buildingChipRef,
54
56
  inputRef,
55
57
  placeholder,
@@ -1,4 +1,4 @@
1
- import type { FC } from 'react';
1
+ import { type FC } from 'react';
2
2
  export type ConnectorVariant = 'and' | 'or';
3
3
  export interface QueryBarConnectorChipProps {
4
4
  chipId: string;
@@ -1,4 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
2
3
  import { CirclePlus } from "../../../../icons/CirclePlus.js";
3
4
  import { CircleSlash } from "../../../../icons/CircleSlash.js";
4
5
  import { cn } from "../../../../utils/cn.js";
@@ -6,15 +7,26 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter, DropdownMenuItem
6
7
  import { DropdownMenuTrigger } from "../../../DropdownMenu/DropdownMenuTrigger.js";
7
8
  import { Kbd, KbdGroup } from "../../../Kbd/index.js";
8
9
  import { VARIANT_LABELS } from "../../lib/constants.js";
10
+ import { useQueryBarContext } from "../../QueryBarContext/index.js";
9
11
  import { chipVariants, segmentContainer } from "../QueryBarChip/classes.js";
10
12
  import { connectorTextVariants } from "./classes.js";
11
13
  const QueryBarConnectorChip = ({ chipId, variant, onChange, className })=>{
14
+ const { menuOpen, closeAutocompleteMenu } = useQueryBarContext();
15
+ const [open, setOpen] = useState(false);
16
+ const handleOpenChange = useCallback((nextOpen)=>{
17
+ if (nextOpen) closeAutocompleteMenu();
18
+ setOpen(nextOpen);
19
+ }, [
20
+ closeAutocompleteMenu
21
+ ]);
12
22
  const label = VARIANT_LABELS[variant];
13
23
  return /*#__PURE__*/ jsxs(DropdownMenu, {
14
24
  positioning: {
15
25
  placement: 'bottom',
16
26
  gutter: 4
17
27
  },
28
+ open: open && !menuOpen,
29
+ onOpenChange: handleOpenChange,
18
30
  children: [
19
31
  /*#__PURE__*/ jsx(DropdownMenuTrigger, {
20
32
  asChild: true,
@@ -1,6 +1,7 @@
1
1
  import type { FC } from 'react';
2
2
  interface QueryBarFilterInputProps {
3
3
  hasContent: boolean;
4
+ minWidth?: number;
4
5
  }
5
6
  export declare const QueryBarFilterInput: FC<QueryBarFilterInputProps>;
6
7
  export {};
@@ -1,7 +1,8 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useQueryBarContext } from "../QueryBarContext/index.js";
3
3
  import { queryBarInputVariants } from "./classes.js";
4
- const QueryBarFilterInput = ({ hasContent })=>{
4
+ const CHAR_WIDTH_PX = 8;
5
+ const QueryBarFilterInput = ({ hasContent, minWidth = 4 })=>{
5
6
  const { inputText, inputRef, placeholder, error, menuOpen, onInputChange, onInputKeyDown, onInputClick } = useQueryBarContext();
6
7
  return /*#__PURE__*/ jsx("input", {
7
8
  ref: inputRef,
@@ -17,7 +18,7 @@ const QueryBarFilterInput = ({ hasContent })=>{
17
18
  onClick: onInputClick,
18
19
  placeholder: hasContent ? void 0 : placeholder,
19
20
  style: hasContent ? {
20
- width: `${Math.max(4, 8 * inputText.length)}px`
21
+ width: `${Math.max(minWidth, inputText.length * CHAR_WIDTH_PX)}px`
21
22
  } : void 0,
22
23
  className: queryBarInputVariants({
23
24
  hasContent
@@ -5,7 +5,7 @@ import { inputVariants } from "../../Input/classes.js";
5
5
  import { findChipSplitIndex, isMenuRelated } from "../lib/index.js";
6
6
  import { useQueryBarContext } from "../QueryBarContext/index.js";
7
7
  import { ChipsWithGaps, TrailingGap } from "./ChipsWithGaps.js";
8
- import { queryBarContainerVariants, queryBarInnerVariants } from "./classes.js";
8
+ import { buildingChipWrapperClass, queryBarContainerVariants, queryBarInnerVariants } from "./classes.js";
9
9
  import { EditingProvider } from "./QueryBarChip/EditingContext.js";
10
10
  import { QueryBarChip } from "./QueryBarChip/QueryBarChip.js";
11
11
  import { QueryBarFilterInput } from "./QueryBarFilterInput.js";
@@ -100,17 +100,22 @@ const QueryBarInput = ({ className, ...props })=>{
100
100
  hideTrailingGap: hideTrailingGap,
101
101
  ...chipsGapProps
102
102
  }),
103
- buildingChipData && /*#__PURE__*/ jsx("div", {
103
+ buildingChipData ? /*#__PURE__*/ jsxs("div", {
104
104
  ref: buildingChipRef,
105
- className: cn('min-w-0', hasContent && 'ml-8'),
106
- children: /*#__PURE__*/ jsx(QueryBarChip, {
107
- building: true,
108
- attribute: buildingChipData.attribute ?? '',
109
- operator: buildingChipData.operator,
110
- value: buildingChipData.value
111
- })
112
- }),
113
- /*#__PURE__*/ jsx(QueryBarFilterInput, {
105
+ className: cn(buildingChipWrapperClass, hasContent && 'ml-8'),
106
+ children: [
107
+ /*#__PURE__*/ jsx(QueryBarChip, {
108
+ building: true,
109
+ attribute: buildingChipData.attribute ?? '',
110
+ operator: buildingChipData.operator,
111
+ value: buildingChipData.value,
112
+ className: "border-none"
113
+ }),
114
+ /*#__PURE__*/ jsx(QueryBarFilterInput, {
115
+ hasContent: true
116
+ })
117
+ ]
118
+ }) : /*#__PURE__*/ jsx(QueryBarFilterInput, {
114
119
  hasContent: hasContent
115
120
  }),
116
121
  /*#__PURE__*/ jsx(ChipsWithGaps, {
@@ -6,6 +6,8 @@ export declare const queryBarContainerVariants: (props?: ({
6
6
  export declare const queryBarInnerVariants: (props?: ({
7
7
  hasContent?: boolean | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
+ /** Wrapper that visually groups the building chip and the input as one unit */
10
+ export declare const buildingChipWrapperClass = "flex items-center gap-2 min-w-0 rounded-8 border border-solid border-border-strong-primary bg-badge-badge-bg";
9
11
  /** Native input element inside the query bar */
10
12
  export declare const queryBarInputVariants: (props?: ({
11
13
  hasContent?: boolean | null | undefined;
@@ -21,6 +21,7 @@ const queryBarInnerVariants = cva('flex min-h-full flex-1 cursor-text flex-wrap
21
21
  hasContent: false
22
22
  }
23
23
  });
24
+ const buildingChipWrapperClass = 'flex items-center gap-2 min-w-0 rounded-8 border border-solid border-border-strong-primary bg-badge-badge-bg';
24
25
  const queryBarInputVariants = cva('h-auto border-none bg-transparent p-0 text-sm shadow-none outline-none ring-0', {
25
26
  variants: {
26
27
  hasContent: {
@@ -32,4 +33,4 @@ const queryBarInputVariants = cva('h-auto border-none bg-transparent p-0 text-sm
32
33
  hasContent: false
33
34
  }
34
35
  });
35
- export { queryBarContainerVariants, queryBarInnerVariants, queryBarInputVariants };
36
+ export { buildingChipWrapperClass, queryBarContainerVariants, queryBarInnerVariants, queryBarInputVariants };
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { isBetweenOperator, isMultiSelectOperator } from "../lib/index.js";
2
+ import { getFieldValues, isBetweenOperator, isMultiSelectOperator } from "../lib/index.js";
3
3
  import { QueryBarDateValueMenu } from "./QueryBarDateValueMenu/index.js";
4
4
  import { QueryBarFieldMenu } from "./QueryBarFieldMenu/index.js";
5
5
  import { QueryBarOperatorMenu } from "./QueryBarOperatorMenu.js";
@@ -7,15 +7,15 @@ import { QueryBarValueMenu } from "./QueryBarValueMenu/index.js";
7
7
  const QueryBarMenu = ({ fields, autocomplete })=>{
8
8
  const { inputText, menuState, selectedField, selectedOperator, menuPositioning, editingMultiValues, editingSingleValue, editingDateIsAbsolute, inputRef, menuRef, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleRangeSelect, handleMenuClose, handleMenuDiscard, handleBuildingValueChange, segmentFilterText, editingSegment } = autocomplete;
9
9
  const fieldFilterText = 'attribute' === editingSegment ? segmentFilterText : inputText;
10
- const operatorFilterText = '';
10
+ const operatorFilterText = editingSegment ? '' : inputText;
11
+ const selectedFieldValues = selectedField ? getFieldValues(selectedField) : [];
11
12
  const valueFilterText = (()=>{
12
13
  if ('value' !== editingSegment) return inputText;
13
14
  if (!isMultiSelectOperator(selectedOperator)) return segmentFilterText;
14
15
  const lastToken = segmentFilterText.split(',').pop()?.trim() ?? '';
15
16
  if (!lastToken) return '';
16
- const fieldValues = selectedField?.values;
17
- if (fieldValues) {
18
- const isKnownValue = fieldValues.some((v)=>v.label.toLowerCase() === lastToken.toLowerCase() || String(v.value).toLowerCase() === lastToken.toLowerCase());
17
+ if (selectedFieldValues.length > 0) {
18
+ const isKnownValue = selectedFieldValues.some((v)=>v.label.toLowerCase() === lastToken.toLowerCase() || String(v.value).toLowerCase() === lastToken.toLowerCase());
19
19
  if (isKnownValue) return '';
20
20
  }
21
21
  return lastToken;
@@ -35,6 +35,7 @@ const QueryBarMenu = ({ fields, autocomplete })=>{
35
35
  }),
36
36
  selectedField && /*#__PURE__*/ jsx(QueryBarOperatorMenu, {
37
37
  fieldType: selectedField.type,
38
+ operators: selectedField.operators,
38
39
  open: 'operator' === menuState,
39
40
  onSelect: handleOperatorSelect,
40
41
  onOpenChange: ()=>handleMenuClose(),
@@ -58,8 +59,8 @@ const QueryBarMenu = ({ fields, autocomplete })=>{
58
59
  menuRef: menuRef,
59
60
  filterText: valueFilterText,
60
61
  initialValue: null != editingSingleValue ? String(editingSingleValue) : void 0
61
- }) : /*#__PURE__*/ jsx(QueryBarValueMenu, {
62
- values: selectedField.values || [],
62
+ }) : selectedFieldValues.length > 0 && /*#__PURE__*/ jsx(QueryBarValueMenu, {
63
+ values: selectedFieldValues,
63
64
  open: 'value' === menuState,
64
65
  onSelect: handleValueSelect,
65
66
  onCommit: handleMultiCommit,
@@ -6,6 +6,12 @@ export interface QueryBarOperatorMenuProps {
6
6
  * The field type to determine which operators to show
7
7
  */
8
8
  fieldType: FieldType;
9
+ /**
10
+ * Optional list of operators from field config.
11
+ * When provided, only these operators are shown (preserving type-based grouping).
12
+ * Falls back to OPERATORS_BY_TYPE[fieldType] when not set.
13
+ */
14
+ operators?: FilterOperator[];
9
15
  /**
10
16
  * Callback when an operator is selected
11
17
  */
@@ -4,13 +4,26 @@ import { cn } from "../../../utils/cn.js";
4
4
  import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter, DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemText, DropdownMenuSeparator } from "../../DropdownMenu/index.js";
5
5
  import { Kbd } from "../../Kbd/Kbd.js";
6
6
  import { KbdGroup } from "../../Kbd/KbdGroup.js";
7
- import { OPERATORS_BY_TYPE, getOperatorLabel } from "../lib/index.js";
7
+ import { OPERATORS_BY_TYPE, OPERATOR_SYMBOLS, getOperatorLabel } from "../lib/index.js";
8
8
  import { MenuEmptyState } from "./MenuEmptyState.js";
9
9
  import { useKeyboardNav } from "./useKeyboardNav.js";
10
- const QueryBarOperatorMenu = ({ fieldType, onSelect, open = false, onOpenChange, onEscape, positioning, inputRef, filterText = '', menuRef, className })=>{
11
- const operatorGroups = OPERATORS_BY_TYPE[fieldType] || [];
10
+ function filterOperatorGroups(groups, operators) {
11
+ const allowed = new Set(operators);
12
+ return groups.map((group)=>group.filter((op)=>allowed.has(op))).filter((group)=>group.length > 0);
13
+ }
14
+ const HIDE_SYMBOLS_FOR = new Set([
15
+ 'boolean'
16
+ ]);
17
+ const QueryBarOperatorMenu = ({ fieldType, operators, onSelect, open = false, onOpenChange, onEscape, positioning, inputRef, filterText = '', menuRef, className })=>{
12
18
  const query = filterText.toLowerCase();
13
- const filteredGroups = useMemo(()=>query ? operatorGroups.map((group)=>group.filter((op)=>getOperatorLabel(op, fieldType).toLowerCase().includes(query))).filter((group)=>group.length > 0) : operatorGroups, [
19
+ const operatorGroups = useMemo(()=>{
20
+ const allGroups = OPERATORS_BY_TYPE[fieldType] || [];
21
+ return operators ? filterOperatorGroups(allGroups, operators) : allGroups;
22
+ }, [
23
+ fieldType,
24
+ operators
25
+ ]);
26
+ const filteredGroups = useMemo(()=>query ? operatorGroups.map((group)=>group.filter((op)=>getOperatorLabel(op, fieldType).toLowerCase().includes(query) || OPERATOR_SYMBOLS[op].toLowerCase().includes(query) || op.toLowerCase().includes(query))).filter((group)=>group.length > 0) : operatorGroups, [
14
27
  operatorGroups,
15
28
  fieldType,
16
29
  query
@@ -42,17 +55,26 @@ const QueryBarOperatorMenu = ({ fieldType, onSelect, open = false, onOpenChange,
42
55
  onHighlightChange: onHighlightChange,
43
56
  children: /*#__PURE__*/ jsxs(DropdownMenuContent, {
44
57
  ref: menuRef,
45
- className: cn('w-64', className),
58
+ className: cn('w-256', className),
46
59
  children: [
47
60
  filteredGroups.length > 0 ? filteredGroups.map((group, groupIdx)=>/*#__PURE__*/ jsxs(Fragment, {
48
61
  children: [
49
62
  /*#__PURE__*/ jsx(DropdownMenuGroup, {
50
- children: group.map((operator)=>/*#__PURE__*/ jsx(DropdownMenuItem, {
63
+ children: group.map((operator)=>/*#__PURE__*/ jsxs(DropdownMenuItem, {
51
64
  value: operator,
52
65
  onSelect: ()=>onSelect(operator),
53
- children: /*#__PURE__*/ jsx(DropdownMenuItemText, {
54
- children: getOperatorLabel(operator, fieldType)
55
- })
66
+ children: [
67
+ /*#__PURE__*/ jsx(DropdownMenuItemText, {
68
+ children: getOperatorLabel(operator, fieldType)
69
+ }),
70
+ !HIDE_SYMBOLS_FOR.has(fieldType) && /*#__PURE__*/ jsx("span", {
71
+ className: "ml-auto inline-flex items-center",
72
+ children: /*#__PURE__*/ jsx(Kbd, {
73
+ className: "font-mono",
74
+ children: OPERATOR_SYMBOLS[operator]
75
+ })
76
+ })
77
+ ]
56
78
  }, operator))
57
79
  }),
58
80
  groupIdx < filteredGroups.length - 1 && /*#__PURE__*/ jsx(DropdownMenuSeparator, {})
@@ -1,4 +1,4 @@
1
- import { chipIdToConditionIndex, getDateDisplayLabel, getOperatorLabel, isDatePreset, isMultiSelectOperator } from "../../lib/index.js";
1
+ import { chipIdToConditionIndex, getDateDisplayLabel, getFieldValues, getOperatorLabel, isDatePreset, isMultiSelectOperator } from "../../lib/index.js";
2
2
  const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperator, conditions, buildingMultiValue, dateRangeFromValue, segmentFilterText })=>{
3
3
  const isBuilding = null !== selectedField && !editingChipId;
4
4
  const editingDateIsAbsolute = (()=>{
@@ -20,7 +20,10 @@ const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperat
20
20
  const values = Array.isArray(condition.value) ? condition.value : null != condition.value ? [
21
21
  condition.value
22
22
  ] : [];
23
- if (condition.error && selectedField?.values) return values.filter((v)=>selectedField.values.some((opt)=>opt.value === v));
23
+ if (condition.error && selectedField) {
24
+ const fieldValues = getFieldValues(selectedField);
25
+ if (fieldValues.length > 0) return values.filter((v)=>fieldValues.some((opt)=>opt.value === v));
26
+ }
24
27
  return values;
25
28
  })();
26
29
  const editingSingleValue = (()=>{
@@ -0,0 +1,20 @@
1
+ import type { FocusEvent, RefObject } from 'react';
2
+ import type { MenuState } from '../../types';
3
+ interface UseFocusManagementDeps {
4
+ menuState: MenuState;
5
+ isFocused: boolean;
6
+ conditionsLength: number;
7
+ inputText: string;
8
+ containerRef: RefObject<HTMLElement | null>;
9
+ inputRef: RefObject<HTMLInputElement | null>;
10
+ editingSegment: string | null;
11
+ setIsFocused: (focused: boolean) => void;
12
+ setMenuState: (state: MenuState) => void;
13
+ resetMenuOffset: () => void;
14
+ resetState: (continueBuilding?: boolean) => void;
15
+ }
16
+ export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, containerRef, inputRef, editingSegment, setIsFocused, setMenuState, resetMenuOffset, resetState, }: UseFocusManagementDeps) => {
17
+ handleFocus: (e: FocusEvent) => void;
18
+ handleBlur: (e: FocusEvent) => void;
19
+ };
20
+ export {};
@@ -0,0 +1,65 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { isMenuRelated } from "../../lib/index.js";
3
+ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, containerRef, inputRef, editingSegment, setIsFocused, setMenuState, resetMenuOffset, resetState })=>{
4
+ const handleFocus = useCallback((e)=>{
5
+ if (e.target?.closest?.('[data-slot="query-bar-connector-chip"]')) return;
6
+ setIsFocused(true);
7
+ }, [
8
+ setIsFocused
9
+ ]);
10
+ const handleBlur = useCallback((e)=>{
11
+ const related = e.relatedTarget;
12
+ if (containerRef.current?.contains(related)) return;
13
+ if (isMenuRelated(related)) return;
14
+ setIsFocused(false);
15
+ resetState();
16
+ }, [
17
+ containerRef,
18
+ resetState,
19
+ setIsFocused
20
+ ]);
21
+ const prevFocusedRef = useRef(false);
22
+ useEffect(()=>{
23
+ if (isFocused && !prevFocusedRef.current && 0 === conditionsLength && '' === inputText) {
24
+ resetMenuOffset();
25
+ setMenuState('field');
26
+ }
27
+ prevFocusedRef.current = isFocused;
28
+ }, [
29
+ isFocused,
30
+ conditionsLength,
31
+ inputText,
32
+ resetMenuOffset,
33
+ setMenuState
34
+ ]);
35
+ useEffect(()=>{
36
+ if ('closed' === menuState) return;
37
+ let outerRaf = 0;
38
+ let innerRaf = 0;
39
+ outerRaf = requestAnimationFrame(()=>{
40
+ innerRaf = requestAnimationFrame(()=>{
41
+ if (editingSegment) {
42
+ const segmentInput = containerRef.current?.querySelector(`[data-slot="segment-${editingSegment}"] input`);
43
+ if (segmentInput && document.activeElement !== segmentInput) {
44
+ segmentInput.focus();
45
+ segmentInput.select();
46
+ } else if (!segmentInput && document.activeElement !== inputRef.current) inputRef.current?.focus();
47
+ } else if (document.activeElement !== inputRef.current) inputRef.current?.focus();
48
+ });
49
+ });
50
+ return ()=>{
51
+ cancelAnimationFrame(outerRaf);
52
+ cancelAnimationFrame(innerRaf);
53
+ };
54
+ }, [
55
+ menuState,
56
+ inputRef,
57
+ editingSegment,
58
+ containerRef
59
+ ]);
60
+ return {
61
+ handleFocus,
62
+ handleBlur
63
+ };
64
+ };
65
+ export { useFocusManagement };
@@ -0,0 +1,27 @@
1
+ import type { ChangeEvent, KeyboardEvent, MutableRefObject, RefObject } from 'react';
2
+ import type { FieldMetadata, FilterOperator, MenuState } from '../../types';
3
+ interface UseInputHandlersDeps {
4
+ inputText: string;
5
+ menuState: MenuState;
6
+ selectedField: FieldMetadata | null;
7
+ isFocused: boolean;
8
+ fields: FieldMetadata[];
9
+ inputRef: RefObject<HTMLInputElement | null>;
10
+ conditionsLengthRef: MutableRefObject<number>;
11
+ effectiveInsertIndexRef: MutableRefObject<number>;
12
+ setInputText: (text: string) => void;
13
+ setMenuState: (state: MenuState) => void;
14
+ setInsertIndex: (fn: (prev: number | null) => number) => void;
15
+ resetMenuOffset: () => void;
16
+ removeConditionAtIndex: (index: number) => void;
17
+ handleFieldSelect: (field: FieldMetadata) => void;
18
+ handleOperatorSelect: (operator: FilterOperator) => void;
19
+ handleCustomValueCommit: (text: string) => void;
20
+ }
21
+ export declare const useInputHandlers: ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, }: UseInputHandlersDeps) => {
22
+ handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
23
+ handleInputClick: () => void;
24
+ handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
25
+ menuRef: RefObject<HTMLDivElement | null>;
26
+ };
27
+ export {};
@@ -0,0 +1,104 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { OPERATOR_SYMBOLS, getOperatorFromLabel, hasFieldValues } from "../../lib/index.js";
3
+ const useInputHandlers = ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
4
+ const menuRef = useRef(null);
5
+ const handleInputChange = useCallback((e)=>{
6
+ const text = e.target.value;
7
+ setInputText(text);
8
+ if (text && !selectedField) setMenuState('field');
9
+ else if (!text && !selectedField) setMenuState(isFocused && 0 === conditionsLengthRef.current ? 'field' : 'closed');
10
+ }, [
11
+ selectedField,
12
+ isFocused,
13
+ setInputText,
14
+ setMenuState,
15
+ conditionsLengthRef
16
+ ]);
17
+ const handleInputClick = useCallback(()=>{
18
+ inputRef.current?.focus();
19
+ if ('closed' === menuState && !selectedField) {
20
+ resetMenuOffset();
21
+ setMenuState('field');
22
+ }
23
+ }, [
24
+ menuState,
25
+ selectedField,
26
+ resetMenuOffset,
27
+ inputRef,
28
+ setMenuState
29
+ ]);
30
+ const handleKeyDown = useCallback((e)=>{
31
+ if ('ArrowDown' === e.key && 'closed' !== menuState) {
32
+ e.preventDefault();
33
+ menuRef.current?.focus();
34
+ return;
35
+ }
36
+ if ('Enter' === e.key && 'field' === menuState && !selectedField && inputText.trim()) {
37
+ const trimmed = inputText.trim().toLowerCase();
38
+ const matched = fields.find((f)=>f.label.toLowerCase() === trimmed || f.name.toLowerCase() === trimmed);
39
+ if (matched) {
40
+ e.preventDefault();
41
+ handleFieldSelect(matched);
42
+ setInputText('');
43
+ return;
44
+ }
45
+ }
46
+ if ('Enter' === e.key && 'operator' === menuState && selectedField && inputText.trim()) {
47
+ const trimmed = inputText.trim();
48
+ let matched = getOperatorFromLabel(trimmed, selectedField.type);
49
+ if (!matched) {
50
+ const symbolMatch = Object.entries(OPERATOR_SYMBOLS).find(([, sym])=>sym.toLowerCase() === trimmed.toLowerCase());
51
+ if (symbolMatch) matched = symbolMatch[0];
52
+ }
53
+ if (!matched) {
54
+ const allOperators = selectedField.operators ?? [];
55
+ const rawMatch = allOperators.find((op)=>op.toLowerCase() === trimmed.toLowerCase());
56
+ if (rawMatch) matched = rawMatch;
57
+ }
58
+ if (matched) {
59
+ e.preventDefault();
60
+ handleOperatorSelect(matched);
61
+ setInputText('');
62
+ return;
63
+ }
64
+ }
65
+ if ('Enter' === e.key && 'value' === menuState && selectedField && !hasFieldValues(selectedField) && inputText.trim()) {
66
+ e.preventDefault();
67
+ handleCustomValueCommit(inputText);
68
+ return;
69
+ }
70
+ if ('Backspace' === e.key && !e.repeat && '' === inputText && conditionsLengthRef.current > 0) {
71
+ e.preventDefault();
72
+ const removeIdx = effectiveInsertIndexRef.current - 1;
73
+ if (removeIdx >= 0) {
74
+ removeConditionAtIndex(removeIdx);
75
+ setInsertIndex((prev)=>{
76
+ const eff = prev ?? conditionsLengthRef.current;
77
+ return eff > 0 ? eff - 1 : 0;
78
+ });
79
+ }
80
+ setMenuState('closed');
81
+ }
82
+ }, [
83
+ inputText,
84
+ removeConditionAtIndex,
85
+ menuState,
86
+ selectedField,
87
+ fields,
88
+ handleFieldSelect,
89
+ handleOperatorSelect,
90
+ handleCustomValueCommit,
91
+ setInputText,
92
+ setMenuState,
93
+ setInsertIndex,
94
+ conditionsLengthRef,
95
+ effectiveInsertIndexRef
96
+ ]);
97
+ return {
98
+ handleInputChange,
99
+ handleInputClick,
100
+ handleKeyDown,
101
+ menuRef
102
+ };
103
+ };
104
+ export { useInputHandlers };
@@ -13,7 +13,7 @@ interface MenuFlowDeps {
13
13
  insertIndex: number;
14
14
  upsertCondition: (field: FieldMetadata, operator: FilterOperator, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: boolean, dateOrigin?: 'relative' | 'absolute') => void;
15
15
  conditions: Condition[];
16
- resetState: () => void;
16
+ resetState: (continueBuilding?: boolean) => void;
17
17
  dateRange: {
18
18
  selectValue: (val: string) => string[] | null;
19
19
  };