@visitwonders/assembly 0.10.1 → 0.11.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 (44) hide show
  1. package/declarations/form/combobox-field.d.ts +71 -0
  2. package/declarations/form/combobox-field.d.ts.map +1 -0
  3. package/declarations/form/combobox-shared.d.ts +36 -0
  4. package/declarations/form/combobox-shared.d.ts.map +1 -0
  5. package/declarations/form/combobox.d.ts +239 -0
  6. package/declarations/form/combobox.d.ts.map +1 -0
  7. package/declarations/form/index.d.ts +4 -0
  8. package/declarations/form/index.d.ts.map +1 -1
  9. package/declarations/form/multi-combobox-field.d.ts +72 -0
  10. package/declarations/form/multi-combobox-field.d.ts.map +1 -0
  11. package/declarations/form/multi-combobox.d.ts +202 -0
  12. package/declarations/form/multi-combobox.d.ts.map +1 -0
  13. package/declarations/layout/h-stack.d.ts.map +1 -1
  14. package/declarations/layout/stack.d.ts.map +1 -1
  15. package/declarations/layout/v-stack.d.ts.map +1 -1
  16. package/declarations/overlay/popover.d.ts +20 -1
  17. package/declarations/overlay/popover.d.ts.map +1 -1
  18. package/dist/_app_/form/combobox-field.js +1 -0
  19. package/dist/_app_/form/combobox-shared.js +1 -0
  20. package/dist/_app_/form/combobox.js +1 -0
  21. package/dist/_app_/form/multi-combobox-field.js +1 -0
  22. package/dist/_app_/form/multi-combobox.js +1 -0
  23. package/dist/data/{sortable-list-css-211fcfeedc08052ccbac7f51549ce0b1.css → sortable-list-css-03e5d237ea377f7d6056e76cc85b2aaa.css} +8 -4
  24. package/dist/data/sortable-list.js +1 -1
  25. package/dist/form/combobox-field.js +37 -0
  26. package/dist/form/combobox-field.js.map +1 -0
  27. package/dist/form/combobox-shared.js +76 -0
  28. package/dist/form/combobox-shared.js.map +1 -0
  29. package/dist/form/combobox.css +345 -0
  30. package/dist/form/combobox.js +612 -0
  31. package/dist/form/combobox.js.map +1 -0
  32. package/dist/form/{display-field-css-890d9be4b5da61613fd017071f330735.css → display-field-css-502236a2343d47e31e52bdb93a769ca1.css} +2 -2
  33. package/dist/form/display-field.js +1 -1
  34. package/dist/form/index.js +4 -0
  35. package/dist/form/index.js.map +1 -1
  36. package/dist/form/multi-combobox-field.js +36 -0
  37. package/dist/form/multi-combobox-field.js.map +1 -0
  38. package/dist/form/multi-combobox.css +422 -0
  39. package/dist/form/multi-combobox.js +626 -0
  40. package/dist/form/multi-combobox.js.map +1 -0
  41. package/dist/layout/h-stack.js.map +1 -1
  42. package/dist/layout/v-stack.js.map +1 -1
  43. package/dist/overlay/popover.js +19 -1
  44. package/package.json +6 -1
