elastic-input 0.3.0 → 0.3.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/README.md CHANGED
@@ -139,6 +139,7 @@ interface FieldConfig {
139
139
  operators?: string[]; // Allowed operators (future use)
140
140
  description?: string; // Shown in autocomplete dropdown
141
141
  placeholder?: string | false; // Hint shown while typing a value (false to suppress)
142
+ suggestions?: boolean; // Whether fetchSuggestions is called (default true)
142
143
  }
143
144
  ```
144
145
 
@@ -1,7 +1,7 @@
1
1
  export interface Suggestion {
2
2
  text: string;
3
3
  label: string;
4
- description?: string;
4
+ description?: any;
5
5
  type?: string;
6
6
  replaceStart: number;
7
7
  replaceEnd: number;
@@ -1180,7 +1180,8 @@ const DEFAULT_COLORS = {
1180
1180
  regexAlternation: "#8250df",
1181
1181
  regexText: "#0a3069",
1182
1182
  matchedParenBg: "#fff3cd",
1183
- warning: "#d4a72c"
1183
+ warning: "#d4a72c",
1184
+ historyDescription: "#656d76"
1184
1185
  };
1185
1186
  const DARK_COLORS = {
1186
1187
  fieldName: "#79c0ff",
@@ -1208,7 +1209,8 @@ const DARK_COLORS = {
1208
1209
  regexAlternation: "#d2a8ff",
1209
1210
  regexText: "#a5d6ff",
1210
1211
  matchedParenBg: "#3d3222",
1211
- warning: "#e3b341"
1212
+ warning: "#e3b341",
1213
+ historyDescription: "#484f58"
1212
1214
  };
1213
1215
  const DEFAULT_STYLES = {
1214
1216
  fontFamily: "'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace",
@@ -1530,7 +1532,7 @@ class AutocompleteEngine {
1530
1532
  return this.searchHistory.slice(0, this.maxSuggestions).map((h) => ({
1531
1533
  text: AutocompleteEngine.wrapHistoryQuery(h.query),
1532
1534
  label: h.label || h.query,
1533
- description: h.timestamp ? new Date(h.timestamp).toLocaleDateString() : void 0,
1535
+ description: h.description,
1534
1536
  type: "history",
1535
1537
  replaceStart: start,
1536
1538
  replaceEnd: end,
@@ -2799,7 +2801,7 @@ function AutocompleteDropdown({
2799
2801
  whiteSpace: "normal",
2800
2802
  wordBreak: "break-all",
2801
2803
  width: "100%"
2802
- } }, highlightMatch(suggestion.label, suggestion.matchPartial, isSelected)), /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "8px", width: "100%" } }, suggestion.description && /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-desc", style: { ...getDropdownItemDescStyle(), flex: 1 } }, suggestion.description), /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-type", style: { ...getDropdownItemTypeStyle(isSelected, mergedStyles), marginLeft: "auto" } }, "history")))
2804
+ } }, highlightMatch(suggestion.label, suggestion.matchPartial, isSelected)), /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "8px", width: "100%" } }, suggestion.description != null && /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-desc", style: { ...getDropdownItemDescStyle(), flex: 1, color: mergedColors.historyDescription, opacity: 1 } }, suggestion.description), /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-type", style: { ...getDropdownItemTypeStyle(isSelected, mergedStyles), marginLeft: "auto" } }, "history")))
2803
2805
  );
2804
2806
  }
2805
2807
  if (suggestion.type === "savedSearch" && renderSavedSearchItem && suggestion.sourceData) {
@@ -3735,13 +3737,14 @@ function ElasticInput(props) {
3735
3737
  const effectiveMaxSuggestions = maxSuggestions || DEFAULT_MAX_SUGGESTIONS;
3736
3738
  const suggestDebounceMs = dropdownConfig == null ? void 0 : dropdownConfig.suggestDebounceMs;
3737
3739
  const dropdownMaxHeightPx = parseInt((stylesProp == null ? void 0 : stylesProp.dropdownMaxHeight) || "300", 10) || 300;
3738
- const enableSavedSearches = (featuresConfig == null ? void 0 : featuresConfig.savedSearches) ?? false;
3739
- const enableHistorySearch = (featuresConfig == null ? void 0 : featuresConfig.historySearch) ?? false;
3740
+ const enableSavedSearches = (featuresConfig == null ? void 0 : featuresConfig.savedSearches) ?? !!savedSearches;
3741
+ const enableHistorySearch = (featuresConfig == null ? void 0 : featuresConfig.historySearch) ?? !!searchHistory;
3740
3742
  const showSavedSearchHint = (dropdownConfig == null ? void 0 : dropdownConfig.showSavedSearchHint) ?? enableSavedSearches;
3741
3743
  const showHistoryHint = (dropdownConfig == null ? void 0 : dropdownConfig.showHistoryHint) ?? enableHistorySearch;
3742
3744
  const showOperators = (dropdownConfig == null ? void 0 : dropdownConfig.showOperators) !== false;
3743
3745
  const triggerOnNavigation = (dropdownOpenIsCallback || dropdownMode !== "input") && (dropdownConfig == null ? void 0 : dropdownConfig.onNavigation) !== false;
3744
3746
  const navigationDelay = (dropdownConfig == null ? void 0 : dropdownConfig.navigationDelay) ?? 0;
3747
+ const loadingDelay = (dropdownConfig == null ? void 0 : dropdownConfig.loadingDelay) ?? 0;
3745
3748
  const renderFieldHint = dropdownConfig == null ? void 0 : dropdownConfig.renderFieldHint;
3746
3749
  const renderHistoryItem = dropdownConfig == null ? void 0 : dropdownConfig.renderHistoryItem;
3747
3750
  const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
@@ -3786,6 +3789,7 @@ function ElasticInput(props) {
3786
3789
  const abortControllerRef = React.useRef(null);
3787
3790
  const highlightTimerRef = React.useRef(null);
3788
3791
  const navDelayTimerRef = React.useRef(null);
3792
+ const loadingDelayTimerRef = React.useRef(null);
3789
3793
  const asyncActiveRef = React.useRef(false);
3790
3794
  const datePickerInitRef = React.useRef(null);
3791
3795
  const datePickerReplaceRef = React.useRef(null);
@@ -3988,11 +3992,15 @@ function ElasticInput(props) {
3988
3992
  }
3989
3993
  }
3990
3994
  const resolvedField = result.context.fieldName ? engineRef.current.resolveField(result.context.fieldName) : void 0;
3991
- const willFetchAsync = !!(fetchSuggestionsProp && result.context.type === "FIELD_VALUE" && result.context.fieldName && (resolvedField == null ? void 0 : resolvedField.type) !== "boolean") || !!(typeof savedSearches === "function" && result.context.type === "SAVED_SEARCH") || !!(typeof searchHistory === "function" && result.context.type === "HISTORY_REF");
3995
+ const willFetchAsync = !!(fetchSuggestionsProp && result.context.type === "FIELD_VALUE" && result.context.fieldName && (resolvedField == null ? void 0 : resolvedField.type) !== "boolean" && (resolvedField == null ? void 0 : resolvedField.suggestions) !== false) || !!(typeof savedSearches === "function" && result.context.type === "SAVED_SEARCH") || !!(typeof searchHistory === "function" && result.context.type === "HISTORY_REF");
3992
3996
  if (result.showDatePicker) {
3993
3997
  asyncActiveRef.current = false;
3994
3998
  (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
3995
3999
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
4000
+ if (loadingDelayTimerRef.current) {
4001
+ clearTimeout(loadingDelayTimerRef.current);
4002
+ loadingDelayTimerRef.current = null;
4003
+ }
3996
4004
  const init = computeDatePickerInit(result.context, parseDateProp);
3997
4005
  const prevInit = datePickerInitRef.current;
3998
4006
  datePickerInitRef.current = init;
@@ -4020,6 +4028,10 @@ function ElasticInput(props) {
4020
4028
  asyncActiveRef.current = false;
4021
4029
  (_b = abortControllerRef.current) == null ? void 0 : _b.abort();
4022
4030
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
4031
+ if (loadingDelayTimerRef.current) {
4032
+ clearTimeout(loadingDelayTimerRef.current);
4033
+ loadingDelayTimerRef.current = null;
4034
+ }
4023
4035
  const newSuggestions = applyFieldHint(result.suggestions, result.context);
4024
4036
  if (newSuggestions.length > 0) {
4025
4037
  setSuggestions(newSuggestions);
@@ -4038,20 +4050,28 @@ function ElasticInput(props) {
4038
4050
  const token = result.context.token;
4039
4051
  const start = token ? token.start : offset;
4040
4052
  const end = token ? token.end : offset;
4041
- const loadingLabel = "Searching...";
4042
- const loadingSuggestion = {
4043
- text: "",
4044
- label: loadingLabel,
4045
- type: "loading",
4046
- replaceStart: start,
4047
- replaceEnd: end
4053
+ const showSpinner = () => {
4054
+ loadingDelayTimerRef.current = null;
4055
+ const loadingSuggestion = {
4056
+ text: "",
4057
+ label: "Searching...",
4058
+ type: "loading",
4059
+ replaceStart: start,
4060
+ replaceEnd: end
4061
+ };
4062
+ setSuggestions([loadingSuggestion]);
4063
+ if (!dropdownAlignToInput) setShowDropdown(false);
4064
+ setShowDatePicker(false);
4065
+ setSelectedSuggestionIndex(-1);
4066
+ showDropdownAtPosition(32, 300);
4048
4067
  };
4049
- setSuggestions([loadingSuggestion]);
4050
- if (!dropdownAlignToInput) setShowDropdown(false);
4051
- setShowDatePicker(false);
4052
- setSelectedSuggestionIndex(-1);
4068
+ if (loadingDelayTimerRef.current) clearTimeout(loadingDelayTimerRef.current);
4069
+ if (loadingDelay > 0) {
4070
+ loadingDelayTimerRef.current = setTimeout(showSpinner, loadingDelay);
4071
+ } else {
4072
+ showSpinner();
4073
+ }
4053
4074
  setAutocompleteContext(contextType);
4054
- showDropdownAtPosition(32, 300);
4055
4075
  }
4056
4076
  if (willFetchAsync) {
4057
4077
  const partial = result.context.partial;
@@ -4086,7 +4106,7 @@ function ElasticInput(props) {
4086
4106
  mapped = fetched.map((h) => ({
4087
4107
  text: AutocompleteEngine.wrapHistoryQuery(h.query),
4088
4108
  label: h.label || h.query,
4089
- description: h.timestamp ? new Date(h.timestamp).toLocaleDateString() : void 0,
4109
+ description: h.description,
4090
4110
  type: "history",
4091
4111
  replaceStart: start,
4092
4112
  replaceEnd: end,
@@ -4109,6 +4129,10 @@ function ElasticInput(props) {
4109
4129
  matchPartial: partial
4110
4130
  }));
4111
4131
  }
4132
+ if (loadingDelayTimerRef.current) {
4133
+ clearTimeout(loadingDelayTimerRef.current);
4134
+ loadingDelayTimerRef.current = null;
4135
+ }
4112
4136
  mapped = mapped.slice(0, effectiveMaxSuggestions);
4113
4137
  if (mapped.length > 0) {
4114
4138
  setSuggestions(mapped);
@@ -4130,6 +4154,10 @@ function ElasticInput(props) {
4130
4154
  }
4131
4155
  }
4132
4156
  } catch (e) {
4157
+ if (loadingDelayTimerRef.current) {
4158
+ clearTimeout(loadingDelayTimerRef.current);
4159
+ loadingDelayTimerRef.current = null;
4160
+ }
4133
4161
  if (!controller.signal.aborted) {
4134
4162
  const errorMsg = e instanceof Error ? e.message : "Error loading suggestions";
4135
4163
  const errorSuggestion = {
@@ -4147,7 +4175,7 @@ function ElasticInput(props) {
4147
4175
  }
4148
4176
  }, debounceMs);
4149
4177
  }
4150
- }, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions]);
4178
+ }, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay]);
4151
4179
  updateSuggestionsRef.current = updateSuggestionsFromTokens;
4152
4180
  const closeDropdown = React.useCallback(() => {
4153
4181
  var _a;
@@ -4348,6 +4376,7 @@ function ElasticInput(props) {
4348
4376
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
4349
4377
  if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
4350
4378
  if (navDelayTimerRef.current) clearTimeout(navDelayTimerRef.current);
4379
+ if (loadingDelayTimerRef.current) clearTimeout(loadingDelayTimerRef.current);
4351
4380
  (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
4352
4381
  };
4353
4382
  }, []);
@@ -4387,7 +4416,7 @@ function ElasticInput(props) {
4387
4416
  } else {
4388
4417
  const rect = getCaretRect();
4389
4418
  if (rect) {
4390
- const height = s.showDatePicker ? 350 : s.suggestions.length * 32;
4419
+ const height = s.showDatePicker ? 350 : capDropdownHeight(s.suggestions.length * 32, dropdownMaxHeightPx);
4391
4420
  setDropdownPosition(getDropdownPosition(rect, height, 300));
4392
4421
  }
4393
4422
  }
@@ -4398,7 +4427,7 @@ function ElasticInput(props) {
4398
4427
  window.removeEventListener("resize", reposition);
4399
4428
  window.removeEventListener("scroll", reposition, true);
4400
4429
  };
4401
- }, [dropdownAlignToInput]);
4430
+ }, [dropdownAlignToInput, dropdownMaxHeightPx]);
4402
4431
  const prevParenMatchRef = React.useRef(null);
4403
4432
  const prevColorsRef = React.useRef(colors);
4404
4433
  React.useEffect(() => {
package/dist/types.d.ts CHANGED
@@ -49,6 +49,8 @@ export interface FieldConfig {
49
49
  description?: string;
50
50
  /** Custom placeholder hint shown in the dropdown while typing a value for this field (e.g. "Search by company name..."). Overrides the default type-based hint. Set to `false` to suppress the hint entirely. */
51
51
  placeholder?: string | false;
52
+ /** Whether `fetchSuggestions` should be called for this field. Defaults to `true`. Set to `false` to skip the async fetch entirely (no "Searching..." spinner, no dropdown). */
53
+ suggestions?: boolean;
52
54
  }
53
55
  /** A saved/named search that users can reference with `#name` syntax. */
