@cdevhub/ngx-tw 0.2.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
  4. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
  6. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
  7. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
  9. package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
  11. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
  13. package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
  15. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
  17. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
  18. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
  20. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
  22. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
  24. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
  26. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
  28. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
  30. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
  32. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
  38. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
  42. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
  44. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
  45. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
  47. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
  49. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
  51. package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
  53. package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
  55. package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
  57. package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
  59. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
  60. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
  61. package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
  62. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
  63. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
  64. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
  65. package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
  66. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
  67. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
  68. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
  69. package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
  70. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
  71. package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
  72. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
  73. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
  74. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
  75. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
  76. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
  77. package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
  78. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
  79. package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
  80. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
  81. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
  82. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
  83. package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
  84. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
  85. package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
  86. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
  87. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
  88. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
  89. package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
  90. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
  91. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
  92. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
  93. package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
  94. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
  95. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
  96. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
  97. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
  98. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
  99. package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
  100. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
  101. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
  102. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
  103. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
  104. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
  105. package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
  106. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
  107. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
  108. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
  109. package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
  110. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
  111. package/package.json +265 -0
  112. package/theme/_base.css +394 -0
  113. package/theme/_dark.css +510 -0
  114. package/theme/_high-contrast.css +290 -0
  115. package/theme/_semantic.css +317 -0
  116. package/theme/_typography.css +9 -0
  117. package/theme/index.css +54 -0
  118. package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
  119. package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
  120. package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
  121. package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
  122. package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
  123. package/types/cdevhub-ngx-tw-button.d.ts +74 -0
  124. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
  125. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
  126. package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
  127. package/types/cdevhub-ngx-tw-card.d.ts +48 -0
  128. package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
  129. package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
  130. package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
  131. package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
  132. package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
  133. package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
  134. package/types/cdevhub-ngx-tw-core.d.ts +719 -0
  135. package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
  136. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
  137. package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
  138. package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
  139. package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
  140. package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
  141. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
  142. package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
  143. package/types/cdevhub-ngx-tw-input.d.ts +110 -0
  144. package/types/cdevhub-ngx-tw-item.d.ts +70 -0
  145. package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
  146. package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
  147. package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
  148. package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
  149. package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
  150. package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
  151. package/types/cdevhub-ngx-tw-select.d.ts +384 -0
  152. package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
  153. package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
  154. package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
  155. package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
  156. package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
  157. package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
  158. package/types/cdevhub-ngx-tw-split.d.ts +265 -0
  159. package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
  160. package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
  161. package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
  162. package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
  163. package/types/cdevhub-ngx-tw-table.d.ts +479 -0
  164. package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
  165. package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
  166. package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
  167. package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
  168. package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
  169. package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
  170. package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
  171. package/types/cdevhub-ngx-tw.d.ts +49 -0
