@lekoala/combobox 0.1.0

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +566 -0
  3. package/custom-elements.json +247 -0
  4. package/dist/combobox.css +447 -0
  5. package/dist/combobox.js +2220 -0
  6. package/dist/combobox.min.css +1 -0
  7. package/dist/combobox.min.js +2 -0
  8. package/dist/types/combo-box.d.ts +112 -0
  9. package/dist/types/combo-box.d.ts.map +1 -0
  10. package/dist/types/combobox.d.ts +530 -0
  11. package/dist/types/combobox.d.ts.map +1 -0
  12. package/dist/types/define.d.ts +2 -0
  13. package/dist/types/define.d.ts.map +1 -0
  14. package/dist/types/helpers.d.ts +251 -0
  15. package/dist/types/helpers.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +24 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/locales/de.d.ts +7 -0
  19. package/dist/types/locales/de.d.ts.map +1 -0
  20. package/dist/types/locales/en.d.ts +8 -0
  21. package/dist/types/locales/en.d.ts.map +1 -0
  22. package/dist/types/locales/es.d.ts +7 -0
  23. package/dist/types/locales/es.d.ts.map +1 -0
  24. package/dist/types/locales/fr.d.ts +7 -0
  25. package/dist/types/locales/fr.d.ts.map +1 -0
  26. package/dist/types/locales/it.d.ts +7 -0
  27. package/dist/types/locales/it.d.ts.map +1 -0
  28. package/dist/types/locales/nl.d.ts +7 -0
  29. package/dist/types/locales/nl.d.ts.map +1 -0
  30. package/dist/types/locales/pt.d.ts +7 -0
  31. package/dist/types/locales/pt.d.ts.map +1 -0
  32. package/dist/types/locales/ru.d.ts +7 -0
  33. package/dist/types/locales/ru.d.ts.map +1 -0
  34. package/dist/types/locales/zh-CN.d.ts +7 -0
  35. package/dist/types/locales/zh-CN.d.ts.map +1 -0
  36. package/dist/types/messages.d.ts +48 -0
  37. package/dist/types/messages.d.ts.map +1 -0
  38. package/package.json +92 -0
  39. package/src/combo-box.js +316 -0
  40. package/src/combobox.css +447 -0
  41. package/src/combobox.js +2975 -0
  42. package/src/define.js +20 -0
  43. package/src/helpers.js +377 -0
  44. package/src/index.js +35 -0
  45. package/src/locales/de.js +17 -0
  46. package/src/locales/en.js +18 -0
  47. package/src/locales/es.js +17 -0
  48. package/src/locales/fr.js +17 -0
  49. package/src/locales/it.js +17 -0
  50. package/src/locales/nl.js +17 -0
  51. package/src/locales/pt.js +17 -0
  52. package/src/locales/ru.js +17 -0
  53. package/src/locales/zh-CN.js +17 -0
  54. package/src/messages.js +55 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LeKoala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,566 @@