54
56
  export interface SavedSearch {
@@ -65,10 +67,10 @@ export interface SavedSearch {
65
67
  export interface HistoryEntry {
66
68
  /** The query string from the history entry. */
67
69
  query: string;
68
- /** Unix timestamp (ms) of when the query was executed. Used for ordering. */
69
- timestamp?: number;
70
70
  /** Optional label for display in the autocomplete dropdown. Falls back to `query`. */
71
71
  label?: string;
72
+ /** Optional description shown below the label (e.g. date, category). Rendered as-is. */
73
+ description?: React.ReactNode;
72
74
  }
73
75
  /** An item returned by the async `fetchSuggestions` callback for field value autocomplete. */
74
76
  export interface SuggestionItem {
@@ -138,6 +140,8 @@ export interface ColorConfig {
138
140
  matchedParenBg?: string;
139
141
  /** Warning-severity squiggly underlines (e.g. ambiguous precedence). */
140
142
  warning?: string;
143
+ /** Color for history item descriptions. Set to `'transparent'` to hide. */
144
+ historyDescription?: string;
141
145
  }
142
146
  /**
143
147
  * Structural and layout style overrides for the input and dropdown.
@@ -239,6 +243,10 @@ export interface DropdownConfig {
239
243
  * immediate. If the user types before the delay elapses, the timer is cancelled.
240
244
  * Ignored when `onNavigation` is false. @default 0 */
241
245
  navigationDelay?: number;
246
+ /** Delay in ms before showing the "Searching..." spinner on first entry into an
247
+ * async field. If the fetch resolves before the delay, the spinner never appears.
248
+ * Subsequent keystrokes preserve previous results regardless of this setting. @default 0 */
249
+ loadingDelay?: number;
242
250
  /** Custom renderer for field value hints. Return a React element for rich content,
243
251
  * or null/undefined for the default hint. */
244
252
  renderFieldHint?: (field: FieldConfig, partial: string) => React.ReactNode | null | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elastic-input",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Syntax-aware smart autocomplete input for Elastic query syntax",
5
5
  "license": "MIT",
6
6
  "repository": {