@placeholderco/placeholder-ui 3.0.0 → 3.0.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.
@@ -242,6 +242,7 @@
242
242
 
243
243
  function onFilterChange(filterValue: string) {
244
244
  if (filterValue.length === 0) {
245
+ core.cancelSearch();
245
246
  core.filterLocal('');
246
247
  open = true;
247
248
  return;
@@ -250,6 +251,7 @@
250
251
  if (searchFunction && filterValue) {
251
252
  core.search(filterValue, () => (open = true));
252
253
  } else {
254
+ core.cancelSearch();
253
255
  core.filterLocal(filterValue);
254
256
  open = true;
255
257
  }
@@ -299,6 +299,7 @@
299
299
  if (searchFunction && text) {
300
300
  core.search(text, () => (open = true));
301
301
  } else {
302
+ core.cancelSearch();
302
303
  core.filterLocal(text);
303
304
  open = true;
304
305
  }
@@ -170,7 +170,14 @@
170
170
 
171
171
  function focus() {
172
172
  open = true;
173
- tick().then(() => textboxElement?.focus());
173
+ tick().then(focusTextbox);
174
+ }
175
+
176
+ // Select any leftover filter text on reopen so typing replaces it
177
+ function focusTextbox() {
178
+ const input = textboxElement as HTMLInputElement | undefined;
179
+ input?.focus();
180
+ input?.select();
174
181
  }
175
182
 
176
183
  $effect(() => {
@@ -205,6 +212,7 @@
205
212
  if (searchFunction && filterValue) {
206
213
  core.search(filterValue, () => (open = true));
207
214
  } else {
215
+ core.cancelSearch();
208
216
  core.filterLocal(filterValue);
209
217
  }
210
218
  }
@@ -319,7 +327,7 @@
319
327
  onclick={() => {
320
328
  open = !open;
321
329
  if (open && computedAllowSearch) {
322
- tick().then(() => textboxElement?.focus());
330
+ tick().then(focusTextbox);
323
331
  }
324
332
  }}
325
333
  onkeydown={(e) => onKeyDown(e)}
@@ -181,7 +181,14 @@
181
181
 
182
182
  function focus() {
183
183
  open = true;
184
- tick().then(() => textboxElement?.focus());
184
+ tick().then(focusTextbox);
185
+ }
186
+
187
+ // Select any leftover filter text on reopen so typing replaces it
188
+ function focusTextbox() {
189
+ const input = textboxElement as HTMLInputElement | undefined;
190
+ input?.focus();
191
+ input?.select();
185
192
  }
186
193
 
187
194
  $effect(() => {
@@ -240,6 +247,7 @@
240
247
  hideCombobox = false;
241
248
  });
242
249
  } else {
250
+ core.cancelSearch();
243
251
  core.filterLocal(value);
244
252
  hideCombobox = false;
245
253
  }
@@ -294,7 +302,7 @@
294
302
  onclick={() => {
295
303
  open = !open;
296
304
  if (open && computedAllowSearch) {
297
- tick().then(() => textboxElement?.focus());
305
+ tick().then(focusTextbox);
298
306
  }
299
307
  }}
300
308
  onkeydown={(e) => onKeyDown(e)}
@@ -82,8 +82,14 @@ export declare abstract class ComboBoxDataCore<TConfig extends ComboBoxDataConfi
82
82
  abstract preloadValues(): boolean;
83
83
  convertOptions(options: ComboBoxItem[]): void;
84
84
  convertGroupOptions(groups: ComboBoxGroup[]): void;
85
- /** Debounced remote search via searchFunction. */
85
+ /**
86
+ * Debounced remote search via searchFunction. Each call supersedes the previous one:
87
+ * a pending debounce is dropped and an in-flight request is aborted, so only the
88
+ * latest query's results are ever applied.
89
+ */
86
90
  search(filterValue: string, onComplete?: () => void): void;
91
+ /** Drop any pending or in-flight remote search, e.g. when the input switches to local filtering. */
92
+ cancelSearch(): void;
87
93
  /** Case-insensitive local filter over already-loaded options. */
88
94
  filterLocal(filterValue: string): void;
89
95
  }
@@ -21,6 +21,7 @@ export class ComboBoxDataCore {
21
21
  searching = $state(false);
22
22
  config;
23
23
  #timeout;
24
+ #searchController;
24
25
  #fetchInitialized = false;
25
26
  #lastOptionsRef;
26
27
  #lastGroupedRef;
@@ -62,7 +63,7 @@ export class ComboBoxDataCore {
62
63
  this.preloadValues();
63
64
  cfg.onOptionsChanged?.();
64
65
  });
65
- $effect(() => () => clearTimeout(this.#timeout));
66
+ $effect(() => () => this.#abortPendingSearch());
66
67
  }
67
68
  convertOptions(options) {
68
69
  const items = options.map((option) => ({ ...option, selected: this.isSelected(option) }));
@@ -80,18 +81,44 @@ export class ComboBoxDataCore {
80
81
  }));
81
82
  this.filteredGroups = [...this.allGroups];
82
83
  }
83
- /** Debounced remote search via searchFunction. */
84
+ /**
85
+ * Debounced remote search via searchFunction. Each call supersedes the previous one:
86
+ * a pending debounce is dropped and an in-flight request is aborted, so only the
87
+ * latest query's results are ever applied.
88
+ */
84
89
  search(filterValue, onComplete) {
85
- clearTimeout(this.#timeout);
90
+ this.#abortPendingSearch();
86
91
  this.searching = true;
87
92
  this.#timeout = setTimeout(() => {
88
- this.config.getSearchFunction()(filterValue).then((response) => {
93
+ const controller = new AbortController();
94
+ this.#searchController = controller;
95
+ this.config.getSearchFunction()(filterValue, controller.signal).then((response) => {
96
+ if (controller.signal.aborted)
97
+ return;
98
+ this.#searchController = undefined;
89
99
  this.convertOptions(response.object ?? []);
90
100
  this.searching = false;
91
101
  onComplete?.();
102
+ }, (error) => {
103
+ // Aborted requests reject by design (e.g. fetch's AbortError) — swallow those.
104
+ if (controller.signal.aborted)
105
+ return;
106
+ this.#searchController = undefined;
107
+ this.searching = false;
108
+ throw error;
92
109
  });
93
110
  }, 300);
94
111
  }
112
+ /** Drop any pending or in-flight remote search, e.g. when the input switches to local filtering. */
113
+ cancelSearch() {
114
+ this.#abortPendingSearch();
115
+ this.searching = false;
116
+ }
117
+ #abortPendingSearch() {
118
+ clearTimeout(this.#timeout);
119
+ this.#searchController?.abort();
120
+ this.#searchController = undefined;
121
+ }
95
122
  /** Case-insensitive local filter over already-loaded options. */
96
123
  filterLocal(filterValue) {
97
124
  if (filterValue.length === 0) {
@@ -23,5 +23,10 @@ export interface ComboBoxItem {
23
23
  isAddNew?: boolean;
24
24
  }
25
25
  export type FetchFunctionType = () => Promise<NotifyModel<ComboBoxItem[]>>;
26
- export type SearchFunctionType = (query: string) => Promise<NotifyModel<ComboBoxItem[]>>;
26
+ /**
27
+ * Remote search. `signal` is aborted when a newer search supersedes this one (the user
28
+ * kept typing, cleared the input, or the component unmounted) — pass it to `fetch` to
29
+ * cancel the request. Results from superseded searches are discarded either way.
30
+ */
31
+ export type SearchFunctionType = (query: string, signal?: AbortSignal) => Promise<NotifyModel<ComboBoxItem[]>>;
27
32
  export type RetrieveLabelFunctionType = (value: any) => Promise<NotifyModel<string | undefined>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@placeholderco/placeholder-ui",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "A modern, customizable Svelte 5 UI component library with comprehensive theming support. Re-brand it with a single config object.",
5
5
  "license": "MIT",
6
6
  "author": "Placeholderco",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@sveltejs/adapter-static": "^4.0.0",
56
- "@sveltejs/kit": "^3.0.0",
56
+ "@sveltejs/kit": "^3.0.1",
57
57
  "@sveltejs/package": "^3.0.0",
58
58
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
59
59
  "@types/node": "^26.6.4",
@@ -62,10 +62,10 @@
62
62
  "prettier-plugin-svelte": "^4.1.1",
63
63
  "publint": "^0.3.25",
64
64
  "rimraf": "^6.1.3",
65
- "svelte": "^5.57.1",
65
+ "svelte": "^5.57.2",
66
66
  "svelte-check": "^4.7.6",
67
67
  "typescript": "^6.0.3",
68
- "vite": "^8.3.2"
68
+ "vite": "^8.3.3"
69
69
  },
70
70
  "dependencies": {
71
71
  "@codemirror/autocomplete": "^6.20.3",
@@ -76,13 +76,13 @@
76
76
  "@codemirror/lang-json": "^6.0.2",
77
77
  "@codemirror/lang-markdown": "^6.5.2",
78
78
  "@codemirror/lang-sql": "^6.10.0",
79
- "@codemirror/language": "^6.12.4",
80
- "@codemirror/legacy-modes": "^6.5.4",
79
+ "@codemirror/language": "^6.13.1",
80
+ "@codemirror/legacy-modes": "^6.5.5",
81
81
  "@codemirror/lint": "^6.9.7",
82
82
  "@codemirror/search": "^6.7.2",
83
83
  "@codemirror/state": "^6.7.6",
84
84
  "@codemirror/theme-one-dark": "^6.1.3",
85
- "@codemirror/view": "^6.43.13",
85
+ "@codemirror/view": "^6.43.14",
86
86
  "@lezer/highlight": "^1.2.5",
87
87
  "@tiptap/core": "^3.31.4",
88
88
  "@tiptap/extension-bubble-menu": "^3.31.4",