1
+ # @lekoala/combobox
2
+
3
+ A small, native-first combobox library for searchable selects, multiple values, tags and remote suggestions.
4
+
5
+ It enhances regular `<input>`, `<datalist>` and `<select>` controls instead of replacing them with a custom form model.
6
+
7
+ ```html
8
+ <script type="module">
9
+ import "@lekoala/combobox/define";
10
+ </script>
11
+
12
+ <combo-box create placeholder="Search or create a framework…">
13
+ <select name="frameworks[]" multiple>
14
+ <option value="react">React</option>
15
+ <option value="vue">Vue</option>
16
+ </select>
17
+ </combo-box>
18
+ ```
19
+
20
+ The library uses the browser where it can:
21
+
22
+ * native form controls keep owning the value;
23
+ * Popover handles the picker top layer;
24
+ * CSS Anchor Positioning handles placement;
25
+ * ARIA combobox/listbox semantics handle keyboard interaction;
26
+ * native `input`, `change`, validation and form reset keep working.
27
+
28
+ There is no JavaScript positioning engine, no Bootstrap dependency and no global `window.*` API.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install @lekoala/combobox
34
+ ```
35
+
36
+ or:
37
+
38
+ ```bash
39
+ bun add @lekoala/combobox
40
+ ```
41
+
42
+ ## Three ways to use it
43
+
44
+ ### `<combo-box>`
45
+
46
+ This is the simplest option for most applications.
47
+
48
+ ```html
49
+ <script type="module">
50
+ import "@lekoala/combobox/define";
51
+ </script>
52
+
53
+ <combo-box search="fuzzy" placeholder="Choose a country…">
54
+ <select name="country">
55
+ <option value="">Choose…</option>
56
+ <option value="be">Belgium</option>
57
+ <option value="fr">France</option>
58
+ </select>
59
+ </combo-box>
60
+ ```
61
+
62
+ Importing `@lekoala/combobox/define` registers `<combo-box>`.
63
+
64
+ Importing the main package does **not** register anything automatically.
65
+
66
+ ### JavaScript
67
+
68
+ You can enhance a native control directly:
69
+
70
+ ```js
71
+ import Combobox from "@lekoala/combobox";
72
+
73
+ const combo = new Combobox(document.querySelector("select"), {
74
+ search: "fuzzy",
75
+ minChars: 1,
76
+ });
77
+ ```
78
+
79
+ ### Classic script / `file://`
80
+
81
+ A standalone build is also available:
82
+
83
+ ```html
84
+ <script src="dist/combobox.js"></script>
85
+
86
+ <combo-box>
87
+ <select>
88
+ ...
89
+ </select>
90
+ </combo-box>
91
+ ```
92
+
93
+ The classic build registers `<combo-box>`, but still does not expose a global `Combobox` object.
94
+
95
+ ## Native controls stay native
96
+
97
+ The original control remains the source of truth.
98
+
99
+ For an input:
100
+
101
+ ```text
102
+ <input list="cities">
103
+
104
+ └── owns the submitted value
105
+ ```
106
+
107
+ For selects:
108
+
109
+ ```text
110
+ <select>
111
+ <select multiple>
112
+
113
+ └── own the submitted value(s)
114
+ ```
115
+
116
+ The searchable input added around a `<select>` is only there for interaction. It has no `name` and never replaces the select in `FormData`.
117
+
118
+ This also means things such as:
119
+
120
+ * `required`;
121
+ * `disabled`;
122
+ * `form.reset()`;
123
+ * native `input` and `change`;
124
+ * server-rendered selections;
125
+
126
+ continue to behave like normal form controls.
127
+
128
+ ## Searchable selects
129
+
130
+ A regular select can be filtered without changing its value model:
131
+
132
+ ```html
133
+ <combo-box search="includes">
134
+ <select name="doctor">
135
+ <option value="1">Dr Jane Smith</option>
136
+ <option value="2">Dr John Martin</option>
137
+ </select>
138
+ </combo-box>
139
+ ```
140
+
141
+ You can also provide your own interaction input:
142
+
143
+ ```html
144
+ <input data-filter-for="doctor" placeholder="Search doctors…">
145
+
146
+ <select id="doctor" name="doctor">
147
+ ...
148
+ </select>
149
+ ```
150
+
151
+ The input is only used for filtering. The select still owns the value.
152
+
153
+ ## Multiple values and tags
154
+
155
+ Multiple selects are rendered as removable chips:
156
+
157
+ ```html
158
+ <combo-box>
159
+ <select name="specialties[]" multiple>
160
+ <option value="cardiology">Cardiology</option>
161
+ <option value="neurology">Neurology</option>
162
+ </select>
163
+ </combo-box>
164
+ ```
165
+
166
+ Enable creation when users may enter new values:
167
+
168
+ ```html
169
+ <combo-box create>
170
+ <select name="tags[]" multiple></select>
171
+ </combo-box>
172
+ ```
173
+
174
+ Created options are added to the native `<select>` just like normal options.
175
+
176
+ ## Matching
177
+
178
+ Built-in search modes are:
179
+
180
+ ```text
181
+ includes
182
+ startswith
183
+ fuzzy
184
+ pattern
185
+ ```
186
+
187
+ Search can cover several fields:
188
+
189
+ ```html
190
+ <combo-box
191
+ search="fuzzy"
192
+ search-fields="label city specialty"
193
+ >
194
+ ...
195
+ </combo-box>
196
+ ```
197
+
198
+ Each field is matched independently. Search never matches by accidentally joining fields together.
199
+
200
+ Matching is case- and accent-friendly where appropriate, so values such as:
201
+
202
+ ```text
203
+ Liège
204
+ liege
205
+ LIEGE
206
+ liège
207
+ ```
208
+
209
+ behave as expected.
210
+
211
+ More specialized matching can be provided from JavaScript.
212
+
213
+ ## Remote results
214
+
215
+ Remote search stays deliberately simple:
216
+
217
+ ```js
218
+ combo.configure({
219
+ minChars: 2,
220
+
221
+ async load(query, { signal }) {
222
+ const response = await fetch(`/api/patients?q=${encodeURIComponent(query)}`, {
223
+ signal,
224
+ });
225
+
226
+ return response.json();
227
+ },
228
+ });
229
+ ```
230
+
231
+ Remote results are **temporary suggestions**. They do not immediately become native `<option>` elements.
232
+
233
+ Once a remote result is selected, it is added to the select so normal form submission continues to work.
234
+
235
+ That distinction is intentional:
236
+
237
+ ```text
238
+ catalogue persistent native options
239
+ results temporary search results
240
+ selection native selected options
241
+ ```
242
+
243
+ `setResults()` and `clearResults()` only deal with temporary results.
244
+
245
+ `setOptions()` replaces the catalogue while keeping currently selected native options, including selected values that originally came from remote results or creation.
246
+
247
+ ## Empty values
248
+
249
+ Empty option values are supported when explicitly enabled:
250
+
251
+ ```html
252
+ <combo-box allow-empty-option>
253
+ <select>
254
+ <option value="">None</option>
255
+ <option value="a">Option A</option>
256
+ </select>
257
+ </combo-box>
258
+ ```
259
+
260
+ Without `allow-empty-option`, `""` is not treated as a normal selectable item when options are added programmatically.
261
+
262
+ ## JavaScript-only behavior
263
+
264
+ Simple options have HTML attributes where that makes sense:
265
+
266
+ ```html
267
+ <combo-box
268
+ create
269
+ search="fuzzy"
270
+ min-chars="2"
271
+ max-items="5"
272
+ max-options="20"
273
+ tab-select
274
+ >
275
+ ```
276
+
277
+ Behavior that requires functions stays in JavaScript:
278
+
279
+ ```js
280
+ element.configure({
281
+ async load(query, context) {
282
+ // ...
283
+ },
284
+
285
+ guards: {
286
+ async remove(item) {
287
+ return confirm(`Remove ${item.label}?`);
288
+ },
289
+ },
290
+
291
+ render: {
292
+ option(item) {
293
+ const strong = document.createElement("strong");
294
+ strong.textContent = item.label;
295
+ return strong;
296
+ },
297
+ },
298
+ });
299
+ ```
300
+
301
+ Strings are always rendered as text. Rich rendering uses DOM nodes rather than an `allowHtml` switch.
302
+
303
+ See [API](docs/API.md) for the full option and method reference.
304
+
305
+ ## Filtering events
306
+
307
+ Filtering can be intercepted:
308
+
309
+ ```js
310
+ combo.input.addEventListener("beforefilter", (event) => {
311
+ if (somethingSpecial) {
312
+ event.preventDefault();
313
+
314
+ // Application-defined behavior...
315
+ }
316
+ });
317
+ ```
318
+
319
+ `beforefilter` is cancellable and exposes the current query.
320
+
321
+ Filtering events belong to the interaction input. `combobox:*` lifecycle events belong to the native source control.
322
+
323
+ The full event table is documented in [API](docs/API.md).
324
+
325
+ ## Selection order
326
+
327
+ For multiple selects, source order and selection order do not have to mean the same thing.
328
+
329
+ When explicit selection order is enabled, values can be reordered with:
330
+
331
+ ```js
332
+ combo.move(fromIndex, toIndex);
333
+ ```
334
+
335
+ Drag-and-drop is intentionally not built into the core. An application can add whatever UI it wants and call `move()`.
336
+
337
+ ## Progressive fallback
338
+
339
+ If the browser does not support the Popover and CSS Anchor features needed by the enhanced picker, the original controls remain usable.
340
+
341
+ * `input + datalist` stays a native datalist;
342
+ * `select` stays a native select;
343
+ * `select multiple` stays a native multiple select;
344
+ * creatable multiple selects get a small native Add input/button.
345
+
346
+ There is no second JavaScript picker implementation for older browsers.
347
+
348
+ You can force this mode in the demo with:
349
+
350
+ ```text
351
+ ?native=1
352
+ ```
353
+
354
+ ## Styling
355
+
356
+ The component ships with a small default stylesheet and is designed to be easy to theme with CSS custom properties.
357
+
358
+ For example:
359
+
360
+ ```css
361
+ combo-box.compact {
362
+ --cb-chip-font-size: 0.75em;
363
+ }
364
+
365
+ combo-box.pills {
366
+ --cb-chip-border-radius: 999px;
367
+ }
368
+
369
+ combo-box.solid {
370
+ --cb-chip-bg: #6d28d9;
371
+ --cb-chip-color: white;
372
+ }
373
+ ```
374
+
375
+ Applications can also return marker elements from renderers and style them with normal CSS:
376
+
377
+ ```js
378
+ render: {
379
+ item(item) {
380
+ const label = document.createElement("span");
381
+ label.className = `tag-tone-${item.data.tone}`;
382
+ label.textContent = item.label;
383
+ return label;
384
+ },
385
+ }
386
+ ```
387
+
388
+ ```css
389
+ .cb-chip:has(.tag-tone-success) {
390
+ --cb-chip-bg: #dcfce7;
391
+ --cb-chip-color: #15803d;
392
+ }
393
+ ```
394
+
395
+ `demo/actual-css.html` shows the same component themed entirely with [Actual CSS](https://github.com/lekoala/actual-css) tokens.
396
+
397
+ ## Demo
398
+
399
+ The main demo covers:
400
+
401
+ * input + datalist;
402
+ * searchable single selects;
403
+ * multiple values and chips;
404
+ * created values;
405
+ * fuzzy and multi-field search;
406
+ * remote loading;
407
+ * custom renderers;
408
+ * selection order;
409
+ * guards;
410
+ * separators;
411
+ * maximum items/results;
412
+ * RTL;
413
+ * runtime disabled states;
414
+ * form reset;
415
+ * custom clear controls.
416
+
417
+ Run it locally with:
418
+
419
+ ```bash
420
+ bun install
421
+ bun run dev
422
+ ```
423
+
424
+ Then open:
425
+
426
+ ```text
427
+ http://127.0.0.1:4173/
428
+ ```
429
+
430
+ The demo uses the generated distribution build, not a special development-only version.
431
+
432
+ ## What is in 0.1
433
+
434
+ The main 0.1 API covers:
435
+
436
+ * input + datalist;
437
+ * single and multiple selects;
438
+ * `<combo-box>`;
439
+ * filtering and matching;
440
+ * multiple search fields;
441
+ * creation;
442
+ * remote loading;
443
+ * chips;
444
+ * separators;
445
+ * `maxItems` and `maxOptions`;
446
+ * selection order and `move()`;
447
+ * `clear()`;
448
+ * form semantics;
449
+ * exact cleanup with `dispose()`.
450
+
451
+ A few more advanced APIs are intentionally still experimental in 0.x:
452
+
453
+ * `observeSource`;
454
+ * custom `tokenize`;
455
+ * cursor pagination / `loadMore()`;
456
+ * rich `render.*` customization.
457
+
458
+ Things that are deliberately **not** part of the library:
459
+
460
+ * a plugin framework;
461
+ * virtualization;
462
+ * built-in drag/drop;
463
+ * checkbox dropdowns;
464
+ * Bootstrap JavaScript;
465
+ * automatic DOM observation by default;
466
+ * a built-in clear button;
467
+ * a JavaScript positioning fallback.
468
+
469
+ The goal is not to become another all-purpose Select2 clone. The library should stay small enough that native controls and browser APIs remain visible underneath it.
470
+
471
+ ## Custom element registration
472
+
473
+ Registration is explicit:
474
+
475
+ ```js
476
+ import { defineCombobox } from "@lekoala/combobox";
477
+
478
+ defineCombobox();
479
+ ```
480
+
481
+ Calling `defineCombobox()` more than once is safe.
482
+
483
+ You can also build your own element name:
484
+
485
+ ```js
486
+ import { ComboBoxElement } from "@lekoala/combobox";
487
+
488
+ customElements.define(
489
+ "app-combobox",
490
+ class extends ComboBoxElement {},
491
+ );
492
+ ```
493
+
494
+ `<combo-box>` uses no Shadow DOM and does not become the form control itself.
495
+
496
+ ## Development
497
+
498
+ The source is pure ESM and has no runtime dependencies.
499
+
500
+ ```bash
501
+ bun install
502
+ bunx playwright install chromium firefox webkit
503
+
504
+ bun run check
505
+ bun run test:browser
506
+ bun run sync
507
+ bun run verify
508
+ ```
509
+
510
+ A few useful commands:
511
+
512
+ ```text
513
+ bun run check
514
+ lint + typecheck + unit tests
515
+
516
+ bun run test:browser
517
+ browser behavior tests against the ESM source
518
+
519
+ bun run sync
520
+ regenerate dist JS/CSS, declarations and custom-elements.json
521
+
522
+ bun run verify
523
+ run the full consistency/package checks
524
+
525
+ bun run check:all
526
+ bun run test:browser:all
527
+ include Firefox and WebKit
528
+ ```
529
+
530
+ Generated distribution files are committed so the demo, package contents and published artifacts can be checked directly.
531
+
532
+ The package ships:
533
+
534
+ * pure ESM entry points;
535
+ * an opt-in `<combo-box>` registration entry;
536
+ * a classic self-registering build;
537
+ * CSS;
538
+ * generated TypeScript declarations;
539
+ * `custom-elements.json`.
540
+
541
+ There are no runtime source maps in 0.1. Declaration maps are kept for TypeScript editor navigation.
542
+
543
+ ## Documentation
544
+
545
+ More detail lives here:
546
+
547
+ * [API](docs/API.md) — options, methods, attributes and events
548
+ * [Architecture](docs/ARCHITECTURE.md) — internal model and design decisions
549
+ * [Use cases](docs/USE_CASES.md) — practical examples
550
+ * [Migration](docs/MIGRATION.md) — moving from `bootstrap5-tags` / `bootstrap5-autocomplete`
551
+ * [Testing](docs/TESTING.md) — browser and behavior coverage
552
+ * [References](docs/REFERENCES.md) — related browser and Open UI work
553
+
554
+ ## Design principles
555
+
556
+ A few rules keep the library intentionally small:
557
+
558
+ 1. The native control owns the value.
559
+ 2. Remote results stay temporary until selected.
560
+ 3. Option identity comes from the actual `<option>`, not only its string value.
561
+ 4. Form behavior should remain native whenever possible.
562
+ 5. The browser handles placement and top-layer behavior.
563
+ 6. Rich rendering uses DOM nodes, not HTML strings.
564
+ 7. Application-specific transport and UI stay application-specific.
565
+
566
+ That is most of the design.