@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.
- package/dist/components/QueryBar/QueryBarContext/types.d.ts +2 -0
- package/dist/components/QueryBar/QueryBarContext/useQueryBarContextValue.d.ts +1 -0
- package/dist/components/QueryBar/QueryBarContext/useQueryBarContextValue.js +3 -1
- package/dist/components/QueryBar/QueryBarInput/QueryBarConnectorChip/QueryBarConnectorChip.d.ts +1 -1
- package/dist/components/QueryBar/QueryBarInput/QueryBarConnectorChip/QueryBarConnectorChip.js +12 -0
- package/dist/components/QueryBar/QueryBarInput/QueryBarFilterInput.d.ts +1 -0
- package/dist/components/QueryBar/QueryBarInput/QueryBarFilterInput.js +3 -2
- package/dist/components/QueryBar/QueryBarInput/QueryBarInput.js +16 -11
- package/dist/components/QueryBar/QueryBarInput/classes.d.ts +2 -0
- package/dist/components/QueryBar/QueryBarInput/classes.js +2 -1
- package/dist/components/QueryBar/QueryBarMenu/QueryBarMenu.js +8 -7
- package/dist/components/QueryBar/QueryBarMenu/QueryBarOperatorMenu.d.ts +6 -0
- package/dist/components/QueryBar/QueryBarMenu/QueryBarOperatorMenu.js +31 -9
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/deriveAutocompleteValues.js +5 -2
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useFocusManagement.d.ts +20 -0
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useFocusManagement.js +65 -0
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useInputHandlers.d.ts +27 -0
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useInputHandlers.js +104 -0
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuFlow.d.ts +1 -1
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuFlow.js +22 -79
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuPositioning.d.ts +2 -3
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useMenuPositioning.js +25 -14
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useQueryBarAutocomplete.d.ts +7 -6
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/useQueryBarAutocomplete.js +59 -109
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/valueCommitHelpers.d.ts +20 -0
- package/dist/components/QueryBar/hooks/useQueryBarAutocomplete/valueCommitHelpers.js +57 -0
- package/dist/components/QueryBar/lib/constants.js +26 -6
- package/dist/components/QueryBar/lib/fields.d.ts +12 -0
- package/dist/components/QueryBar/lib/fields.js +10 -0
- package/dist/components/QueryBar/lib/index.d.ts +1 -0
- package/dist/components/QueryBar/lib/index.js +2 -1
- package/dist/components/QueryBar/types.d.ts +6 -0
- package/dist/metadata/components.json +8 -2
- 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,
|
package/dist/components/QueryBar/QueryBarInput/QueryBarConnectorChip/QueryBarConnectorChip.js
CHANGED
|
@@ -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,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
|
|
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(
|
|
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
|
|
103
|
+
buildingChipData ? /*#__PURE__*/ jsxs("div", {
|
|
104
104
|
ref: buildingChipRef,
|
|
105
|
-
className: cn(
|
|
106
|
-
children:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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:
|
|
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
|
-
|
|
11
|
-
const
|
|
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
|
|
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-
|
|
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__*/
|
|
63
|
+
children: group.map((operator)=>/*#__PURE__*/ jsxs(DropdownMenuItem, {
|
|
51
64
|
value: operator,
|
|
52
65
|
onSelect: ()=>onSelect(operator),
|
|
53
|
-
children:
|
|
54
|
-
|
|
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
|
|
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
|
};
|