@@ -0,0 +1,612 @@
1
+ import "./combobox.css"
2
+ import Component from '@glimmer/component';
3
+ import { tracked, cached } from '@glimmer/tracking';
4
+ import { on } from '@ember/modifier';
5
+ import { fn } from '@ember/helper';
6
+ import { modifier } from 'ember-modifier';
7
+ import { uniqueId } from 'ember-primitives/utils';
8
+ import Popover from '../overlay/popover.js';
9
+ import Icon from '../media/icon.js';
10
+ import { ChevronDown, X } from 'lucide';
11
+ import { isOptionGroup, flattenOptions, splitOnMatch } from './combobox-shared.js';
12
+ import { precompileTemplate } from '@ember/template-compilation';
13
+ import { setComponentTemplate } from '@ember/component';
14
+ import { g, i, n } from 'decorator-transforms/runtime';
15
+
16
+ ;
17
+
18
+ /** Rows skipped per PageUp / PageDown keypress. Matches the WAI-ARIA
19
+ * listbox pattern's "move by page" convention. */
20
+ const PAGE_STEP = 10;
21
+
22
+ // ============================================================================
23
+ // Main Component
24
+ // ============================================================================
25
+
26
+ class ComboBox extends Component {
27
+ static {
28
+ g(this.prototype, "isOpen", [tracked], function () {
29
+ return false;
30
+ });
31
+ }
32
+ #isOpen = (i(this, "isOpen"), void 0);
33
+ static {
34
+ g(this.prototype, "query", [tracked], function () {
35
+ return '';
36
+ });
37
+ }
38
+ #query = (i(this, "query"), void 0);
39
+ static {
40
+ g(this.prototype, "activeId", [tracked], function () {
41
+ return null;
42
+ });
43
+ }
44
+ #activeId = (i(this, "activeId"), void 0);
45
+ static {
46
+ g(this.prototype, "remoteItems", [tracked], function () {
47
+ return null;
48
+ });
49
+ }
50
+ #remoteItems = (i(this, "remoteItems"), void 0); // Async-mode state. Only used when @onSearch is provided.
51
+ static {
52
+ g(this.prototype, "isLoadingInternal", [tracked], function () {
53
+ return false;
54
+ });
55
+ }
56
+ #isLoadingInternal = (i(this, "isLoadingInternal"), void 0);
57
+ searchSeq = 0;
58
+ searchTimer = null;
59
+ inputElement = null;
60
+ listboxElement = null;
61
+ componentId = `combobox-${uniqueId()}`;
62
+ listboxId = `${this.componentId}-listbox`;
63
+ createOptionId = `${this.componentId}-create`;
64
+ willDestroy() {
65
+ super.willDestroy();
66
+ if (this.searchTimer) clearTimeout(this.searchTimer);
67
+ }
68
+ // --------------------------------------------------------------------------
69
+ // Items + filtering
70
+ // --------------------------------------------------------------------------
71
+ get isAsync() {
72
+ return this.args.onSearch !== undefined;
73
+ }
74
+ /**
75
+ * Source of truth for what's available to render. In async mode, we use
76
+ * the latest remote payload once it's resolved; before that, we fall
77
+ * back to `@items` so the dropdown isn't empty on first open.
78
+ */
79
+ get effectiveItems() {
80
+ if (this.isAsync && this.remoteItems !== null) {
81
+ return this.remoteItems;
82
+ }
83
+ return this.args.items ?? [];
84
+ }
85
+ get normalizedGroups() {
86
+ const items = this.effectiveItems;
87
+ if (items.length === 0) return [];
88
+ if (isOptionGroup(items[0])) {
89
+ return items.map((g, i) => ({
90
+ label: g.label,
91
+ headingId: `${this.componentId}-group-${i}`,
92
+ options: g.options
93
+ }));
94
+ }
95
+ return [{
96
+ label: null,
97
+ headingId: null,
98
+ options: items
99
+ }];
100
+ }
101
+ /**
102
+ * Per-group filtered output for rendering. In async mode we trust the
103
+ * server response and skip the local substring filter; otherwise we
104
+ * apply case-insensitive substring on `label` + `description`.
105
+ * Empty groups (no matches) are dropped so we don't render bare
106
+ * headings.
107
+ */
108
+ get renderedGroups() {
109
+ const trimmed = this.query.trim().toLowerCase();
110
+ const skipLocalFilter = this.isAsync || !trimmed;
111
+ const result = [];
112
+ for (const group of this.normalizedGroups) {
113
+ const matched = skipLocalFilter ? group.options : group.options.filter(option => {
114
+ const inLabel = option.label.toLowerCase().includes(trimmed);
115
+ const inDescription = option.description ? option.description.toLowerCase().includes(trimmed) : false;
116
+ return inLabel || inDescription;
117
+ });
118
+ if (matched.length === 0) continue;
119
+ const rows = matched.map(option => {
120
+ const optionId = `${this.componentId}-option-${option.value}`;
121
+ return {
122
+ kind: 'option',
123
+ option,
124
+ optionId,
125
+ isActive: this.activeId === optionId,
126
+ isSelected: this.args.value === option.value
127
+ };
128
+ });
129
+ result.push({
130
+ key: group.headingId ?? 'ungrouped',
131
+ label: group.label,
132
+ headingId: group.headingId,
133
+ rows
134
+ });
135
+ }
136
+ return result;
137
+ }
138
+ /**
139
+ * Flat list of option rows used for keyboard navigation. Excludes
140
+ * disabled options and group headings (headings aren't selectable).
141
+ * The create row, if visible, is appended at the end.
142
+ */
143
+ static {
144
+ n(this.prototype, "renderedGroups", [cached]);
145
+ }
146
+ get navigableRows() {
147
+ const rows = [];
148
+ for (const group of this.renderedGroups) {
149
+ for (const row of group.rows) {
150
+ if (!row.option.isDisabled) rows.push(row);
151
+ }
152
+ }
153
+ const create = this.createRow;
154
+ if (create) rows.push(create);
155
+ return rows;
156
+ }
157
+ static {
158
+ n(this.prototype, "navigableRows", [cached]);
159
+ }
160
+ get hasOptionResults() {
161
+ return this.renderedGroups.some(g => g.rows.length > 0);
162
+ }
163
+ // --------------------------------------------------------------------------
164
+ // Create row
165
+ // --------------------------------------------------------------------------
166
+ get isCreateVisible() {
167
+ if (!this.args.isCreatable) return false;
168
+ const trimmed = this.query.trim();
169
+ if (!trimmed) return false;
170
+ // Suppress when the query exactly matches an existing label
171
+ // (case-insensitive). Searches across all options, even ones the
172
+ // current filter would hide — duplicate-by-label check is global.
173
+ const flat = flattenOptions(this.effectiveItems);
174
+ const lower = trimmed.toLowerCase();
175
+ return !flat.some(opt => opt.label.toLowerCase() === lower);
176
+ }
177
+ get createRow() {
178
+ if (!this.isCreateVisible) return null;
179
+ const query = this.query.trim();
180
+ const label = this.args.createLabel ? this.args.createLabel(query) : `Create "${query}"`;
181
+ return {
182
+ kind: 'create',
183
+ optionId: this.createOptionId,
184
+ isActive: this.activeId === this.createOptionId,
185
+ query,
186
+ label
187
+ };
188
+ }
189
+ // --------------------------------------------------------------------------
190
+ // Async search
191
+ // --------------------------------------------------------------------------
192
+ get isLoading() {
193
+ if (this.args.isLoading !== undefined) return this.args.isLoading;
194
+ return this.isLoadingInternal;
195
+ }
196
+ get loadingText() {
197
+ return this.args.loadingText ?? 'Loading…';
198
+ }
199
+ get searchDebounceMs() {
200
+ return this.args.searchDebounceMs ?? 200;
201
+ }
202
+ scheduleAsyncSearch(query) {
203
+ if (!this.args.onSearch) return;
204
+ if (this.searchTimer) clearTimeout(this.searchTimer);
205
+ this.isLoadingInternal = true;
206
+ this.searchTimer = setTimeout(() => {
207
+ this.searchTimer = null;
208
+ void this.runAsyncSearch(query);
209
+ }, this.searchDebounceMs);
210
+ }
211
+ async runAsyncSearch(query) {
212
+ if (!this.args.onSearch) return;
213
+ const seq = ++this.searchSeq;
214
+ try {
215
+ const result = await this.args.onSearch(query);
216
+ if (seq !== this.searchSeq) return; // stale, discard
217
+ this.remoteItems = result;
218
+ } catch (error) {
219
+ // Only surface the error for the latest request; stale errors
220
+ // are as uninteresting as stale results.
221
+ if (seq === this.searchSeq) {
222
+ this.args.onSearchError?.(error);
223
+ }
224
+ } finally {
225
+ if (seq === this.searchSeq) {
226
+ this.isLoadingInternal = false;
227
+ // Filtered list changed — re-anchor the active row.
228
+ this.resetActiveToFirstNavigable();
229
+ }
230
+ }
231
+ }
232
+ // --------------------------------------------------------------------------
233
+ // Selection display
234
+ // --------------------------------------------------------------------------
235
+ get selectedOption() {
236
+ if (this.args.value == null) return null;
237
+ const flat = flattenOptions(this.effectiveItems);
238
+ return flat.find(opt => opt.value === this.args.value) ?? null;
239
+ }
240
+ /**
241
+ * What the input renders as its `value` attribute.
242
+ *
243
+ * - When the dropdown is open, we surface the user's typed query so
244
+ * typing feels like editing a normal text field and the filter
245
+ * reflects what they see.
246
+ * - When closed, we surface the selected option's label (or empty
247
+ * string if nothing's selected) so the input acts as a read-only
248
+ * display of the current value.
249
+ *
250
+ * The placeholder argument shows when this is empty.
251
+ */
252
+ get displayValue() {
253
+ if (this.isOpen) return this.query;
254
+ return this.selectedOption?.label ?? '';
255
+ }
256
+ get hasValue() {
257
+ return this.args.value != null && this.args.value !== '';
258
+ }
259
+ get isClearable() {
260
+ return this.args.isClearable ?? true;
261
+ }
262
+ get showClearButton() {
263
+ return this.isClearable && this.hasValue && !this.args.isDisabled;
264
+ }
265
+ get noResultsText() {
266
+ return this.args.noResultsText ?? 'No results';
267
+ }
268
+ /**
269
+ * Text for the visually-hidden aria-live region. Screen-reader users
270
+ * need feedback on how their typing is shaping the result set — this
271
+ * announces counts and empty-state without showing anything visible.
272
+ *
273
+ * The region renders only when the dropdown is open so we don't
274
+ * announce stale counts after the user closes it.
275
+ */
276
+ get announcement() {
277
+ if (!this.isOpen) return '';
278
+ if (this.isLoading) return this.loadingText;
279
+ const optionCount = this.navigableRows.filter(r => r.kind === 'option').length;
280
+ if (optionCount === 0) {
281
+ if (this.isCreateVisible) return 'Create new option available';
282
+ return this.noResultsText;
283
+ }
284
+ return `${optionCount} ${optionCount === 1 ? 'option' : 'options'} available`;
285
+ }
286
+ get showEmptyState() {
287
+ if (this.isLoading) return false;
288
+ if (this.hasOptionResults) return false;
289
+ if (this.isCreateVisible) return false;
290
+ return true;
291
+ }
292
+ get ariaExpanded() {
293
+ return this.isOpen ? 'true' : 'false';
294
+ }
295
+ get ariaRequired() {
296
+ return this.args.isRequired ? 'true' : undefined;
297
+ }
298
+ get ariaInvalid() {
299
+ return this.args.isInvalid ? 'true' : undefined;
300
+ }
301
+ // --------------------------------------------------------------------------
302
+ // Element registration
303
+ // --------------------------------------------------------------------------
304
+ setupInput = modifier(element => {
305
+ this.inputElement = element;
306
+ return () => {
307
+ this.inputElement = null;
308
+ };
309
+ });
310
+ setupListbox = modifier(element => {
311
+ this.listboxElement = element;
312
+ return () => {
313
+ this.listboxElement = null;
314
+ };
315
+ });
316
+ // --------------------------------------------------------------------------
317
+ // Event handlers — input (which is also the trigger)
318
+ // --------------------------------------------------------------------------
319
+ handleInputFocus = event => {
320
+ this.args.onFocus?.(event);
321
+ };
322
+ handleInputBlur = event => {
323
+ this.args.onBlur?.(event);
324
+ };
325
+ handleInputClick = () => {
326
+ if (this.args.isDisabled) return;
327
+ if (!this.isOpen) this.openDropdown();
328
+ };
329
+ handleInput = event => {
330
+ const target = event.target;
331
+ this.query = target.value;
332
+ if (!this.isOpen) this.openDropdown({
333
+ resetActive: false
334
+ });
335
+ if (this.isAsync) {
336
+ this.scheduleAsyncSearch(this.query);
337
+ } else {
338
+ this.resetActiveToFirstNavigable();
339
+ }
340
+ };
341
+ handleInputKeyDown = event => {
342
+ if (!this.isOpen) {
343
+ // When closed, certain keys open the dropdown.
344
+ switch (event.key) {
345
+ case 'ArrowDown':
346
+ case 'ArrowUp':
347
+ event.preventDefault();
348
+ this.openDropdown();
349
+ return;
350
+ case 'Enter':
351
+ // Don't preventDefault — let form submission happen if appropriate.
352
+ return;
353
+ case 'Escape':
354
+ if (this.showClearButton) {
355
+ event.preventDefault();
356
+ this.clearSelection();
357
+ }
358
+ return;
359
+ default:
360
+ return;
361
+ }
362
+ }
363
+ // Open: full keyboard nav.
364
+ switch (event.key) {
365
+ case 'ArrowDown':
366
+ event.preventDefault();
367
+ this.moveActive(1);
368
+ break;
369
+ case 'ArrowUp':
370
+ event.preventDefault();
371
+ this.moveActive(-1);
372
+ break;
373
+ case 'PageDown':
374
+ event.preventDefault();
375
+ this.movePage(1);
376
+ break;
377
+ case 'PageUp':
378
+ event.preventDefault();
379
+ this.movePage(-1);
380
+ break;
381
+ case 'Home':
382
+ event.preventDefault();
383
+ this.setActiveToFirst();
384
+ break;
385
+ case 'End':
386
+ event.preventDefault();
387
+ this.setActiveToLast();
388
+ break;
389
+ case 'Enter':
390
+ event.preventDefault();
391
+ this.activateActive();
392
+ break;
393
+ case 'Escape':
394
+ event.preventDefault();
395
+ this.closeDropdown();
396
+ break;
397
+ case 'Tab':
398
+ // Tab is navigational. Close without committing.
399
+ this.closeDropdown();
400
+ break;
401
+ }
402
+ };
403
+ handleChevronMouseDown = event => {
404
+ // Stop the input from losing focus when the chevron is clicked.
405
+ event.preventDefault();
406
+ };
407
+ handleChevronClick = event => {
408
+ event.preventDefault();
409
+ if (this.args.isDisabled) return;
410
+ if (this.isOpen) {
411
+ this.closeDropdown();
412
+ } else {
413
+ this.openDropdown();
414
+ }
415
+ this.inputElement?.focus({
416
+ preventScroll: true
417
+ });
418
+ };
419
+ // --------------------------------------------------------------------------
420
+ // Open / close
421
+ // --------------------------------------------------------------------------
422
+ openDropdown = (opts = {}) => {
423
+ if (this.args.isDisabled || this.isOpen) return;
424
+ this.isOpen = true;
425
+ // When a user opens via click/keyboard with no query, start fresh.
426
+ // When opening because the user typed (handleInput), the query is
427
+ // already set — preserve it and just anchor active to first match.
428
+ if (opts.resetActive !== false) {
429
+ this.query = '';
430
+ this.resetActiveToInitial();
431
+ } else {
432
+ this.resetActiveToFirstNavigable();
433
+ }
434
+ this.args.onOpen?.();
435
+ };
436
+ closeDropdown = () => {
437
+ // Guard so we don't fire onClose repeatedly — closeDropdown is
438
+ // called from click-outside, Escape, Tab, selection, and the
439
+ // Popover's own open-change handler, any of which may fire when
440
+ // the dropdown is already closed.
441
+ const wasOpen = this.isOpen;
442
+ this.isOpen = false;
443
+ this.query = '';
444
+ this.activeId = null;
445
+ if (this.isAsync) {
446
+ this.remoteItems = null;
447
+ this.isLoadingInternal = false;
448
+ if (this.searchTimer) {
449
+ clearTimeout(this.searchTimer);
450
+ this.searchTimer = null;
451
+ }
452
+ }
453
+ if (wasOpen) this.args.onClose?.();
454
+ };
455
+ handleOpenChange = open => {
456
+ if (open) {
457
+ this.openDropdown();
458
+ } else {
459
+ this.closeDropdown();
460
+ }
461
+ };
462
+ // --------------------------------------------------------------------------
463
+ // Active-row management
464
+ // --------------------------------------------------------------------------
465
+ resetActiveToInitial() {
466
+ const rows = this.navigableRows;
467
+ if (rows.length === 0) {
468
+ this.activeId = null;
469
+ return;
470
+ }
471
+ if (this.args.value != null) {
472
+ const selected = rows.find(r => r.kind === 'option' && r.option.value === this.args.value);
473
+ if (selected) {
474
+ this.activeId = selected.optionId;
475
+ return;
476
+ }
477
+ }
478
+ this.activeId = rows[0].optionId;
479
+ }
480
+ resetActiveToFirstNavigable() {
481
+ const rows = this.navigableRows;
482
+ this.activeId = rows.length > 0 ? rows[0].optionId : null;
483
+ }
484
+ setActiveToFirst() {
485
+ const rows = this.navigableRows;
486
+ if (rows.length === 0) return;
487
+ this.activeId = rows[0].optionId;
488
+ this.scrollActiveIntoView();
489
+ }
490
+ setActiveToLast() {
491
+ const rows = this.navigableRows;
492
+ if (rows.length === 0) return;
493
+ this.activeId = rows[rows.length - 1].optionId;
494
+ this.scrollActiveIntoView();
495
+ }
496
+ moveActive(direction) {
497
+ const rows = this.navigableRows;
498
+ if (rows.length === 0) {
499
+ this.activeId = null;
500
+ return;
501
+ }
502
+ const currentIndex = this.activeId ? rows.findIndex(r => r.optionId === this.activeId) : -1;
503
+ let nextIndex;
504
+ if (currentIndex === -1) {
505
+ nextIndex = direction === 1 ? 0 : rows.length - 1;
506
+ } else {
507
+ nextIndex = currentIndex + direction;
508
+ if (nextIndex < 0) nextIndex = rows.length - 1;
509
+ if (nextIndex >= rows.length) nextIndex = 0;
510
+ }
511
+ this.activeId = rows[nextIndex].optionId;
512
+ this.scrollActiveIntoView();
513
+ }
514
+ /**
515
+ * Move the active row by `PAGE_STEP` items. Unlike `moveActive`,
516
+ * PageUp/PageDown don't wrap — they clamp to the first/last row so
517
+ * the user always lands on a valid entry at the ends.
518
+ */
519
+ movePage(direction) {
520
+ const rows = this.navigableRows;
521
+ if (rows.length === 0) {
522
+ this.activeId = null;
523
+ return;
524
+ }
525
+ const currentIndex = this.activeId ? rows.findIndex(r => r.optionId === this.activeId) : direction === 1 ? -1 : rows.length;
526
+ const targetIndex = currentIndex + direction * PAGE_STEP;
527
+ const clamped = Math.max(0, Math.min(rows.length - 1, targetIndex));
528
+ this.activeId = rows[clamped].optionId;
529
+ this.scrollActiveIntoView();
530
+ }
531
+ scrollActiveIntoView() {
532
+ if (!this.activeId) return;
533
+ // `activeId` is a unique element ID, so getElementById is the
534
+ // fastest + most robust lookup — works even if listboxElement
535
+ // hasn't registered yet.
536
+ const node = document.getElementById(this.activeId);
537
+ node?.scrollIntoView({
538
+ block: 'nearest'
539
+ });
540
+ }
541
+ // --------------------------------------------------------------------------
542
+ // Selection / activation
543
+ // --------------------------------------------------------------------------
544
+ activateActive() {
545
+ if (!this.activeId) return;
546
+ const row = this.navigableRows.find(r => r.optionId === this.activeId);
547
+ if (!row) return;
548
+ if (row.kind === 'create') {
549
+ this.fireCreate();
550
+ return;
551
+ }
552
+ if (row.option.isDisabled) return;
553
+ this.selectOption(row.option);
554
+ }
555
+ selectOption = option => {
556
+ if (option.isDisabled) return;
557
+ this.args.onChange?.(option.value, option);
558
+ this.closeDropdown();
559
+ this.inputElement?.focus({
560
+ preventScroll: true
561
+ });
562
+ };
563
+ fireCreate = () => {
564
+ if (!this.args.onCreate) return;
565
+ const query = this.query.trim();
566
+ if (!query) return;
567
+ this.args.onCreate(query);
568
+ // Clear the input so the consumer-rendered new option becomes visible
569
+ // immediately if they fold it into items synchronously. Don't close —
570
+ // the consumer may want the dropdown to stay open for confirmation.
571
+ this.query = '';
572
+ this.resetActiveToFirstNavigable();
573
+ };
574
+ handleOptionMouseDown = event => {
575
+ // Prevent the input from losing focus when an option is clicked.
576
+ event.preventDefault();
577
+ };
578
+ handleOptionMouseEnter = optionId => {
579
+ this.activeId = optionId;
580
+ };
581
+ handleClearMouseDown = event => {
582
+ event.preventDefault();
583
+ };
584
+ handleClearClick = event => {
585
+ event.preventDefault();
586
+ event.stopPropagation();
587
+ this.clearSelection();
588
+ };
589
+ clearSelection() {
590
+ this.args.onChange?.(null, null);
591
+ this.inputElement?.focus({
592
+ preventScroll: true
593
+ });
594
+ }
595
+ static {
596
+ setComponentTemplate(precompileTemplate("{{!-- Several elements bind to mousedown to call preventDefault \u2014 this is\n the canonical pattern for keeping focus on the input when the user\n clicks chevron/clear/option (the click handler runs after, with\n focus intact, avoiding spurious blur events). Click handlers are\n also bound, so keyboard activation still works. --}}\n{{!-- template-lint-disable no-pointer-down-event-binding --}}\n<Popover @trigger=\"manual\" @open={{this.isOpen}} @onOpenChange={{this.handleOpenChange}} @dismissOnClickOutside={{true}} @dismissOnEscape={{false}} class=\"combobox_e2657ea63\" data-invalid={{if @isInvalid \"true\"}} data-disabled={{if @isDisabled \"true\"}} data-open={{if this.isOpen \"true\"}} data-test-combobox ...attributes as |popover|>\n {{#if @name}}\n <input type=\"hidden\" name={{@name}} value={{if @value @value \"\"}} data-test-combobox-input-hidden />\n {{/if}}\n\n {{!-- Visually-hidden live region so screen readers learn how the\n result set changes as the user types. aria-atomic makes the\n SR read the whole announcement on each update rather than\n splicing deltas. --}}\n <div class=\"combobox-sr-only_e2657ea63\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" data-test-combobox-announcement>{{this.announcement}}</div>\n\n <div class=\"combobox-trigger_e2657ea63\" data-test-combobox-trigger-wrapper>\n {{!-- WAI-ARIA \"Editable Combobox With List Autocomplete\" pattern:\n role=\"combobox\" lives on the input itself; the input IS the\n trigger and the search field. The data-test-combobox-trigger\n and data-test-combobox-input hooks both point at the input\n so existing test selectors and the documented input selector\n both work. --}}\n {{!-- template-lint-disable no-redundant-role --}}\n <input {{popover.registerTrigger}} {{this.setupInput}} {{on \"click\" this.handleInputClick}} {{on \"focus\" this.handleInputFocus}} {{on \"blur\" this.handleInputBlur}} {{on \"input\" this.handleInput}} {{on \"keydown\" this.handleInputKeyDown}} id={{@id}} type=\"text\" class=\"combobox-input_e2657ea63\" role=\"combobox\" aria-autocomplete=\"list\" aria-haspopup=\"listbox\" aria-expanded={{this.ariaExpanded}} aria-controls={{this.listboxId}} aria-activedescendant={{this.activeId}} aria-required={{this.ariaRequired}} aria-invalid={{this.ariaInvalid}} aria-describedby={{@aria-describedby}} autocomplete=\"off\" spellcheck=\"false\" placeholder={{@placeholder}} value={{this.displayValue}} disabled={{@isDisabled}} data-test-combobox-trigger data-test-combobox-input />\n\n {{#if this.showClearButton}}\n <button type=\"button\" class=\"combobox-clear_e2657ea63\" aria-label=\"Clear selection\" tabindex=\"-1\" {{on \"mousedown\" this.handleClearMouseDown}} {{on \"click\" this.handleClearClick}} data-test-combobox-clear>\n <Icon @icon={{X}} @size=\"sm\" />\n </button>\n {{/if}}\n\n <button type=\"button\" class=\"combobox-chevron_e2657ea63\" aria-label={{if this.isOpen \"Close\" \"Open\"}} tabindex=\"-1\" disabled={{@isDisabled}} {{on \"mousedown\" this.handleChevronMouseDown}} {{on \"click\" this.handleChevronClick}} data-test-combobox-chevron>\n <Icon @icon={{ChevronDown}} @size=\"sm\" />\n </button>\n </div>\n\n <popover.Content @side=\"bottom\" @align=\"start\" @sideOffset={{4}} @animation=\"scale\" class=\"combobox-popover-content_e2657ea63\">\n <div id={{this.listboxId}} role=\"listbox\" class=\"combobox-listbox_e2657ea63\" tabindex=\"-1\" {{this.setupListbox}} data-test-combobox-listbox>\n {{#each this.renderedGroups key=\"key\" as |group|}}\n {{#if group.label}}\n <div role=\"group\" aria-labelledby={{group.headingId}} class=\"combobox-group_e2657ea63\" data-test-combobox-group>\n <div id={{group.headingId}} class=\"combobox-group-heading_e2657ea63\" data-test-combobox-group-heading>{{group.label}}</div>\n {{!-- Per WAI-ARIA, listbox > group > option is a valid\n hierarchy for grouped listboxes. --}}\n {{!-- template-lint-disable require-context-role --}}\n {{#each group.rows key=\"optionId\" as |row|}}\n <div id={{row.optionId}} role=\"option\" class=\"combobox-option_e2657ea63\" aria-selected={{if row.isSelected \"true\" \"false\"}} aria-disabled={{if row.option.isDisabled \"true\"}} data-active={{if row.isActive \"true\"}} data-selected={{if row.isSelected \"true\"}} data-disabled={{if row.option.isDisabled \"true\"}} data-option-id={{row.optionId}} data-value={{row.option.value}} {{on \"mousedown\" this.handleOptionMouseDown}} {{on \"click\" (fn this.selectOption row.option)}} {{on \"mouseenter\" (fn this.handleOptionMouseEnter row.optionId)}} data-test-combobox-option>\n <div class=\"combobox-option-content_e2657ea63\">\n <span class=\"combobox-option-label_e2657ea63\" data-test-combobox-option-label>\n {{~#each (splitOnMatch row.option.label this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"combobox-mark_e2657ea63\" data-test-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{#if row.option.description}}\n <span class=\"combobox-option-description_e2657ea63\" data-test-combobox-option-description>\n {{~#each (splitOnMatch row.option.description this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"combobox-mark_e2657ea63\" data-test-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{/if}}\n </div>\n {{#if row.isSelected}}\n <span class=\"combobox-option-check_e2657ea63\" aria-hidden=\"true\"></span>\n {{/if}}\n </div>\n {{/each}}\n </div>\n {{else}}\n {{#each group.rows key=\"optionId\" as |row|}}\n <div id={{row.optionId}} role=\"option\" class=\"combobox-option_e2657ea63\" aria-selected={{if row.isSelected \"true\" \"false\"}} aria-disabled={{if row.option.isDisabled \"true\"}} data-active={{if row.isActive \"true\"}} data-selected={{if row.isSelected \"true\"}} data-disabled={{if row.option.isDisabled \"true\"}} data-option-id={{row.optionId}} data-value={{row.option.value}} {{on \"mousedown\" this.handleOptionMouseDown}} {{on \"click\" (fn this.selectOption row.option)}} {{on \"mouseenter\" (fn this.handleOptionMouseEnter row.optionId)}} data-test-combobox-option>\n <div class=\"combobox-option-content_e2657ea63\">\n <span class=\"combobox-option-label_e2657ea63\" data-test-combobox-option-label>\n {{~#each (splitOnMatch row.option.label this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"combobox-mark_e2657ea63\" data-test-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{#if row.option.description}}\n <span class=\"combobox-option-description_e2657ea63\" data-test-combobox-option-description>\n {{~#each (splitOnMatch row.option.description this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"combobox-mark_e2657ea63\" data-test-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{/if}}\n </div>\n {{#if row.isSelected}}\n <span class=\"combobox-option-check_e2657ea63\" aria-hidden=\"true\"></span>\n {{/if}}\n </div>\n {{/each}}\n {{/if}}\n {{/each}}\n\n {{#if this.createRow}}\n <div id={{this.createRow.optionId}} role=\"option\" class=\"combobox-option_e2657ea63 combobox-create_e2657ea63\" aria-selected=\"false\" data-active={{if this.createRow.isActive \"true\"}} data-option-id={{this.createRow.optionId}} {{on \"mousedown\" this.handleOptionMouseDown}} {{on \"click\" this.fireCreate}} {{on \"mouseenter\" (fn this.handleOptionMouseEnter this.createRow.optionId)}} data-test-combobox-create>\n <span class=\"combobox-create-icon_e2657ea63\" aria-hidden=\"true\">+</span>\n <span class=\"combobox-option-label_e2657ea63\">{{this.createRow.label}}</span>\n </div>\n {{/if}}\n\n {{#if this.isLoading}}\n <div class=\"combobox-loading_e2657ea63\" data-test-combobox-loading>{{this.loadingText}}</div>\n {{else if this.showEmptyState}}\n <div class=\"combobox-empty_e2657ea63\" data-test-combobox-empty>{{this.noResultsText}}</div>\n {{/if}}\n </div>\n </popover.Content>\n</Popover>", {
597
+ strictMode: true,
598
+ scope: () => ({
599
+ Popover,
600
+ on,
601
+ Icon,
602
+ X,
603
+ ChevronDown,
604
+ fn,
605
+ splitOnMatch
606
+ })
607
+ }), this);
608
+ }
609
+ }
610
+
611
+ export { ComboBox as default };
612
+ //# sourceMappingURL=combobox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"combobox.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -1,4 +1,4 @@
1
- /* <inline>/src/form/display-field-css-890d9be4b5da61613fd017071f330735.css */
1
+ /* <inline>/src/form/display-field-css-502236a2343d47e31e52bdb93a769ca1.css */
2
2
 