@@ -0,0 +1,1721 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, signal, computed, ChangeDetectionStrategy, Component, TemplateRef, Directive, input, model, output, contentChild, viewChild, ViewContainerRef, Injector, DestroyRef, linkedSignal, effect, untracked, forwardRef } from '@angular/core';
3
+ import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+ import { merge } from 'rxjs';
6
+ import { TW_ERROR_STATE_MATCHER, buildSelectLikePositions, resolveSelectScrollStrategy, consumeOverlayEscape } from '@cdevhub/ngx-tw/core';
7
+ import { Overlay } from '@angular/cdk/overlay';
8
+ import { ComponentPortal } from '@angular/cdk/portal';
9
+ import { Platform } from '@angular/cdk/platform';
10
+ import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
11
+ import { tv } from 'tailwind-variants';
12
+ import { FormFieldComponent, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
13
+ import { NgTemplateOutlet } from '@angular/common';
14
+
15
+ /**
16
+ * Internal overlay panel for `tw-combobox`. Renders the listbox region with
17
+ * grouped/flat option rows, the loading slot, and the empty fallback. Not
18
+ * exported from the public API — instances are attached via `ComponentPortal`
19
+ * by the parent `ComboboxComponent`.
20
+ */
21
+ class ComboboxOverlayComponent {
22
+ /** @internal */
23
+ hostElement = inject((ElementRef));
24
+ // ── Config signals pushed by the parent ──
25
+ /** @internal */
26
+ listboxId = signal('', ...(ngDevMode ? [{ debugName: "listboxId" }] : /* istanbul ignore next */ []));
27
+ /** @internal */
28
+ labelledBy = signal('', ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
29
+ /** @internal */
30
+ panelMaxHeight = signal(256, ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : /* istanbul ignore next */ []));
31
+ /** @internal */
32
+ emptyMessage = signal('No results', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
33
+ /** @internal */
34
+ query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
35
+ /** @internal */
36
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
37
+ /** @internal */
38
+ activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
39
+ /** @internal */
40
+ renderedRows = signal([], ...(ngDevMode ? [{ debugName: "renderedRows" }] : /* istanbul ignore next */ []));
41
+ /** @internal */
42
+ isSelected = signal(() => false, ...(ngDevMode ? [{ debugName: "isSelected" }] : /* istanbul ignore next */ []));
43
+ /** @internal */
44
+ optionIdFn = signal((i) => `option-${i}`, ...(ngDevMode ? [{ debugName: "optionIdFn" }] : /* istanbul ignore next */ []));
45
+ /** @internal */
46
+ groupHeaderIdFn = signal((g) => `group-${g}`, ...(ngDevMode ? [{ debugName: "groupHeaderIdFn" }] : /* istanbul ignore next */ []));
47
+ /** @internal */
48
+ customPanelClass = signal('', ...(ngDevMode ? [{ debugName: "customPanelClass" }] : /* istanbul ignore next */ []));
49
+ /** @internal */
50
+ optionTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "optionTemplate" }] : /* istanbul ignore next */ []));
51
+ /** @internal */
52
+ emptyTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : /* istanbul ignore next */ []));
53
+ /** @internal */
54
+ loadingTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "loadingTemplate" }] : /* istanbul ignore next */ []));
55
+ // ── Callbacks pushed by the parent ──
56
+ /** @internal */
57
+ onOptionPick = signal(() => { }, ...(ngDevMode ? [{ debugName: "onOptionPick" }] : /* istanbul ignore next */ []));
58
+ /** @internal */
59
+ onOptionHover = signal(() => { }, ...(ngDevMode ? [{ debugName: "onOptionHover" }] : /* istanbul ignore next */ []));
60
+ // ── Computed classes ──
61
+ /** @internal */
62
+ panelClasses = computed(() => {
63
+ const base = 'block w-full bg-surface-overlay border border-border rounded-md shadow-md overflow-hidden';
64
+ const custom = this.customPanelClass();
65
+ return custom ? `${base} ${custom}` : base;
66
+ }, ...(ngDevMode ? [{ debugName: "panelClasses" }] : /* istanbul ignore next */ []));
67
+ /** @internal */
68
+ optionClass(index, disabled) {
69
+ const base = 'flex cursor-pointer items-center gap-2 px-3 py-2 text-sm text-fg select-none transition-colors duration-normal motion-reduce:transition-none';
70
+ const parts = [base];
71
+ if (disabled) {
72
+ parts.push('opacity-50 pointer-events-none cursor-default');
73
+ }
74
+ else if (this.activeIndex() === index) {
75
+ parts.push('bg-surface-muted');
76
+ }
77
+ else {
78
+ parts.push('hover:bg-surface-muted');
79
+ }
80
+ return parts.join(' ');
81
+ }
82
+ /** @internal */
83
+ groupHeaderId(group) {
84
+ return this.groupHeaderIdFn()(group);
85
+ }
86
+ /** @internal */
87
+ buildOptionContext(index, option) {
88
+ return {
89
+ $implicit: option.option,
90
+ option: option.option,
91
+ label: option.label,
92
+ value: option.value,
93
+ selected: this.isSelected()(index),
94
+ active: this.activeIndex() === index,
95
+ disabled: option.disabled,
96
+ index,
97
+ };
98
+ }
99
+ /** @internal track function for `@for`. */
100
+ trackRow(_index, row) {
101
+ if (row.kind === 'group')
102
+ return `g:${row.group}`;
103
+ return `o:${row.index}`;
104
+ }
105
+ // ── Interactions ──
106
+ /** @internal — `preventDefault` on mousedown keeps DOM focus on the input. */
107
+ onOptionMousedown(event, _index) {
108
+ event.preventDefault();
109
+ }
110
+ /** @internal */
111
+ onOptionClick(event, index) {
112
+ event.preventDefault();
113
+ event.stopPropagation();
114
+ this.onOptionPick()(index);
115
+ }
116
+ /** @internal */
117
+ onOptionMouseEnter(index, disabled) {
118
+ if (disabled)
119
+ return;
120
+ this.onOptionHover()(index);
121
+ }
122
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ComboboxOverlayComponent, isStandalone: true, selector: "tw-combobox-overlay", host: { properties: { "class": "panelClasses()", "animate.enter": "\"scale-in\"", "animate.leave": "\"scale-out\"" } }, ngImport: i0, template: `
124
+ @if (loading()) {
125
+ @if (loadingTemplate(); as tpl) {
126
+ <ng-container *ngTemplateOutlet="tpl" />
127
+ } @else {
128
+ <div class="flex items-center justify-center gap-2 px-4 py-6 text-sm text-fg-muted">
129
+ <svg
130
+ viewBox="0 0 24 24"
131
+ fill="none"
132
+ aria-hidden="true"
133
+ class="size-4 animate-spin"
134
+ >
135
+ <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" opacity="0.25" />
136
+ <path
137
+ d="M21 12a9 9 0 0 0-9-9"
138
+ stroke="currentColor"
139
+ stroke-width="3"
140
+ stroke-linecap="round"
141
+ />
142
+ </svg>
143
+ <span>Loading…</span>
144
+ </div>
145
+ }
146
+ }
147
+
148
+ <ul
149
+ [id]="listboxId()"
150
+ role="listbox"
151
+ [attr.aria-labelledby]="labelledBy() || null"
152
+ aria-multiselectable="false"
153
+ [style.maxHeight.px]="panelMaxHeight()"
154
+ class="list-none m-0 p-0 overflow-y-auto py-1"
155
+ >
156
+ @if (renderedRows().length === 0 && !loading()) {
157
+ @if (emptyTemplate(); as tpl) {
158
+ <ng-container *ngTemplateOutlet="tpl; context: { $implicit: query() }" />
159
+ } @else {
160
+ <li class="px-4 py-10 text-center text-sm text-fg-muted list-none">
161
+ {{ emptyMessage() }}
162
+ </li>
163
+ }
164
+ } @else {
165
+ @for (row of renderedRows(); track trackRow($index, row)) {
166
+ @if (row.kind === 'group') {
167
+ <li
168
+ role="group"
169
+ [attr.aria-labelledby]="groupHeaderId(row.group)"
170
+ class="list-none"
171
+ >
172
+ <div
173
+ [id]="groupHeaderId(row.group)"
174
+ class="px-3 py-1 text-2xs uppercase tracking-wide text-fg-subtle font-semibold"
175
+ >
176
+ {{ row.group }}
177
+ </div>
178
+ </li>
179
+ } @else {
180
+ <li
181
+ role="option"
182
+ [id]="optionIdFn()(row.index)"
183
+ [attr.aria-selected]="isSelected()(row.index) ? 'true' : 'false'"
184
+ [attr.aria-disabled]="row.option.disabled ? 'true' : null"
185
+ [class]="optionClass(row.index, row.option.disabled)"
186
+ (mousedown)="onOptionMousedown($event, row.index)"
187
+ (click)="onOptionClick($event, row.index)"
188
+ (mouseenter)="onOptionMouseEnter(row.index, row.option.disabled)"
189
+ >
190
+ @if (optionTemplate(); as tpl) {
191
+ <ng-container
192
+ *ngTemplateOutlet="
193
+ tpl;
194
+ context: buildOptionContext(row.index, row.option)
195
+ "
196
+ />
197
+ } @else {
198
+ <span class="flex-1 min-w-0">
199
+ <span class="block truncate">{{ row.option.label }}</span>
200
+ @if (row.option.description) {
201
+ <span class="block text-xs text-fg-muted truncate">{{ row.option.description }}</span>
202
+ }
203
+ </span>
204
+ }
205
+ </li>
206
+ }
207
+ }
208
+ }
209
+ </ul>
210
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
211
+ }
212
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxOverlayComponent, decorators: [{
213
+ type: Component,
214
+ args: [{
215
+ selector: 'tw-combobox-overlay',
216
+ changeDetection: ChangeDetectionStrategy.OnPush,
217
+ imports: [NgTemplateOutlet],
218
+ host: {
219
+ '[class]': 'panelClasses()',
220
+ '[animate.enter]': '"scale-in"',
221
+ '[animate.leave]': '"scale-out"',
222
+ },
223
+ template: `
224
+ @if (loading()) {
225
+ @if (loadingTemplate(); as tpl) {
226
+ <ng-container *ngTemplateOutlet="tpl" />
227
+ } @else {
228
+ <div class="flex items-center justify-center gap-2 px-4 py-6 text-sm text-fg-muted">
229
+ <svg
230
+ viewBox="0 0 24 24"
231
+ fill="none"
232
+ aria-hidden="true"
233
+ class="size-4 animate-spin"
234
+ >
235
+ <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" opacity="0.25" />
236
+ <path
237
+ d="M21 12a9 9 0 0 0-9-9"
238
+ stroke="currentColor"
239
+ stroke-width="3"
240
+ stroke-linecap="round"
241
+ />
242
+ </svg>
243
+ <span>Loading…</span>
244
+ </div>
245
+ }
246
+ }
247
+
248
+ <ul
249
+ [id]="listboxId()"
250
+ role="listbox"
251
+ [attr.aria-labelledby]="labelledBy() || null"
252
+ aria-multiselectable="false"
253
+ [style.maxHeight.px]="panelMaxHeight()"
254
+ class="list-none m-0 p-0 overflow-y-auto py-1"
255
+ >
256
+ @if (renderedRows().length === 0 && !loading()) {
257
+ @if (emptyTemplate(); as tpl) {
258
+ <ng-container *ngTemplateOutlet="tpl; context: { $implicit: query() }" />
259
+ } @else {
260
+ <li class="px-4 py-10 text-center text-sm text-fg-muted list-none">
261
+ {{ emptyMessage() }}
262
+ </li>
263
+ }
264
+ } @else {
265
+ @for (row of renderedRows(); track trackRow($index, row)) {
266
+ @if (row.kind === 'group') {
267
+ <li
268
+ role="group"
269
+ [attr.aria-labelledby]="groupHeaderId(row.group)"
270
+ class="list-none"
271
+ >
272
+ <div
273
+ [id]="groupHeaderId(row.group)"
274
+ class="px-3 py-1 text-2xs uppercase tracking-wide text-fg-subtle font-semibold"
275
+ >
276
+ {{ row.group }}
277
+ </div>
278
+ </li>
279
+ } @else {
280
+ <li
281
+ role="option"
282
+ [id]="optionIdFn()(row.index)"
283
+ [attr.aria-selected]="isSelected()(row.index) ? 'true' : 'false'"
284
+ [attr.aria-disabled]="row.option.disabled ? 'true' : null"
285
+ [class]="optionClass(row.index, row.option.disabled)"
286
+ (mousedown)="onOptionMousedown($event, row.index)"
287
+ (click)="onOptionClick($event, row.index)"
288
+ (mouseenter)="onOptionMouseEnter(row.index, row.option.disabled)"
289
+ >
290
+ @if (optionTemplate(); as tpl) {
291
+ <ng-container
292
+ *ngTemplateOutlet="
293
+ tpl;
294
+ context: buildOptionContext(row.index, row.option)
295
+ "
296
+ />
297
+ } @else {
298
+ <span class="flex-1 min-w-0">
299
+ <span class="block truncate">{{ row.option.label }}</span>
300
+ @if (row.option.description) {
301
+ <span class="block text-xs text-fg-muted truncate">{{ row.option.description }}</span>
302
+ }
303
+ </span>
304
+ }
305
+ </li>
306
+ }
307
+ }
308
+ }
309
+ </ul>
310
+ `,
311
+ }]
312
+ }] });
313
+
314
+ // ── Constants ──
315
+ /** Duration of leave animation (ms) — matches scale-out/fade-out in theme/_base.css. */
316
+ const ANIMATION_DURATION = 120;
317
+ /** Debounce window (ms) for LiveAnnouncer result announcements while typing. */
318
+ const ANNOUNCE_DEBOUNCE = 200;
319
+ /** Sentinel for an unresolved write-value awaiting late-arriving options. */
320
+ const UNRESOLVED = Symbol('combobox-unresolved');
321
+ let nextComboboxId = 0;
322
+ // ── Accessor defaults ──
323
+ function defaultOptionLabel(o) {
324
+ const opt = o;
325
+ if (typeof opt.label === 'string')
326
+ return opt.label;
327
+ return String(opt.value ?? '');
328
+ }
329
+ function defaultOptionValue(o) {
330
+ return o.value;
331
+ }
332
+ function defaultOptionDisabled(o) {
333
+ return !!o.disabled;
334
+ }
335
+ function defaultOptionGroup(o) {
336
+ return o.group;
337
+ }
338
+ function defaultOptionDescription(o) {
339
+ return o.description;
340
+ }
341
+ function defaultStartsWithFilter(option, query) {
342
+ const trimmed = query.trim().toLowerCase();
343
+ if (!trimmed)
344
+ return true;
345
+ const label = defaultOptionLabel(option).toLowerCase();
346
+ return label.startsWith(trimmed);
347
+ }
348
+ // ── tv() config ──
349
+ const comboboxVariants = tv({
350
+ slots: {
351
+ root: 'relative inline-block w-full',
352
+ // container-focus indicator — the `<input>` clears its own outline
353
+ // (`outline-none` in the input slot below) so the surrounding trigger
354
+ // surface owns the visible ring via `focus-within:`. The color/width
355
+ // variants below paint this ring per the `color`/`errorState` axis.
356
+ trigger: 'flex w-full items-center gap-1.5 rounded-md border border-border bg-surface text-fg transition-colors duration-normal motion-reduce:transition-none hover:border-border-strong focus-within:outline-2 focus-within:outline-offset-2',
357
+ input: 'flex-1 min-w-0 bg-transparent outline-none placeholder:text-fg-subtle disabled:cursor-not-allowed',
358
+ clearButton: 'inline-flex items-center justify-center shrink-0 size-5 rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
359
+ chevron: 'shrink-0 text-fg-muted transition-transform duration-normal motion-reduce:transition-none',
360
+ spinner: 'shrink-0 text-fg-muted animate-spin',
361
+ },
362
+ variants: {
363
+ size: {
364
+ // xs density: chevron and spinner use `size-3.5` (14px) — the codified
365
+ // half-step between size-3 and size-4 that lines up with text-xs metric
366
+ // inside a 28px trigger.
367
+ xs: { trigger: 'px-2 py-1 text-xs', chevron: 'size-3.5', clearButton: 'size-4', spinner: 'size-3.5' },
368
+ sm: { trigger: 'px-3 py-1.5 text-sm', chevron: 'size-4', spinner: 'size-4' },
369
+ md: { trigger: 'px-4 py-2 text-sm', chevron: 'size-4', spinner: 'size-4' },
370
+ lg: { trigger: 'px-5 py-2.5 text-base', chevron: 'size-5', spinner: 'size-5' },
371
+ xl: { trigger: 'px-6 py-3 text-base', chevron: 'size-5', spinner: 'size-5' },
372
+ },
373
+ color: {
374
+ primary: { trigger: 'focus-within:outline-primary-500' },
375
+ secondary: { trigger: 'focus-within:outline-secondary-500' },
376
+ accent: { trigger: 'focus-within:outline-accent-500' },
377
+ neutral: { trigger: 'focus-within:outline-border-strong' },
378
+ info: { trigger: 'focus-within:outline-info-500' },
379
+ success: { trigger: 'focus-within:outline-success-500' },
380
+ warning: { trigger: 'focus-within:outline-warning-500' },
381
+ error: { trigger: 'focus-within:outline-error-500' },
382
+ },
383
+ disabled: {
384
+ true: { root: 'opacity-50 pointer-events-none cursor-not-allowed' },
385
+ false: {},
386
+ },
387
+ open: {
388
+ true: { chevron: 'rotate-180' },
389
+ false: {},
390
+ },
391
+ naked: {
392
+ true: {
393
+ trigger: 'border-0 bg-transparent rounded-none px-0 py-0 hover:border-0 focus-within:outline-0 focus-within:outline-offset-0',
394
+ },
395
+ false: {},
396
+ },
397
+ errorState: {
398
+ true: { trigger: 'focus-within:outline-error-500' },
399
+ false: {},
400
+ },
401
+ },
402
+ defaultVariants: {
403
+ size: 'md',
404
+ color: 'primary',
405
+ disabled: false,
406
+ open: false,
407
+ naked: false,
408
+ errorState: false,
409
+ },
410
+ }, { twMerge: true });
411
+ // ── Slot directives ──
412
+ /** Structural directive projecting a custom template for each option row. Context: `TwComboboxOptionContext<T>`. */
413
+ class ComboboxOptionTemplateDirective {
414
+ /** @internal */
415
+ templateRef = inject((TemplateRef));
416
+ /** @internal */
417
+ static ngTemplateContextGuard(_dir, _ctx) {
418
+ return true;
419
+ }
420
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxOptionTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
421
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ComboboxOptionTemplateDirective, isStandalone: true, selector: "[twComboboxOption]", ngImport: i0 });
422
+ }
423
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxOptionTemplateDirective, decorators: [{
424
+ type: Directive,
425
+ args: [{ selector: '[twComboboxOption]' }]
426
+ }] });
427
+ /** Structural directive projecting a custom template for the empty-results state. Context: `{ $implicit: query }`. */
428
+ class ComboboxEmptyTemplateDirective {
429
+ /** @internal */
430
+ templateRef = inject((TemplateRef));
431
+ /** @internal */
432
+ static ngTemplateContextGuard(_dir, _ctx) {
433
+ return true;
434
+ }
435
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxEmptyTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
436
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ComboboxEmptyTemplateDirective, isStandalone: true, selector: "[twComboboxEmpty]", ngImport: i0 });
437
+ }
438
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxEmptyTemplateDirective, decorators: [{
439
+ type: Directive,
440
+ args: [{ selector: '[twComboboxEmpty]' }]
441
+ }] });
442
+ /** Structural directive projecting a custom template above the list while `loading=true`. */
443
+ class ComboboxLoadingTemplateDirective {
444
+ /** @internal */
445
+ templateRef = inject((TemplateRef));
446
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxLoadingTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
447
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ComboboxLoadingTemplateDirective, isStandalone: true, selector: "[twComboboxLoading]", ngImport: i0 });
448
+ }
449
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxLoadingTemplateDirective, decorators: [{
450
+ type: Directive,
451
+ args: [{ selector: '[twComboboxLoading]' }]
452
+ }] });
453
+ /** Attribute directive marking a leading adornment projected inside the input row. */
454
+ class ComboboxPrefixDirective {
455
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxPrefixDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
456
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ComboboxPrefixDirective, isStandalone: true, selector: "[twComboboxPrefix]", host: { classAttribute: "flex items-center shrink-0 text-fg-muted" }, ngImport: i0 });
457
+ }
458
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxPrefixDirective, decorators: [{
459
+ type: Directive,
460
+ args: [{
461
+ selector: '[twComboboxPrefix]',
462
+ host: { class: 'flex items-center shrink-0 text-fg-muted' },
463
+ }]
464
+ }] });
465
+ /** Attribute directive marking a trailing adornment projected inside the input row, before the clear button. */
466
+ class ComboboxSuffixDirective {
467
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxSuffixDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
468
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ComboboxSuffixDirective, isStandalone: true, selector: "[twComboboxSuffix]", host: { classAttribute: "flex items-center shrink-0 text-fg-muted" }, ngImport: i0 });
469
+ }
470
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxSuffixDirective, decorators: [{
471
+ type: Directive,
472
+ args: [{
473
+ selector: '[twComboboxSuffix]',
474
+ host: { class: 'flex items-center shrink-0 text-fg-muted' },
475
+ }]
476
+ }] });
477
+ // ── Component ──
478
+ /**
479
+ * Editable single-select typeahead form control. Follows the WAI-ARIA 1.2
480
+ * combobox + listbox pattern with `aria-activedescendant` (DOM focus stays
481
+ * on the `<input>`). Supports local filtering, async result streams via
482
+ * `queryChange`, grouped options, strict and free-text modes, and full
483
+ * integration with template-driven, reactive, and signal forms plus
484
+ * `tw-form-field`.
485
+ */
486
+ class ComboboxComponent {
487
+ // ── Inputs ──
488
+ /** Array of options to render in the popover. Accepts plain records or `TwComboboxOption<T>`. */
489
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
490
+ /** Accessor returning the visible label for an option. Used by the default filter and the trigger label resolver. */
491
+ optionLabel = input(defaultOptionLabel, ...(ngDevMode ? [{ debugName: "optionLabel" }] : /* istanbul ignore next */ []));
492
+ /** Accessor returning the value emitted via `valueCommit` when this option is picked. */
493
+ optionValue = input(defaultOptionValue, ...(ngDevMode ? [{ debugName: "optionValue" }] : /* istanbul ignore next */ []));
494
+ /** Accessor returning whether an option is non-interactive. */
495
+ optionDisabled = input(defaultOptionDisabled, ...(ngDevMode ? [{ debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
496
+ /** Accessor returning a group label. Options sharing a group render under a labelled `role="group"` region. */
497
+ optionGroup = input(defaultOptionGroup, ...(ngDevMode ? [{ debugName: "optionGroup" }] : /* istanbul ignore next */ []));
498
+ /** Accessor returning an optional secondary description rendered under the label in the default option row. */
499
+ optionDescription = input(defaultOptionDescription, ...(ngDevMode ? [{ debugName: "optionDescription" }] : /* istanbul ignore next */ []));
500
+ /** Filter function applied client-side whenever `inputValue` changes. Pass `null` to disable client filtering (async mode). Defaults to case-insensitive `startsWith` on the label. */
501
+ filterFn = input(defaultStartsWithFilter, ...(ngDevMode ? [{ debugName: "filterFn" }] : /* istanbul ignore next */ []));
502
+ /** When `true`, free-text commits are rejected — the input reverts to the last committed label on blur. */
503
+ strict = input(false, ...(ngDevMode ? [{ debugName: "strict" }] : /* istanbul ignore next */ []));
504
+ /** Placeholder shown when the input is empty. */
505
+ placeholder = input(undefined, ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
506
+ /** Disables the input and prevents the popover from opening. */
507
+ disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
508
+ /** Sets `aria-required="true"` on the input. */
509
+ requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
510
+ /** Controls trigger padding and font size per the inline padding scale. */
511
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
512
+ /** Semantic color for the focus ring. */
513
+ color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
514
+ /** Whether the trailing chevron affordance is rendered. */
515
+ // TRUE-default: the chevron signals the dropdown affordance; without it the
516
+ // combobox reads as a plain input. Opt-out is for inline search inputs.
517
+ showChevron = input(true, ...(ngDevMode ? [{ debugName: "showChevron" }] : /* istanbul ignore next */ []));
518
+ /** Whether the inline clear (×) button appears while `inputValue` is non-empty. */
519
+ // TRUE-default: the clear affordance is the expected combobox UX once a value
520
+ // is set; opt-out is for required-only flows.
521
+ clearable = input(true, ...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
522
+ /** When `true`, shows an in-popover spinner and an inline spinner in the trigger while the popover is open. */
523
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
524
+ /** Debounce window (ms) before `queryChange` emits. Local filtering is not debounced. */
525
+ queryDebounce = input(150, ...(ngDevMode ? [{ debugName: "queryDebounce" }] : /* istanbul ignore next */ []));
526
+ /** Minimum query length before the popover opens automatically. `0` opens on focus. */
527
+ minQueryLength = input(0, ...(ngDevMode ? [{ debugName: "minQueryLength" }] : /* istanbul ignore next */ []));
528
+ /** Whether the popover opens automatically when the input receives focus. */
529
+ // TRUE-default: clicking into a combobox opens the dropdown — standard select-
530
+ // like UX. Opt-out is for command-palette-style flows triggered only by typing.
531
+ openOnFocus = input(true, ...(ngDevMode ? [{ debugName: "openOnFocus" }] : /* istanbul ignore next */ []));
532
+ /** Maximum height (px) of the popover scroll region. */
533
+ panelMaxHeight = input(256, ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : /* istanbul ignore next */ []));
534
+ /** Overlay width strategy. `'trigger'` matches input width; `'auto'` lets content decide; a number is applied as px; a string is passed as a CSS length. */
535
+ panelWidth = input('trigger', ...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
536
+ /** Extra class(es) appended to the overlay panel for consumer customization. */
537
+ panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
538
+ /** CDK overlay scroll strategy. */
539
+ scrollStrategy = input('reposition', ...(ngDevMode ? [{ debugName: "scrollStrategy" }] : /* istanbul ignore next */ []));
540
+ /** Pixel offset between the input and the popover. */
541
+ offset = input(4, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
542
+ /** Fallback empty-state message when no `*twComboboxEmpty` template is projected. */
543
+ emptyMessage = input('No results', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
544
+ /** Equality comparator used to reconcile `value` with options during `writeValue`. */
545
+ compareWith = input(Object.is, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
546
+ /** Accessible name for the combobox input. */
547
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
548
+ /** ID of an external label element. */
549
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
550
+ /** ID of an external descriptor element. */
551
+ ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
552
+ /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the combobox uses the `TW_ERROR_STATE_MATCHER` token's value. */
553
+ errorStateMatcher = input(undefined, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
554
+ // ── Models ──
555
+ /** Two-way bound committed value. May be an option's value (`T`), a typed string (free-text mode), or `null`. */
556
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
557
+ /** Two-way bound visible text in the input. Bound separately from `value` so async consumers can drive the query. */
558
+ inputValue = model('', ...(ngDevMode ? [{ debugName: "inputValue" }] : /* istanbul ignore next */ []));
559
+ /** Two-way bound open state of the popover. */
560
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
561
+ // ── Outputs ──
562
+ /** Fires after the query text changes, debounced by `queryDebounce`. Async-mode consumers subscribe to this to fetch results. */
563
+ queryChange = output();
564
+ /** Fires when the user picks an option from the list (not on free-text commit). */
565
+ optionSelected = output();
566
+ /** Fires whenever `value` changes, with a `source` discriminator distinguishing option / free-text / reset / programmatic origin. */
567
+ valueCommit = output();
568
+ /** Fires when the popover opens or closes. */
569
+ openedChange = output();
570
+ // ── Content queries ──
571
+ /** @internal */
572
+ optionTemplateChild = contentChild(ComboboxOptionTemplateDirective, ...(ngDevMode ? [{ debugName: "optionTemplateChild" }] : /* istanbul ignore next */ []));
573
+ /** @internal */
574
+ emptyTemplateChild = contentChild(ComboboxEmptyTemplateDirective, ...(ngDevMode ? [{ debugName: "emptyTemplateChild" }] : /* istanbul ignore next */ []));
575
+ /** @internal */
576
+ loadingTemplateChild = contentChild(ComboboxLoadingTemplateDirective, ...(ngDevMode ? [{ debugName: "loadingTemplateChild" }] : /* istanbul ignore next */ []));
577
+ // ── View queries ──
578
+ inputRef = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
579
+ triggerSurfaceRef = viewChild('triggerSurface', ...(ngDevMode ? [{ debugName: "triggerSurfaceRef" }] : /* istanbul ignore next */ []));
580
+ // ── Injected deps ──
581
+ overlayService = inject(Overlay);
582
+ elementRef = inject((ElementRef));
583
+ viewContainerRef = inject(ViewContainerRef);
584
+ injector = inject(Injector);
585
+ destroyRef = inject(DestroyRef);
586
+ focusMonitor = inject(FocusMonitor);
587
+ liveAnnouncer = inject(LiveAnnouncer);
588
+ platform = inject(Platform);
589
+ formField = inject(FormFieldComponent, { optional: true });
590
+ ngControl = inject(NgControl, { optional: true, self: true });
591
+ parentForm = inject(NgForm, { optional: true });
592
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
593
+ defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
594
+ // ── Identity ──
595
+ uid = nextComboboxId++;
596
+ /** @internal */
597
+ hostId = `tw-combobox-${this.uid}`;
598
+ /** @internal */
599
+ listboxId = `${this.hostId}-listbox`;
600
+ /** @internal */
601
+ optionId = (index) => `${this.hostId}-option-${index}`;
602
+ /** @internal */
603
+ groupHeaderId = (group) => `${this.hostId}-group-${group.replace(/\s+/g, '-').toLowerCase()}`;
604
+ // ── Internal state signals ──
605
+ /** @internal */
606
+ composing = signal(false, ...(ngDevMode ? [{ debugName: "composing" }] : /* istanbul ignore next */ []));
607
+ /** @internal */
608
+ focusedSignal = signal(false, ...(ngDevMode ? [{ debugName: "focusedSignal" }] : /* istanbul ignore next */ []));
609
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
610
+ _ngControlRev = signal(0, ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
611
+ _formSubmitRev = signal(0, ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
612
+ describedByIdsSignal = signal([], ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
613
+ labelledByIdsSignal = signal([], ...(ngDevMode ? [{ debugName: "labelledByIdsSignal" }] : /* istanbul ignore next */ []));
614
+ /** @internal */
615
+ lastCommittedLabel = signal('', ...(ngDevMode ? [{ debugName: "lastCommittedLabel" }] : /* istanbul ignore next */ []));
616
+ pendingWriteValue = signal(UNRESOLVED, ...(ngDevMode ? [{ debugName: "pendingWriteValue" }] : /* istanbul ignore next */ []));
617
+ closingSignal = signal(false, ...(ngDevMode ? [{ debugName: "closingSignal" }] : /* istanbul ignore next */ []));
618
+ /** Re-runs the input handler once after IME composition ends. */
619
+ composingPendingValue = null;
620
+ onChange = () => { };
621
+ onTouched = () => { };
622
+ overlayRef = null;
623
+ overlayInstanceSignal = signal(null, ...(ngDevMode ? [{ debugName: "overlayInstanceSignal" }] : /* istanbul ignore next */ []));
624
+ closeTimer = null;
625
+ queryDebounceTimer = null;
626
+ announceTimer = null;
627
+ resizeObserver = null;
628
+ get overlayInstance() {
629
+ return this.overlayInstanceSignal();
630
+ }
631
+ // ── Derived state ──
632
+ /** @internal */
633
+ isDisabled = computed(() => this.disabledInput() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
634
+ /** @internal Auto-naked when wrapped in tw-form-field. */
635
+ naked = computed(() => !!this.formField, ...(ngDevMode ? [{ debugName: "naked" }] : /* istanbul ignore next */ []));
636
+ /** @internal Resolved visible options (after filter, with derived view fields). */
637
+ visibleOptions = computed(() => {
638
+ const opts = this.options();
639
+ const labelFn = this.optionLabel();
640
+ const valueFn = this.optionValue();
641
+ const disabledFn = this.optionDisabled();
642
+ const groupFn = this.optionGroup();
643
+ const descFn = this.optionDescription();
644
+ const filter = this.filterFn();
645
+ const query = this.inputValue();
646
+ const filtered = filter === null ? opts : opts.filter((o) => filter(o, query));
647
+ return filtered.map((option) => ({
648
+ option,
649
+ label: labelFn(option),
650
+ value: valueFn(option),
651
+ disabled: disabledFn(option),
652
+ group: groupFn(option),
653
+ description: descFn(option),
654
+ }));
655
+ }, ...(ngDevMode ? [{ debugName: "visibleOptions" }] : /* istanbul ignore next */ []));
656
+ /** @internal Rendered rows (group headers + option rows). */
657
+ renderedRows = computed(() => {
658
+ const visible = this.visibleOptions();
659
+ const rows = [];
660
+ let currentGroup;
661
+ visible.forEach((v, index) => {
662
+ if (v.group !== currentGroup) {
663
+ if (v.group !== undefined) {
664
+ rows.push({ kind: 'group', group: v.group });
665
+ }
666
+ currentGroup = v.group;
667
+ }
668
+ rows.push({ kind: 'option', option: v, index });
669
+ });
670
+ return rows;
671
+ }, ...(ngDevMode ? [{ debugName: "renderedRows" }] : /* istanbul ignore next */ []));
672
+ /** Returns the index of the first enabled option, or -1 when none. */
673
+ firstEnabledIndex(visible = this.visibleOptions()) {
674
+ for (let i = 0; i < visible.length; i++) {
675
+ if (!visible[i].disabled)
676
+ return i;
677
+ }
678
+ return -1;
679
+ }
680
+ /** Returns the index of the last enabled option, or -1 when none. */
681
+ lastEnabledIndex(visible = this.visibleOptions()) {
682
+ for (let i = visible.length - 1; i >= 0; i--) {
683
+ if (!visible[i].disabled)
684
+ return i;
685
+ }
686
+ return -1;
687
+ }
688
+ /** @internal Active descendant index, resets to the first enabled when the visible list changes. */
689
+ activeIndex = linkedSignal({ ...(ngDevMode ? { debugName: "activeIndex" } : /* istanbul ignore next */ {}), source: () => this.visibleOptions(),
690
+ computation: (visible) => {
691
+ for (let i = 0; i < visible.length; i++) {
692
+ if (!visible[i].disabled)
693
+ return i;
694
+ }
695
+ return -1;
696
+ } });
697
+ /** @internal */
698
+ activeOption = computed(() => {
699
+ const idx = this.activeIndex();
700
+ const visible = this.visibleOptions();
701
+ if (idx < 0 || idx >= visible.length)
702
+ return null;
703
+ return visible[idx];
704
+ }, ...(ngDevMode ? [{ debugName: "activeOption" }] : /* istanbul ignore next */ []));
705
+ /** @internal */
706
+ activeOptionId = computed(() => {
707
+ const idx = this.activeIndex();
708
+ if (idx < 0 || idx >= this.visibleOptions().length)
709
+ return null;
710
+ return this.optionId(idx);
711
+ }, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : /* istanbul ignore next */ []));
712
+ /** @internal */
713
+ isEmpty = computed(() => this.inputValue().length === 0 && this.value() == null, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
714
+ /** @internal */
715
+ showClearButton = computed(() => this.clearable() && !this.isDisabled() && this.inputValue().length > 0, ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
716
+ /** @internal */
717
+ showSpinner = computed(() => this.loading(), ...(ngDevMode ? [{ debugName: "showSpinner" }] : /* istanbul ignore next */ []));
718
+ /** @internal */
719
+ resolvedDescribedBy = computed(() => {
720
+ const extra = this.describedByIdsSignal();
721
+ const user = this.ariaDescribedby();
722
+ const merged = [...extra];
723
+ if (user)
724
+ merged.push(...user.split(/\s+/).filter(Boolean));
725
+ return merged.length ? merged.join(' ') : '';
726
+ }, ...(ngDevMode ? [{ debugName: "resolvedDescribedBy" }] : /* istanbul ignore next */ []));
727
+ /** @internal */
728
+ resolvedLabelledBy = computed(() => {
729
+ const extra = this.labelledByIdsSignal();
730
+ const user = this.ariaLabelledby();
731
+ const merged = [...extra];
732
+ if (user)
733
+ merged.push(...user.split(/\s+/).filter(Boolean));
734
+ return merged.length ? merged.join(' ') : '';
735
+ }, ...(ngDevMode ? [{ debugName: "resolvedLabelledBy" }] : /* istanbul ignore next */ []));
736
+ /** @internal Error-state per the configured `ErrorStateMatcher`. Reads the bound `NgControl.invalid` through the matcher. */
737
+ errorState = computed(() => {
738
+ this._ngControlRev();
739
+ this._formSubmitRev();
740
+ const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
741
+ const form = this.parentFormGroup ??
742
+ this.parentForm;
743
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
744
+ }, ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
745
+ // ── tv() output ──
746
+ variantResult = computed(() => comboboxVariants({
747
+ size: this.size(),
748
+ color: this.color(),
749
+ disabled: this.isDisabled(),
750
+ open: this.open(),
751
+ naked: this.naked(),
752
+ errorState: this.errorState(),
753
+ }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
754
+ /** @internal */
755
+ rootClasses = computed(() => this.variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
756
+ /** @internal */
757
+ triggerClasses = computed(() => this.variantResult().trigger(), ...(ngDevMode ? [{ debugName: "triggerClasses" }] : /* istanbul ignore next */ []));
758
+ /** @internal */
759
+ inputClasses = computed(() => this.variantResult().input(), ...(ngDevMode ? [{ debugName: "inputClasses" }] : /* istanbul ignore next */ []));
760
+ /** @internal */
761
+ clearButtonClasses = computed(() => this.variantResult().clearButton(), ...(ngDevMode ? [{ debugName: "clearButtonClasses" }] : /* istanbul ignore next */ []));
762
+ /** @internal */
763
+ chevronClasses = computed(() => this.variantResult().chevron(), ...(ngDevMode ? [{ debugName: "chevronClasses" }] : /* istanbul ignore next */ []));
764
+ /** @internal */
765
+ spinnerClasses = computed(() => this.variantResult().spinner(), ...(ngDevMode ? [{ debugName: "spinnerClasses" }] : /* istanbul ignore next */ []));
766
+ // ── FormFieldControl impl ──
767
+ /** @internal */
768
+ id = computed(() => this.hostId, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
769
+ /** @internal */
770
+ focused = this.focusedSignal.asReadonly();
771
+ /** @internal */
772
+ empty = this.isEmpty;
773
+ /** @internal */
774
+ disabled = this.isDisabled;
775
+ /** @internal */
776
+ required = computed(() => this.requiredInput(), ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
777
+ /** @internal */
778
+ controlType = 'combobox';
779
+ /** @internal */
780
+ userAriaDescribedBy = computed(() => this.ariaDescribedby(), ...(ngDevMode ? [{ debugName: "userAriaDescribedBy" }] : /* istanbul ignore next */ []));
781
+ /** @internal */
782
+ userAriaLabelledby = computed(() => this.ariaLabelledby(), ...(ngDevMode ? [{ debugName: "userAriaLabelledby" }] : /* istanbul ignore next */ []));
783
+ // ── Constructor ──
784
+ constructor() {
785
+ // Material-style CVA wiring: declare ourselves as the value accessor on any
786
+ // host-level `NgControl` (FormControlDirective, NgModel, etc.). This avoids
787
+ // the circular-DI that a static `NG_VALUE_ACCESSOR` provider would create
788
+ // because `NgControl` is injected with `self: true` on the same element.
789
+ if (this.ngControl) {
790
+ this.ngControl.valueAccessor = this;
791
+ }
792
+ // Mirror parent `open` model into overlay lifecycle.
793
+ effect(() => {
794
+ const shouldOpen = this.open();
795
+ const disabled = this.isDisabled();
796
+ if (disabled && this.overlayInstance) {
797
+ this.closeOverlay({ silent: false });
798
+ return;
799
+ }
800
+ if (shouldOpen && !this.overlayInstance && !disabled && !this.closingSignal()) {
801
+ this.openOverlay();
802
+ }
803
+ else if (!shouldOpen && this.overlayInstance && !this.closingSignal()) {
804
+ this.closeOverlay({ silent: false });
805
+ }
806
+ });
807
+ // Push state into overlay component whenever anything relevant changes.
808
+ // While the panel is animating out, freeze content updates so list mutations
809
+ // (e.g. the input filtering down after commitOption) don't visually flicker
810
+ // the panel during the leave animation.
811
+ effect(() => {
812
+ const instance = this.overlayInstance;
813
+ if (!instance)
814
+ return;
815
+ if (this.closingSignal())
816
+ return;
817
+ const renderedRows = this.renderedRows();
818
+ const activeIndex = this.activeIndex();
819
+ const panelMaxHeight = this.panelMaxHeight();
820
+ const emptyMessage = this.emptyMessage();
821
+ const query = this.inputValue();
822
+ const loading = this.loading();
823
+ const labelledBy = this.resolvedLabelledBy();
824
+ const optionTemplate = this.optionTemplateChild()?.templateRef;
825
+ const emptyTemplate = this.emptyTemplateChild()?.templateRef;
826
+ const loadingTemplate = this.loadingTemplateChild()?.templateRef;
827
+ const customPanelClass = this.resolvePanelClass();
828
+ untracked(() => {
829
+ instance.renderedRows.set(renderedRows);
830
+ instance.activeIndex.set(activeIndex);
831
+ instance.panelMaxHeight.set(panelMaxHeight);
832
+ instance.emptyMessage.set(emptyMessage);
833
+ instance.query.set(query);
834
+ instance.loading.set(loading);
835
+ instance.labelledBy.set(labelledBy);
836
+ instance.optionTemplate.set(optionTemplate);
837
+ instance.emptyTemplate.set(emptyTemplate);
838
+ instance.loadingTemplate.set(loadingTemplate);
839
+ instance.customPanelClass.set(customPanelClass);
840
+ instance.listboxId.set(this.listboxId);
841
+ instance.optionIdFn.set(this.optionId);
842
+ instance.groupHeaderIdFn.set(this.groupHeaderId);
843
+ instance.isSelected.set((index) => this.isVisibleOptionSelected(index));
844
+ });
845
+ });
846
+ // Late-arriving options reconciler for the async writeValue race.
847
+ effect(() => {
848
+ const opts = this.options();
849
+ const pending = untracked(this.pendingWriteValue);
850
+ if (pending === UNRESOLVED)
851
+ return;
852
+ if (opts.length === 0)
853
+ return;
854
+ const cmp = this.compareWith();
855
+ const valueFn = this.optionValue();
856
+ const labelFn = this.optionLabel();
857
+ const match = opts.find((o) => {
858
+ try {
859
+ return cmp(valueFn(o), pending);
860
+ }
861
+ catch {
862
+ return false;
863
+ }
864
+ });
865
+ untracked(() => {
866
+ if (match) {
867
+ this.inputValue.set(labelFn(match));
868
+ this.lastCommittedLabel.set(labelFn(match));
869
+ }
870
+ // Always clear the sentinel — subsequent options changes won't re-resolve.
871
+ this.pendingWriteValue.set(UNRESOLVED);
872
+ });
873
+ });
874
+ // Debounced LiveAnnouncer result-count announcements.
875
+ effect(() => {
876
+ const visible = this.visibleOptions();
877
+ const query = this.inputValue();
878
+ const isOpen = this.open();
879
+ untracked(() => {
880
+ if (!isOpen)
881
+ return;
882
+ this.clearAnnounceTimer();
883
+ this.announceTimer = setTimeout(() => {
884
+ const count = visible.length;
885
+ const message = count === 0
886
+ ? `No results for ${query}`
887
+ : count === 1
888
+ ? '1 result'
889
+ : `${count} results`;
890
+ this.liveAnnouncer.announce(message, 'polite');
891
+ }, ANNOUNCE_DEBOUNCE);
892
+ });
893
+ });
894
+ // FocusMonitor for blur → onTouched + focused state.
895
+ const monitorSub = this.focusMonitor
896
+ .monitor(this.elementRef, true)
897
+ .subscribe((origin) => {
898
+ const wasFocused = this.focusedSignal();
899
+ this.focusedSignal.set(!!origin);
900
+ if (!origin && wasFocused) {
901
+ this.onTouched();
902
+ // Blur often flips `touched` on the bound `NgControl`; bump the
903
+ // revision so `errorState` recomputes.
904
+ this._ngControlRev.update((n) => n + 1);
905
+ }
906
+ });
907
+ this.destroyRef.onDestroy(() => {
908
+ monitorSub.unsubscribe();
909
+ this.focusMonitor.stopMonitoring(this.elementRef);
910
+ this.clearCloseTimer();
911
+ this.clearQueryDebounceTimer();
912
+ this.clearAnnounceTimer();
913
+ this.resizeObserver?.disconnect();
914
+ this.resizeObserver = null;
915
+ this.overlayRef?.dispose();
916
+ this.overlayRef = null;
917
+ this.overlayInstanceSignal.set(null);
918
+ });
919
+ }
920
+ // ── Public methods ──
921
+ /** Opens the popover. No-op when disabled or already open. */
922
+ openPanel() {
923
+ if (this.isDisabled() || this.open())
924
+ return;
925
+ this.open.set(true);
926
+ }
927
+ /** Closes the popover. No-op when already closed. */
928
+ closePanel() {
929
+ if (!this.open())
930
+ return;
931
+ this.open.set(false);
932
+ }
933
+ /** Programmatically focuses the input. */
934
+ focus() {
935
+ this.inputRef()?.nativeElement.focus();
936
+ }
937
+ // ── Visible-option helpers ──
938
+ /** @internal */
939
+ isVisibleOptionSelected(index) {
940
+ const visible = this.visibleOptions();
941
+ if (index < 0 || index >= visible.length)
942
+ return false;
943
+ const current = this.value();
944
+ if (current === null || current === undefined)
945
+ return false;
946
+ const cmp = this.compareWith();
947
+ try {
948
+ return cmp(visible[index].value, current);
949
+ }
950
+ catch {
951
+ return false;
952
+ }
953
+ }
954
+ // ── Input event handlers ──
955
+ /** @internal */
956
+ onInput(event) {
957
+ if (this.composing()) {
958
+ // Defer; we'll re-run with the final value on compositionend.
959
+ this.composingPendingValue = event.target.value;
960
+ return;
961
+ }
962
+ const target = event.target;
963
+ const value = target.value;
964
+ this.inputValue.set(value);
965
+ this.scheduleQueryEmit(value);
966
+ // Open if not blocked by minQueryLength and not disabled.
967
+ if (!this.isDisabled() && value.length >= this.minQueryLength()) {
968
+ if (!this.open()) {
969
+ this.open.set(true);
970
+ }
971
+ }
972
+ else if (value.length < this.minQueryLength() && this.open()) {
973
+ this.open.set(false);
974
+ }
975
+ }
976
+ /** @internal */
977
+ onCompositionStart() {
978
+ this.composing.set(true);
979
+ }
980
+ /** @internal */
981
+ onCompositionEnd(event) {
982
+ this.composing.set(false);
983
+ const target = event.target;
984
+ const value = target.value || this.composingPendingValue || '';
985
+ this.composingPendingValue = null;
986
+ // Re-run the input handler logic once with the resolved value.
987
+ this.inputValue.set(value);
988
+ this.scheduleQueryEmit(value);
989
+ if (!this.isDisabled() && value.length >= this.minQueryLength() && !this.open()) {
990
+ this.open.set(true);
991
+ }
992
+ }
993
+ /** @internal */
994
+ onFocus() {
995
+ if (this.isDisabled())
996
+ return;
997
+ if (this.openOnFocus() && this.inputValue().length >= this.minQueryLength() && !this.open()) {
998
+ this.open.set(true);
999
+ }
1000
+ }
1001
+ /** @internal */
1002
+ onBlur() {
1003
+ if (this.composing())
1004
+ return;
1005
+ // Don't commit while the user is interacting with the overlay (option mousedown
1006
+ // calls preventDefault to keep focus on the input, so blur here means real focus loss).
1007
+ if (this.isDisabled())
1008
+ return;
1009
+ this.commit('blur');
1010
+ }
1011
+ /** @internal */
1012
+ onKeydown(event) {
1013
+ if (this.composing())
1014
+ return;
1015
+ if (this.isDisabled())
1016
+ return;
1017
+ const key = event.key;
1018
+ // Alt+Arrow combos
1019
+ if (event.altKey && key === 'ArrowDown') {
1020
+ event.preventDefault();
1021
+ this.openPanel();
1022
+ return;
1023
+ }
1024
+ if (event.altKey && key === 'ArrowUp') {
1025
+ event.preventDefault();
1026
+ this.closePanel();
1027
+ return;
1028
+ }
1029
+ switch (key) {
1030
+ case 'ArrowDown': {
1031
+ event.preventDefault();
1032
+ if (!this.open()) {
1033
+ this.openPanel();
1034
+ this.moveActiveTo(this.firstEnabledIndex(), 1);
1035
+ }
1036
+ else {
1037
+ this.moveActive(1);
1038
+ }
1039
+ return;
1040
+ }
1041
+ case 'ArrowUp': {
1042
+ event.preventDefault();
1043
+ if (!this.open()) {
1044
+ this.openPanel();
1045
+ this.moveActiveTo(this.lastEnabledIndex(), -1);
1046
+ }
1047
+ else {
1048
+ this.moveActive(-1);
1049
+ }
1050
+ return;
1051
+ }
1052
+ case 'Home': {
1053
+ if (this.inputValue().length === 0 || event.altKey) {
1054
+ event.preventDefault();
1055
+ if (!this.open())
1056
+ this.openPanel();
1057
+ this.moveActiveTo(0, 1);
1058
+ }
1059
+ return;
1060
+ }
1061
+ case 'End': {
1062
+ if (this.inputValue().length === 0 || event.altKey) {
1063
+ event.preventDefault();
1064
+ if (!this.open())
1065
+ this.openPanel();
1066
+ this.moveActiveTo(this.visibleOptions().length - 1, -1);
1067
+ }
1068
+ return;
1069
+ }
1070
+ case 'Enter': {
1071
+ const active = this.activeOption();
1072
+ if (this.open() && active && !active.disabled) {
1073
+ event.preventDefault();
1074
+ event.stopPropagation();
1075
+ this.commitOption(active, 'option');
1076
+ return;
1077
+ }
1078
+ // Form-submit passthrough: do NOT call preventDefault.
1079
+ return;
1080
+ }
1081
+ case 'Escape': {
1082
+ if (this.open()) {
1083
+ event.preventDefault();
1084
+ event.stopPropagation();
1085
+ this.inputValue.set(this.lastCommittedLabel());
1086
+ this.closePanel();
1087
+ }
1088
+ else if (this.clearable() && this.inputValue().length > 0) {
1089
+ event.preventDefault();
1090
+ this.clear();
1091
+ }
1092
+ return;
1093
+ }
1094
+ case 'Tab': {
1095
+ // Commit (no preventDefault) so focus moves naturally.
1096
+ this.commit('tab');
1097
+ return;
1098
+ }
1099
+ case 'Backspace': {
1100
+ // Explicit no-op on empty input — do not close the popover.
1101
+ if (this.inputValue().length === 0) {
1102
+ event.preventDefault();
1103
+ }
1104
+ return;
1105
+ }
1106
+ default:
1107
+ return;
1108
+ }
1109
+ }
1110
+ // ── Clear button ──
1111
+ /** @internal — preventDefault to keep DOM focus on the input. */
1112
+ onClearMousedown(event) {
1113
+ event.preventDefault();
1114
+ event.stopPropagation();
1115
+ }
1116
+ /** @internal */
1117
+ onClearClick(event) {
1118
+ event.preventDefault();
1119
+ event.stopPropagation();
1120
+ this.clear();
1121
+ this.inputRef()?.nativeElement.focus();
1122
+ }
1123
+ /** Clears the input and committed value, emitting `valueCommit({ source: 'reset' })`. */
1124
+ clear() {
1125
+ if (this.isDisabled())
1126
+ return;
1127
+ this.value.set(null);
1128
+ this.inputValue.set('');
1129
+ this.lastCommittedLabel.set('');
1130
+ this.valueCommit.emit({ value: null, source: 'reset' });
1131
+ this.onChange(null);
1132
+ this.closePanel();
1133
+ }
1134
+ // ── Active-index navigation ──
1135
+ moveActive(delta) {
1136
+ const visible = this.visibleOptions();
1137
+ if (visible.length === 0) {
1138
+ this.activeIndex.set(-1);
1139
+ return;
1140
+ }
1141
+ const n = visible.length;
1142
+ let idx = this.activeIndex();
1143
+ if (idx < 0)
1144
+ idx = delta > 0 ? -1 : n;
1145
+ for (let i = 0; i < n; i++) {
1146
+ idx = (idx + delta + n) % n;
1147
+ if (!visible[idx].disabled) {
1148
+ this.activeIndex.set(idx);
1149
+ this.scrollActiveIntoView();
1150
+ return;
1151
+ }
1152
+ }
1153
+ }
1154
+ moveActiveTo(startIndex, direction) {
1155
+ const visible = this.visibleOptions();
1156
+ if (visible.length === 0) {
1157
+ this.activeIndex.set(-1);
1158
+ return;
1159
+ }
1160
+ let i = Math.max(0, Math.min(startIndex, visible.length - 1));
1161
+ const max = visible.length;
1162
+ for (let n = 0; n < max; n++) {
1163
+ if (!visible[i].disabled) {
1164
+ this.activeIndex.set(i);
1165
+ this.scrollActiveIntoView();
1166
+ return;
1167
+ }
1168
+ i += direction;
1169
+ if (i < 0 || i >= visible.length)
1170
+ return;
1171
+ }
1172
+ }
1173
+ scrollActiveIntoView() {
1174
+ if (!this.platform.isBrowser)
1175
+ return;
1176
+ queueMicrotask(() => {
1177
+ const id = this.activeOptionId();
1178
+ if (!id)
1179
+ return;
1180
+ const el = document.getElementById(id);
1181
+ if (el && typeof el.scrollIntoView === 'function') {
1182
+ el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
1183
+ }
1184
+ });
1185
+ }
1186
+ // ── Commit resolver ──
1187
+ /** @internal */
1188
+ commit(source) {
1189
+ if (this.isDisabled() || this.composing())
1190
+ return;
1191
+ const rawText = this.inputValue();
1192
+ const text = rawText.trim();
1193
+ const active = this.activeOption();
1194
+ const labelFn = this.optionLabel();
1195
+ const disabledFn = this.optionDisabled();
1196
+ // 1. Active option wins.
1197
+ if (active && !active.disabled) {
1198
+ this.commitOption(active, 'option');
1199
+ return;
1200
+ }
1201
+ // 2. Exact label match auto-resolves.
1202
+ const exact = this.options().find((o) => !disabledFn(o) && labelFn(o).trim() === text && text.length > 0);
1203
+ if (exact) {
1204
+ const valueFn = this.optionValue();
1205
+ const groupFn = this.optionGroup();
1206
+ const descFn = this.optionDescription();
1207
+ this.commitOption({
1208
+ option: exact,
1209
+ label: labelFn(exact),
1210
+ value: valueFn(exact),
1211
+ disabled: false,
1212
+ group: groupFn(exact),
1213
+ description: descFn(exact),
1214
+ }, 'option');
1215
+ return;
1216
+ }
1217
+ // 3. Empty text → null.
1218
+ if (text === '') {
1219
+ const wasNonNull = this.value() !== null || this.inputValue() !== '';
1220
+ this.value.set(null);
1221
+ this.inputValue.set('');
1222
+ this.lastCommittedLabel.set('');
1223
+ if (wasNonNull) {
1224
+ this.valueCommit.emit({ value: null, source: 'reset' });
1225
+ this.onChange(null);
1226
+ }
1227
+ this.closePanel();
1228
+ return;
1229
+ }
1230
+ // 4. No match.
1231
+ if (this.strict()) {
1232
+ this.inputValue.set(this.lastCommittedLabel());
1233
+ if (source !== 'enter') {
1234
+ this.closePanel();
1235
+ }
1236
+ else if (this.visibleOptions().length === 0) {
1237
+ this.closePanel();
1238
+ }
1239
+ return;
1240
+ }
1241
+ // Free-text commit.
1242
+ this.value.set(text);
1243
+ this.inputValue.set(text);
1244
+ this.lastCommittedLabel.set(text);
1245
+ this.valueCommit.emit({ value: text, source: 'free-text' });
1246
+ this.onChange(text);
1247
+ this.closePanel();
1248
+ }
1249
+ /** @internal */
1250
+ commitOption(option, source) {
1251
+ const value = option.value;
1252
+ const label = option.label;
1253
+ this.value.set(value);
1254
+ this.inputValue.set(label);
1255
+ this.lastCommittedLabel.set(label);
1256
+ this.optionSelected.emit({ option: option.option, value, label });
1257
+ this.valueCommit.emit({ value, source });
1258
+ this.onChange(value);
1259
+ this.liveAnnouncer.announce(`${label} selected`, 'polite');
1260
+ this.closePanel();
1261
+ }
1262
+ // ── Query debounce ──
1263
+ scheduleQueryEmit(value) {
1264
+ this.clearQueryDebounceTimer();
1265
+ const delay = this.queryDebounce();
1266
+ this.queryDebounceTimer = setTimeout(() => {
1267
+ this.queryDebounceTimer = null;
1268
+ this.queryChange.emit(value);
1269
+ }, delay);
1270
+ }
1271
+ clearQueryDebounceTimer() {
1272
+ if (this.queryDebounceTimer !== null) {
1273
+ clearTimeout(this.queryDebounceTimer);
1274
+ this.queryDebounceTimer = null;
1275
+ }
1276
+ }
1277
+ clearAnnounceTimer() {
1278
+ if (this.announceTimer !== null) {
1279
+ clearTimeout(this.announceTimer);
1280
+ this.announceTimer = null;
1281
+ }
1282
+ }
1283
+ // ── Overlay lifecycle ──
1284
+ openOverlay() {
1285
+ this.ensureOverlay();
1286
+ this.attachOverlayComponent();
1287
+ this.subscribeBackdrop();
1288
+ this.subscribeOverlayEscape();
1289
+ // Reset active to first enabled when opening.
1290
+ this.activeIndex.set(this.firstEnabledIndex());
1291
+ this.liveAnnouncer.announce(`${this.visibleOptions().length} suggestions available`, 'polite');
1292
+ this.openedChange.emit({
1293
+ open: true,
1294
+ trigger: this.inputRef()?.nativeElement ?? this.elementRef.nativeElement,
1295
+ });
1296
+ }
1297
+ closeOverlay(_opts) {
1298
+ if (this.closingSignal() || !this.overlayInstance)
1299
+ return;
1300
+ this.closingSignal.set(true);
1301
+ this.clearAnnounceTimer();
1302
+ this.closeTimer = setTimeout(() => {
1303
+ this.closeTimer = null;
1304
+ if (this.overlayRef?.hasAttached()) {
1305
+ this.overlayRef.detach();
1306
+ }
1307
+ this.overlayInstanceSignal.set(null);
1308
+ this.resizeObserver?.disconnect();
1309
+ this.resizeObserver = null;
1310
+ untracked(() => this.open.set(false));
1311
+ this.closingSignal.set(false);
1312
+ this.openedChange.emit({
1313
+ open: false,
1314
+ trigger: this.inputRef()?.nativeElement ?? this.elementRef.nativeElement,
1315
+ });
1316
+ }, ANIMATION_DURATION);
1317
+ }
1318
+ ensureOverlay() {
1319
+ if (this.overlayRef) {
1320
+ this.updateOverlaySize();
1321
+ return;
1322
+ }
1323
+ const positionStrategy = this.overlayService
1324
+ .position()
1325
+ .flexibleConnectedTo(this.triggerSurfaceRef()?.nativeElement ?? this.elementRef.nativeElement)
1326
+ .withPositions(buildSelectLikePositions(this.offset()))
1327
+ .withFlexibleDimensions(false)
1328
+ .withPush(false)
1329
+ .withViewportMargin(8);
1330
+ this.overlayRef = this.overlayService.create({
1331
+ positionStrategy,
1332
+ scrollStrategy: resolveSelectScrollStrategy(this.scrollStrategy(), this.overlayService),
1333
+ hasBackdrop: true,
1334
+ backdropClass: 'cdk-overlay-transparent-backdrop',
1335
+ panelClass: 'tw-combobox-panel',
1336
+ });
1337
+ this.installResizeObserver();
1338
+ this.updateOverlaySize();
1339
+ }
1340
+ installResizeObserver() {
1341
+ if (!this.platform.isBrowser || typeof ResizeObserver === 'undefined')
1342
+ return;
1343
+ const target = this.elementRef.nativeElement;
1344
+ this.resizeObserver?.disconnect();
1345
+ this.resizeObserver = new ResizeObserver(() => this.updateOverlaySize());
1346
+ this.resizeObserver.observe(target);
1347
+ }
1348
+ updateOverlaySize() {
1349
+ if (!this.overlayRef)
1350
+ return;
1351
+ const width = this.panelWidth();
1352
+ if (width === 'trigger') {
1353
+ const anchor = this.triggerSurfaceRef()?.nativeElement ?? this.elementRef.nativeElement;
1354
+ const rect = anchor.getBoundingClientRect();
1355
+ this.overlayRef.updateSize({ width: rect.width });
1356
+ }
1357
+ else if (width === 'auto') {
1358
+ this.overlayRef.updateSize({ width: undefined, minWidth: undefined });
1359
+ }
1360
+ else if (typeof width === 'number') {
1361
+ this.overlayRef.updateSize({ width });
1362
+ }
1363
+ else {
1364
+ this.overlayRef.updateSize({ width });
1365
+ }
1366
+ }
1367
+ attachOverlayComponent() {
1368
+ if (!this.overlayRef)
1369
+ return;
1370
+ const portal = new ComponentPortal(ComboboxOverlayComponent, this.viewContainerRef, this.injector);
1371
+ const ref = this.overlayRef.attach(portal);
1372
+ const instance = ref.instance;
1373
+ instance.onOptionPick.set((index) => {
1374
+ const visible = this.visibleOptions();
1375
+ if (index < 0 || index >= visible.length)
1376
+ return;
1377
+ const opt = visible[index];
1378
+ if (opt.disabled)
1379
+ return;
1380
+ this.commitOption(opt, 'option');
1381
+ });
1382
+ instance.onOptionHover.set((index) => {
1383
+ const visible = this.visibleOptions();
1384
+ if (index < 0 || index >= visible.length)
1385
+ return;
1386
+ if (visible[index].disabled)
1387
+ return;
1388
+ this.activeIndex.set(index);
1389
+ });
1390
+ this.overlayInstanceSignal.set(instance);
1391
+ }
1392
+ subscribeBackdrop() {
1393
+ if (!this.overlayRef)
1394
+ return;
1395
+ const sub = this.overlayRef.backdropClick().subscribe(() => {
1396
+ this.closePanel();
1397
+ });
1398
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
1399
+ }
1400
+ /**
1401
+ * Listens for `Escape` on the overlay's `keydownEvents()`. Mirrors
1402
+ * `SelectComponent` so focus inside the overlay panel (e.g., if the user
1403
+ * tabs into projected loading/empty templates) still dismisses cleanly.
1404
+ * The combobox's own `<input>` keydown handler already short-circuits
1405
+ * Escape when focus is on the input; this layer is the safety net for
1406
+ * panel-internal focus.
1407
+ */
1408
+ subscribeOverlayEscape() {
1409
+ if (!this.overlayRef)
1410
+ return;
1411
+ const teardown = consumeOverlayEscape(this.overlayRef, (event) => {
1412
+ event.preventDefault();
1413
+ this.inputValue.set(this.lastCommittedLabel());
1414
+ this.closePanel();
1415
+ });
1416
+ this.destroyRef.onDestroy(() => teardown());
1417
+ }
1418
+ resolvePanelClass() {
1419
+ const raw = this.panelClass();
1420
+ return Array.isArray(raw) ? raw.join(' ') : raw;
1421
+ }
1422
+ clearCloseTimer() {
1423
+ if (this.closeTimer !== null) {
1424
+ clearTimeout(this.closeTimer);
1425
+ this.closeTimer = null;
1426
+ }
1427
+ }
1428
+ // ── ControlValueAccessor ──
1429
+ writeValue(value) {
1430
+ if (value === null || value === undefined) {
1431
+ this.value.set(null);
1432
+ this.inputValue.set('');
1433
+ this.lastCommittedLabel.set('');
1434
+ this.pendingWriteValue.set(UNRESOLVED);
1435
+ this.valueCommit.emit({ value: null, source: 'programmatic' });
1436
+ return;
1437
+ }
1438
+ // Store verbatim.
1439
+ this.value.set(value);
1440
+ const cmp = this.compareWith();
1441
+ const valueFn = this.optionValue();
1442
+ const labelFn = this.optionLabel();
1443
+ const match = this.options().find((o) => {
1444
+ try {
1445
+ return cmp(valueFn(o), value);
1446
+ }
1447
+ catch {
1448
+ return false;
1449
+ }
1450
+ });
1451
+ if (match) {
1452
+ const label = labelFn(match);
1453
+ this.inputValue.set(label);
1454
+ this.lastCommittedLabel.set(label);
1455
+ this.pendingWriteValue.set(UNRESOLVED);
1456
+ }
1457
+ else {
1458
+ const label = String(value);
1459
+ this.inputValue.set(label);
1460
+ this.lastCommittedLabel.set(label);
1461
+ this.pendingWriteValue.set(value);
1462
+ }
1463
+ this.valueCommit.emit({ value, source: 'programmatic' });
1464
+ }
1465
+ registerOnChange(fn) {
1466
+ this.onChange = fn;
1467
+ }
1468
+ registerOnTouched(fn) {
1469
+ this.onTouched = fn;
1470
+ }
1471
+ setDisabledState(isDisabled) {
1472
+ this.cvaDisabled.set(isDisabled);
1473
+ if (isDisabled && this.open()) {
1474
+ this.closePanel();
1475
+ }
1476
+ }
1477
+ // ── FormFieldControl methods ──
1478
+ /** @internal */
1479
+ setDescribedByIds(ids) {
1480
+ this.describedByIdsSignal.set([...ids]);
1481
+ }
1482
+ /** @internal */
1483
+ setLabelledByIds(ids) {
1484
+ this.labelledByIdsSignal.set([...ids]);
1485
+ }
1486
+ /** @internal */
1487
+ onContainerClick(_event) {
1488
+ if (this.isDisabled())
1489
+ return;
1490
+ this.inputRef()?.nativeElement.focus();
1491
+ }
1492
+ ngOnInit() {
1493
+ // NgControl's `control` is set by the parent FormControl* directive before
1494
+ // children's `ngOnInit`. Subscribe here so errorState reacts to status/value
1495
+ // changes on the bound control.
1496
+ const ctrl = this.ngControl?.control;
1497
+ if (ctrl) {
1498
+ const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
1499
+ if (streams.length) {
1500
+ merge(...streams)
1501
+ .pipe(takeUntilDestroyed(this.destroyRef))
1502
+ .subscribe(() => this._ngControlRev.update((n) => n + 1));
1503
+ }
1504
+ }
1505
+ const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
1506
+ if (submit) {
1507
+ submit
1508
+ .pipe(takeUntilDestroyed(this.destroyRef))
1509
+ .subscribe(() => this._formSubmitRev.update((n) => n + 1));
1510
+ }
1511
+ }
1512
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1513
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ComboboxComponent, isStandalone: true, selector: "tw-combobox", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, optionGroup: { classPropertyName: "optionGroup", publicName: "optionGroup", isSignal: true, isRequired: false, transformFunction: null }, optionDescription: { classPropertyName: "optionDescription", publicName: "optionDescription", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, strict: { classPropertyName: "strict", publicName: "strict", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, showChevron: { classPropertyName: "showChevron", publicName: "showChevron", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, queryDebounce: { classPropertyName: "queryDebounce", publicName: "queryDebounce", isSignal: true, isRequired: false, transformFunction: null }, minQueryLength: { classPropertyName: "minQueryLength", publicName: "minQueryLength", isSignal: true, isRequired: false, transformFunction: null }, openOnFocus: { classPropertyName: "openOnFocus", publicName: "openOnFocus", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, scrollStrategy: { classPropertyName: "scrollStrategy", publicName: "scrollStrategy", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, inputValue: { classPropertyName: "inputValue", publicName: "inputValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", queryChange: "queryChange", optionSelected: "optionSelected", valueCommit: "valueCommit", openedChange: "openedChange" }, host: { properties: { "class": "rootClasses()" } }, providers: [
1514
+ {
1515
+ provide: TW_FORM_FIELD_CONTROL,
1516
+ useExisting: forwardRef(() => ComboboxComponent),
1517
+ },
1518
+ ], queries: [{ propertyName: "optionTemplateChild", first: true, predicate: ComboboxOptionTemplateDirective, descendants: true, isSignal: true }, { propertyName: "emptyTemplateChild", first: true, predicate: ComboboxEmptyTemplateDirective, descendants: true, isSignal: true }, { propertyName: "loadingTemplateChild", first: true, predicate: ComboboxLoadingTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "triggerSurfaceRef", first: true, predicate: ["triggerSurface"], descendants: true, isSignal: true }], ngImport: i0, template: `
1519
+ <div #triggerSurface [class]="triggerClasses()">
1520
+ <ng-content select="[twComboboxPrefix]" />
1521
+
1522
+ <input
1523
+ #inputEl
1524
+ type="text"
1525
+ [id]="hostId"
1526
+ [class]="inputClasses()"
1527
+ role="combobox"
1528
+ aria-autocomplete="list"
1529
+ aria-haspopup="listbox"
1530
+ [attr.aria-expanded]="open() ? 'true' : 'false'"
1531
+ [attr.aria-controls]="open() ? listboxId : null"
1532
+ [attr.aria-activedescendant]="open() ? (activeOptionId() || null) : null"
1533
+ [attr.aria-required]="requiredInput() ? 'true' : null"
1534
+ [attr.aria-invalid]="errorState() ? 'true' : null"
1535
+ [attr.aria-label]="ariaLabel() || null"
1536
+ [attr.aria-labelledby]="resolvedLabelledBy() || null"
1537
+ [attr.aria-describedby]="resolvedDescribedBy() || null"
1538
+ [attr.aria-disabled]="isDisabled() ? 'true' : null"
1539
+ [disabled]="isDisabled()"
1540
+ [placeholder]="placeholder() ?? null"
1541
+ [value]="inputValue()"
1542
+ (input)="onInput($event)"
1543
+ (keydown)="onKeydown($event)"
1544
+ (compositionstart)="onCompositionStart()"
1545
+ (compositionend)="onCompositionEnd($event)"
1546
+ (focus)="onFocus()"
1547
+ (blur)="onBlur()"
1548
+ />
1549
+
1550
+ <ng-content select="[twComboboxSuffix]" />
1551
+
1552
+ @if (showSpinner()) {
1553
+ <svg
1554
+ viewBox="0 0 24 24"
1555
+ fill="none"
1556
+ aria-hidden="true"
1557
+ [class]="spinnerClasses()"
1558
+ >
1559
+ <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" opacity="0.25" />
1560
+ <path
1561
+ d="M21 12a9 9 0 0 0-9-9"
1562
+ stroke="currentColor"
1563
+ stroke-width="3"
1564
+ stroke-linecap="round"
1565
+ />
1566
+ </svg>
1567
+ }
1568
+
1569
+ @if (showClearButton()) {
1570
+ <button
1571
+ type="button"
1572
+ tabindex="-1"
1573
+ [class]="clearButtonClasses()"
1574
+ aria-label="Clear"
1575
+ (mousedown)="onClearMousedown($event)"
1576
+ (click)="onClearClick($event)"
1577
+ >
1578
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
1579
+ <path
1580
+ fill-rule="evenodd"
1581
+ d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
1582
+ clip-rule="evenodd"
1583
+ />
1584
+ </svg>
1585
+ </button>
1586
+ }
1587
+
1588
+ @if (showChevron()) {
1589
+ <svg
1590
+ viewBox="0 0 20 20"
1591
+ fill="currentColor"
1592
+ aria-hidden="true"
1593
+ [class]="chevronClasses()"
1594
+ >
1595
+ <path
1596
+ fill-rule="evenodd"
1597
+ d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z"
1598
+ clip-rule="evenodd"
1599
+ />
1600
+ </svg>
1601
+ }
1602
+ </div>
1603
+
1604
+ <span hidden>
1605
+ <ng-content />
1606
+ </span>
1607
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1608
+ }
1609
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComboboxComponent, decorators: [{
1610
+ type: Component,
1611
+ args: [{
1612
+ selector: 'tw-combobox',
1613
+ changeDetection: ChangeDetectionStrategy.OnPush,
1614
+ providers: [
1615
+ {
1616
+ provide: TW_FORM_FIELD_CONTROL,
1617
+ useExisting: forwardRef(() => ComboboxComponent),
1618
+ },
1619
+ ],
1620
+ template: `
1621
+ <div #triggerSurface [class]="triggerClasses()">
1622
+ <ng-content select="[twComboboxPrefix]" />
1623
+
1624
+ <input
1625
+ #inputEl
1626
+ type="text"
1627
+ [id]="hostId"
1628
+ [class]="inputClasses()"
1629
+ role="combobox"
1630
+ aria-autocomplete="list"
1631
+ aria-haspopup="listbox"
1632
+ [attr.aria-expanded]="open() ? 'true' : 'false'"
1633
+ [attr.aria-controls]="open() ? listboxId : null"
1634
+ [attr.aria-activedescendant]="open() ? (activeOptionId() || null) : null"
1635
+ [attr.aria-required]="requiredInput() ? 'true' : null"
1636
+ [attr.aria-invalid]="errorState() ? 'true' : null"
1637
+ [attr.aria-label]="ariaLabel() || null"
1638
+ [attr.aria-labelledby]="resolvedLabelledBy() || null"
1639
+ [attr.aria-describedby]="resolvedDescribedBy() || null"
1640
+ [attr.aria-disabled]="isDisabled() ? 'true' : null"
1641
+ [disabled]="isDisabled()"
1642
+ [placeholder]="placeholder() ?? null"
1643
+ [value]="inputValue()"
1644
+ (input)="onInput($event)"
1645
+ (keydown)="onKeydown($event)"
1646
+ (compositionstart)="onCompositionStart()"
1647
+ (compositionend)="onCompositionEnd($event)"
1648
+ (focus)="onFocus()"
1649
+ (blur)="onBlur()"
1650
+ />
1651
+
1652
+ <ng-content select="[twComboboxSuffix]" />
1653
+
1654
+ @if (showSpinner()) {
1655
+ <svg
1656
+ viewBox="0 0 24 24"
1657
+ fill="none"
1658
+ aria-hidden="true"
1659
+ [class]="spinnerClasses()"
1660
+ >
1661
+ <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" opacity="0.25" />
1662
+ <path
1663
+ d="M21 12a9 9 0 0 0-9-9"
1664
+ stroke="currentColor"
1665
+ stroke-width="3"
1666
+ stroke-linecap="round"
1667
+ />
1668
+ </svg>
1669
+ }
1670
+
1671
+ @if (showClearButton()) {
1672
+ <button
1673
+ type="button"
1674
+ tabindex="-1"
1675
+ [class]="clearButtonClasses()"
1676
+ aria-label="Clear"
1677
+ (mousedown)="onClearMousedown($event)"
1678
+ (click)="onClearClick($event)"
1679
+ >
1680
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
1681
+ <path
1682
+ fill-rule="evenodd"
1683
+ d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
1684
+ clip-rule="evenodd"
1685
+ />
1686
+ </svg>
1687
+ </button>
1688
+ }
1689
+
1690
+ @if (showChevron()) {
1691
+ <svg
1692
+ viewBox="0 0 20 20"
1693
+ fill="currentColor"
1694
+ aria-hidden="true"
1695
+ [class]="chevronClasses()"
1696
+ >
1697
+ <path
1698
+ fill-rule="evenodd"
1699
+ d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z"
1700
+ clip-rule="evenodd"
1701
+ />
1702
+ </svg>
1703
+ }
1704
+ </div>
1705
+
1706
+ <span hidden>
1707
+ <ng-content />
1708
+ </span>
1709
+ `,
1710
+ host: {
1711
+ '[class]': 'rootClasses()',
1712
+ },
1713
+ }]
1714
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], optionGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroup", required: false }] }], optionDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDescription", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], strict: [{ type: i0.Input, args: [{ isSignal: true, alias: "strict", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], showChevron: [{ type: i0.Input, args: [{ isSignal: true, alias: "showChevron", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], queryDebounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryDebounce", required: false }] }], minQueryLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minQueryLength", required: false }] }], openOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnFocus", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], scrollStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollStrategy", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], valueCommit: [{ type: i0.Output, args: ["valueCommit"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], optionTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComboboxOptionTemplateDirective), { isSignal: true }] }], emptyTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComboboxEmptyTemplateDirective), { isSignal: true }] }], loadingTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComboboxLoadingTemplateDirective), { isSignal: true }] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], triggerSurfaceRef: [{ type: i0.ViewChild, args: ['triggerSurface', { isSignal: true }] }] } });
1715
+
1716
+ /**
1717
+ * Generated bundle index. Do not edit.
1718
+ */
1719
+
1720
+ export { ComboboxComponent, ComboboxEmptyTemplateDirective, ComboboxLoadingTemplateDirective, ComboboxOptionTemplateDirective, ComboboxPrefixDirective, ComboboxSuffixDirective };
1721
+ //# sourceMappingURL=cdevhub-ngx-tw-combobox.mjs.map