elastic-input 0.3.1 → 0.3.3

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.
@@ -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;
@@ -1530,7 +1530,7 @@ class AutocompleteEngine {
1530
1530
  return this.searchHistory.slice(0, this.maxSuggestions).map((h) => ({
1531
1531
  text: AutocompleteEngine.wrapHistoryQuery(h.query),
1532
1532
  label: h.label || h.query,
1533
- description: h.timestamp ? new Date(h.timestamp).toLocaleDateString() : void 0,
1533
+ description: h.description,
1534
1534
  type: "history",
1535
1535
  replaceStart: start,
1536
1536
  replaceEnd: end,
@@ -2799,7 +2799,7 @@ function AutocompleteDropdown({
2799
2799
  whiteSpace: "normal",
2800
2800
  wordBreak: "break-all",
2801
2801
  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")))
2802
+ } }, 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 } }, suggestion.description), /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-type", style: { ...getDropdownItemTypeStyle(isSelected, mergedStyles), marginLeft: "auto" } }, "history")))
2803
2803
  );
2804
2804
  }
2805
2805
  if (suggestion.type === "savedSearch" && renderSavedSearchItem && suggestion.sourceData) {
@@ -3735,13 +3735,14 @@ function ElasticInput(props) {
3735
3735
  const effectiveMaxSuggestions = maxSuggestions || DEFAULT_MAX_SUGGESTIONS;
3736
3736
  const suggestDebounceMs = dropdownConfig == null ? void 0 : dropdownConfig.suggestDebounceMs;
3737
3737
  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;
3738
+ const enableSavedSearches = (featuresConfig == null ? void 0 : featuresConfig.savedSearches) ?? !!savedSearches;
3739
+ const enableHistorySearch = (featuresConfig == null ? void 0 : featuresConfig.historySearch) ?? !!searchHistory;
3740
3740
  const showSavedSearchHint = (dropdownConfig == null ? void 0 : dropdownConfig.showSavedSearchHint) ?? enableSavedSearches;
3741
3741
  const showHistoryHint = (dropdownConfig == null ? void 0 : dropdownConfig.showHistoryHint) ?? enableHistorySearch;
3742
3742
  const showOperators = (dropdownConfig == null ? void 0 : dropdownConfig.showOperators) !== false;
3743
3743
  const triggerOnNavigation = (dropdownOpenIsCallback || dropdownMode !== "input") && (dropdownConfig == null ? void 0 : dropdownConfig.onNavigation) !== false;
3744
3744
  const navigationDelay = (dropdownConfig == null ? void 0 : dropdownConfig.navigationDelay) ?? 0;
3745
+ const loadingDelay = (dropdownConfig == null ? void 0 : dropdownConfig.loadingDelay) ?? 0;
3745
3746
  const renderFieldHint = dropdownConfig == null ? void 0 : dropdownConfig.renderFieldHint;
3746
3747
  const renderHistoryItem = dropdownConfig == null ? void 0 : dropdownConfig.renderHistoryItem;
3747
3748
  const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
@@ -3786,6 +3787,7 @@ function ElasticInput(props) {
3786
3787
  const abortControllerRef = React.useRef(null);
3787
3788
  const highlightTimerRef = React.useRef(null);
3788
3789
  const navDelayTimerRef = React.useRef(null);
3790
+ const loadingDelayTimerRef = React.useRef(null);
3789
3791
  const asyncActiveRef = React.useRef(false);
3790
3792
  const datePickerInitRef = React.useRef(null);
3791
3793
  const datePickerReplaceRef = React.useRef(null);
@@ -3993,6 +3995,10 @@ function ElasticInput(props) {
3993
3995
  asyncActiveRef.current = false;
3994
3996
  (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
3995
3997
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
3998
+ if (loadingDelayTimerRef.current) {
3999
+ clearTimeout(loadingDelayTimerRef.current);
4000
+ loadingDelayTimerRef.current = null;
4001
+ }
3996
4002
  const init = computeDatePickerInit(result.context, parseDateProp);
3997
4003
  const prevInit = datePickerInitRef.current;
3998
4004
  datePickerInitRef.current = init;
@@ -4020,6 +4026,10 @@ function ElasticInput(props) {
4020
4026
  asyncActiveRef.current = false;
4021
4027
  (_b = abortControllerRef.current) == null ? void 0 : _b.abort();
4022
4028
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
4029
+ if (loadingDelayTimerRef.current) {
4030
+ clearTimeout(loadingDelayTimerRef.current);
4031
+ loadingDelayTimerRef.current = null;
4032
+ }
4023
4033
  const newSuggestions = applyFieldHint(result.suggestions, result.context);
4024
4034
  if (newSuggestions.length > 0) {
4025
4035
  setSuggestions(newSuggestions);
@@ -4038,20 +4048,28 @@ function ElasticInput(props) {
4038
4048
  const token = result.context.token;
4039
4049
  const start = token ? token.start : offset;
4040
4050
  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
4051
+ const showSpinner = () => {
4052
+ loadingDelayTimerRef.current = null;
4053
+ const loadingSuggestion = {
4054
+ text: "",
4055
+ label: "Searching...",
4056
+ type: "loading",
4057
+ replaceStart: start,
4058
+ replaceEnd: end
4059
+ };
4060
+ setSuggestions([loadingSuggestion]);
4061
+ if (!dropdownAlignToInput) setShowDropdown(false);
4062
+ setShowDatePicker(false);
4063
+ setSelectedSuggestionIndex(-1);
4064
+ showDropdownAtPosition(32, 300);
4048
4065
  };
4049
- setSuggestions([loadingSuggestion]);
4050
- if (!dropdownAlignToInput) setShowDropdown(false);
4051
- setShowDatePicker(false);
4052
- setSelectedSuggestionIndex(-1);
4066
+ if (loadingDelayTimerRef.current) clearTimeout(loadingDelayTimerRef.current);
4067
+ if (loadingDelay > 0) {
4068
+ loadingDelayTimerRef.current = setTimeout(showSpinner, loadingDelay);
4069
+ } else {
4070
+ showSpinner();
4071
+ }
4053
4072
  setAutocompleteContext(contextType);
4054
- showDropdownAtPosition(32, 300);
4055
4073
  }
4056
4074
  if (willFetchAsync) {
4057
4075
  const partial = result.context.partial;
@@ -4086,7 +4104,7 @@ function ElasticInput(props) {
4086
4104
  mapped = fetched.map((h) => ({
4087
4105
  text: AutocompleteEngine.wrapHistoryQuery(h.query),
4088
4106
  label: h.label || h.query,
4089
- description: h.timestamp ? new Date(h.timestamp).toLocaleDateString() : void 0,
4107
+ description: h.description,
4090
4108
  type: "history",
4091
4109
  replaceStart: start,
4092
4110
  replaceEnd: end,
@@ -4109,6 +4127,10 @@ function ElasticInput(props) {
4109
4127
  matchPartial: partial
4110
4128
  }));
4111
4129
  }
4130
+ if (loadingDelayTimerRef.current) {
4131
+ clearTimeout(loadingDelayTimerRef.current);
4132
+ loadingDelayTimerRef.current = null;
4133
+ }
4112
4134
  mapped = mapped.slice(0, effectiveMaxSuggestions);
4113
4135
  if (mapped.length > 0) {
4114
4136
  setSuggestions(mapped);
@@ -4130,6 +4152,10 @@ function ElasticInput(props) {
4130
4152
  }
4131
4153
  }
4132
4154
  } catch (e) {
4155
+ if (loadingDelayTimerRef.current) {
4156
+ clearTimeout(loadingDelayTimerRef.current);
4157
+ loadingDelayTimerRef.current = null;
4158
+ }
4133
4159
  if (!controller.signal.aborted) {
4134
4160
  const errorMsg = e instanceof Error ? e.message : "Error loading suggestions";
4135
4161
  const errorSuggestion = {
@@ -4147,7 +4173,7 @@ function ElasticInput(props) {
4147
4173
  }
4148
4174
  }, debounceMs);
4149
4175
  }
4150
- }, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions]);
4176
+ }, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay]);
4151
4177
  updateSuggestionsRef.current = updateSuggestionsFromTokens;
4152
4178
  const closeDropdown = React.useCallback(() => {
4153
4179
  var _a;
@@ -4348,6 +4374,7 @@ function ElasticInput(props) {
4348
4374
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
4349
4375
  if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
4350
4376
  if (navDelayTimerRef.current) clearTimeout(navDelayTimerRef.current);
4377
+ if (loadingDelayTimerRef.current) clearTimeout(loadingDelayTimerRef.current);
4351
4378
  (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
4352
4379
  };
4353
4380
  }, []);
@@ -4662,6 +4689,12 @@ function ElasticInput(props) {
4662
4689
  case "Enter":
4663
4690
  if (s.selectedSuggestionIndex >= 0) {
4664
4691
  const selected = s.suggestions[s.selectedSuggestionIndex];
4692
+ if (selected.type === "loading" || selected.type === "error") {
4693
+ e.preventDefault();
4694
+ closeDropdown();
4695
+ if (onSearch) onSearch(currentValueRef.current, s.ast);
4696
+ return;
4697
+ }
4665
4698
  if (selected.type === "hint" && selected.text !== "#" && selected.text !== "!") {
4666
4699
  e.preventDefault();
4667
4700
  closeDropdown();
@@ -4684,7 +4717,10 @@ function ElasticInput(props) {
4684
4717
  acceptSuggestion(selected, "Enter");
4685
4718
  return;
4686
4719
  }
4687
- break;
4720
+ e.preventDefault();
4721
+ closeDropdown();
4722
+ if (onSearch) onSearch(currentValueRef.current, s.ast);
4723
+ return;
4688
4724
  case "Tab": {
4689
4725
  if (onTabProp) {
4690
4726
  e.preventDefault();
package/dist/types.d.ts CHANGED
@@ -67,10 +67,10 @@ export interface SavedSearch {
67
67
  export interface HistoryEntry {
68
68
  /** The query string from the history entry. */
69
69
  query: string;
70
- /** Unix timestamp (ms) of when the query was executed. Used for ordering. */
71
- timestamp?: number;
72
70
  /** Optional label for display in the autocomplete dropdown. Falls back to `query`. */
73
71
  label?: string;
72
+ /** Optional description shown below the label (e.g. date, category). Rendered as-is. */
73
+ description?: React.ReactNode;
74
74
  }
75
75
  /** An item returned by the async `fetchSuggestions` callback for field value autocomplete. */
76
76
  export interface SuggestionItem {
@@ -241,6 +241,10 @@ export interface DropdownConfig {
241
241
  * immediate. If the user types before the delay elapses, the timer is cancelled.
242
242
  * Ignored when `onNavigation` is false. @default 0 */
243
243
  navigationDelay?: number;
244
+ /** Delay in ms before showing the "Searching..." spinner on first entry into an
245
+ * async field. If the fetch resolves before the delay, the spinner never appears.
246
+ * Subsequent keystrokes preserve previous results regardless of this setting. @default 0 */
247
+ loadingDelay?: number;
244
248
  /** Custom renderer for field value hints. Return a React element for rich content,
245
249
  * or null/undefined for the default hint. */
246
250
  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.1",
3
+ "version": "0.3.3",
4
4
  "description": "Syntax-aware smart autocomplete input for Elastic query syntax",
5
5
  "license": "MIT",
6
6
  "repository": {