3
3
  .display-field_e03c8ad71 {
4
4
  display: flex;
@@ -22,7 +22,7 @@
22
22
  }
23
23
 
24
24
  .display-field-label_e03c8ad71::after {
25
- content: ':';
25
+ content: ":";
26
26
  }
27
27
 
28
28
  .display-field-value_e03c8ad71 {
@@ -1,4 +1,4 @@
1
- import "./display-field-css-890d9be4b5da61613fd017071f330735.css"
1
+ import "./display-field-css-502236a2343d47e31e52bdb93a769ca1.css"
2
2
  import Component from '@glimmer/component';
3
3
  import { guidFor } from '@ember/object/internals';
4
4
  import Label from './label.js';
@@ -1,6 +1,10 @@
1
1
  export { default as Calendar } from './calendar.js';
2
2
  export { default as Checkbox } from './checkbox.js';
3
3
  export { default as CheckboxGroup } from './checkbox-group.js';
4
+ export { default as ComboBox } from './combobox.js';
5
+ export { default as ComboBoxField } from './combobox-field.js';
6
+ export { default as MultiComboBox } from './multi-combobox.js';
7
+ export { default as MultiComboBoxField } from './multi-combobox-field.js';
4
8
  export { default as Control } from './control.js';
5
9
  export { default as CountrySelect } from './country-select.js';
6
10
  export { default as CountrySelectField } from './country-select-field.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}