@raintonic/formaui 0.4.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +80 -35
  2. package/README.md +22 -26
  3. package/fesm2022/raintonic-formaui-cdk-drag-drop.mjs +39 -41
  4. package/fesm2022/raintonic-formaui-cdk-drag-drop.mjs.map +1 -1
  5. package/fesm2022/raintonic-formaui-cdk-form-field.mjs +207 -3
  6. package/fesm2022/raintonic-formaui-cdk-form-field.mjs.map +1 -1
  7. package/fesm2022/raintonic-formaui-cdk-overlay.mjs +19 -1
  8. package/fesm2022/raintonic-formaui-cdk-overlay.mjs.map +1 -1
  9. package/fesm2022/raintonic-formaui-cdk-virtual-scroll.mjs +5 -12
  10. package/fesm2022/raintonic-formaui-cdk-virtual-scroll.mjs.map +1 -1
  11. package/fesm2022/raintonic-formaui-components-accordion.mjs +8 -5
  12. package/fesm2022/raintonic-formaui-components-accordion.mjs.map +1 -1
  13. package/fesm2022/raintonic-formaui-components-alert.mjs +16 -2
  14. package/fesm2022/raintonic-formaui-components-alert.mjs.map +1 -1
  15. package/fesm2022/raintonic-formaui-components-autocomplete.mjs +255 -462
  16. package/fesm2022/raintonic-formaui-components-autocomplete.mjs.map +1 -1
  17. package/fesm2022/raintonic-formaui-components-avatar.mjs +34 -59
  18. package/fesm2022/raintonic-formaui-components-avatar.mjs.map +1 -1
  19. package/fesm2022/raintonic-formaui-components-badge.mjs +2 -2
  20. package/fesm2022/raintonic-formaui-components-badge.mjs.map +1 -1
  21. package/fesm2022/raintonic-formaui-components-breadcrumb.mjs +4 -4
  22. package/fesm2022/raintonic-formaui-components-breadcrumb.mjs.map +1 -1
  23. package/fesm2022/raintonic-formaui-components-button-group.mjs +2 -2
  24. package/fesm2022/raintonic-formaui-components-button-group.mjs.map +1 -1
  25. package/fesm2022/raintonic-formaui-components-button.mjs +15 -20
  26. package/fesm2022/raintonic-formaui-components-button.mjs.map +1 -1
  27. package/fesm2022/raintonic-formaui-components-card.mjs +2 -2
  28. package/fesm2022/raintonic-formaui-components-card.mjs.map +1 -1
  29. package/fesm2022/raintonic-formaui-components-checkbox.mjs +2 -2
  30. package/fesm2022/raintonic-formaui-components-checkbox.mjs.map +1 -1
  31. package/fesm2022/raintonic-formaui-components-chip.mjs +97 -0
  32. package/fesm2022/raintonic-formaui-components-chip.mjs.map +1 -0
  33. package/fesm2022/raintonic-formaui-components-data-table.mjs +69 -29
  34. package/fesm2022/raintonic-formaui-components-data-table.mjs.map +1 -1
  35. package/fesm2022/raintonic-formaui-components-date-picker.mjs +223 -144
  36. package/fesm2022/raintonic-formaui-components-date-picker.mjs.map +1 -1
  37. package/fesm2022/raintonic-formaui-components-divider.mjs +2 -2
  38. package/fesm2022/raintonic-formaui-components-divider.mjs.map +1 -1
  39. package/fesm2022/raintonic-formaui-components-drawer.mjs +2 -2
  40. package/fesm2022/raintonic-formaui-components-drawer.mjs.map +1 -1
  41. package/fesm2022/raintonic-formaui-components-dropdown-menu.mjs +888 -0
  42. package/fesm2022/raintonic-formaui-components-dropdown-menu.mjs.map +1 -0
  43. package/fesm2022/raintonic-formaui-components-dual-tier-navigation.mjs +774 -0
  44. package/fesm2022/raintonic-formaui-components-dual-tier-navigation.mjs.map +1 -0
  45. package/fesm2022/raintonic-formaui-components-empty-state.mjs +2 -2
  46. package/fesm2022/raintonic-formaui-components-empty-state.mjs.map +1 -1
  47. package/fesm2022/raintonic-formaui-components-file-upload.mjs +2 -2
  48. package/fesm2022/raintonic-formaui-components-file-upload.mjs.map +1 -1
  49. package/fesm2022/raintonic-formaui-components-form-field.mjs +81 -50
  50. package/fesm2022/raintonic-formaui-components-form-field.mjs.map +1 -1
  51. package/fesm2022/raintonic-formaui-components-icon.mjs +2 -2
  52. package/fesm2022/raintonic-formaui-components-icon.mjs.map +1 -1
  53. package/fesm2022/raintonic-formaui-components-input.mjs +47 -12
  54. package/fesm2022/raintonic-formaui-components-input.mjs.map +1 -1
  55. package/fesm2022/raintonic-formaui-components-list.mjs +4 -4
  56. package/fesm2022/raintonic-formaui-components-list.mjs.map +1 -1
  57. package/fesm2022/raintonic-formaui-components-number-input.mjs +20 -12
  58. package/fesm2022/raintonic-formaui-components-number-input.mjs.map +1 -1
  59. package/fesm2022/raintonic-formaui-components-paginator.mjs +2 -2
  60. package/fesm2022/raintonic-formaui-components-paginator.mjs.map +1 -1
  61. package/fesm2022/raintonic-formaui-components-password-input.mjs +35 -110
  62. package/fesm2022/raintonic-formaui-components-password-input.mjs.map +1 -1
  63. package/fesm2022/raintonic-formaui-components-popover.mjs +3 -2
  64. package/fesm2022/raintonic-formaui-components-popover.mjs.map +1 -1
  65. package/fesm2022/raintonic-formaui-components-progressbar.mjs +3 -2
  66. package/fesm2022/raintonic-formaui-components-progressbar.mjs.map +1 -1
  67. package/fesm2022/raintonic-formaui-components-radio.mjs +5 -6
  68. package/fesm2022/raintonic-formaui-components-radio.mjs.map +1 -1
  69. package/fesm2022/raintonic-formaui-components-select.mjs +257 -412
  70. package/fesm2022/raintonic-formaui-components-select.mjs.map +1 -1
  71. package/fesm2022/raintonic-formaui-components-side-panel.mjs +2 -2
  72. package/fesm2022/raintonic-formaui-components-side-panel.mjs.map +1 -1
  73. package/fesm2022/raintonic-formaui-components-sidebar-nav-menu.mjs +525 -0
  74. package/fesm2022/raintonic-formaui-components-sidebar-nav-menu.mjs.map +1 -0
  75. package/fesm2022/raintonic-formaui-components-skeleton.mjs +2 -2
  76. package/fesm2022/raintonic-formaui-components-skeleton.mjs.map +1 -1
  77. package/fesm2022/raintonic-formaui-components-slider.mjs +2 -2
  78. package/fesm2022/raintonic-formaui-components-slider.mjs.map +1 -1
  79. package/fesm2022/raintonic-formaui-components-spinner.mjs +2 -2
  80. package/fesm2022/raintonic-formaui-components-spinner.mjs.map +1 -1
  81. package/fesm2022/raintonic-formaui-components-stepper.mjs +50 -45
  82. package/fesm2022/raintonic-formaui-components-stepper.mjs.map +1 -1
  83. package/fesm2022/raintonic-formaui-components-strength-meter.mjs +149 -0
  84. package/fesm2022/raintonic-formaui-components-strength-meter.mjs.map +1 -0
  85. package/fesm2022/raintonic-formaui-components-tab.mjs +2 -2
  86. package/fesm2022/raintonic-formaui-components-tab.mjs.map +1 -1
  87. package/fesm2022/raintonic-formaui-components-time-picker.mjs +194 -154
  88. package/fesm2022/raintonic-formaui-components-time-picker.mjs.map +1 -1
  89. package/fesm2022/raintonic-formaui-components-toggle-group.mjs +302 -0
  90. package/fesm2022/raintonic-formaui-components-toggle-group.mjs.map +1 -0
  91. package/fesm2022/raintonic-formaui-components-toggle.mjs +2 -2
  92. package/fesm2022/raintonic-formaui-components-toggle.mjs.map +1 -1
  93. package/fesm2022/raintonic-formaui-components-toolbar.mjs +2 -2
  94. package/fesm2022/raintonic-formaui-components-toolbar.mjs.map +1 -1
  95. package/fesm2022/raintonic-formaui-components-tooltip.mjs +10 -4
  96. package/fesm2022/raintonic-formaui-components-tooltip.mjs.map +1 -1
  97. package/fesm2022/raintonic-formaui-components-topbar.mjs +60 -0
  98. package/fesm2022/raintonic-formaui-components-topbar.mjs.map +1 -0
  99. package/fesm2022/raintonic-formaui-components-tree-select.mjs +59 -69
  100. package/fesm2022/raintonic-formaui-components-tree-select.mjs.map +1 -1
  101. package/fesm2022/raintonic-formaui-components-tree-table.mjs +2 -2
  102. package/fesm2022/raintonic-formaui-components-tree-table.mjs.map +1 -1
  103. package/fesm2022/raintonic-formaui-components-tree.mjs +31 -5
  104. package/fesm2022/raintonic-formaui-components-tree.mjs.map +1 -1
  105. package/fesm2022/raintonic-formaui-core.mjs +279 -1
  106. package/fesm2022/raintonic-formaui-core.mjs.map +1 -1
  107. package/fesm2022/raintonic-formaui-services-breakpoint.mjs +93 -0
  108. package/fesm2022/raintonic-formaui-services-breakpoint.mjs.map +1 -0
  109. package/fesm2022/raintonic-formaui-services-dialog.mjs +314 -16
  110. package/fesm2022/raintonic-formaui-services-dialog.mjs.map +1 -1
  111. package/fesm2022/raintonic-formaui-services-notification.mjs +93 -29
  112. package/fesm2022/raintonic-formaui-services-notification.mjs.map +1 -1
  113. package/fesm2022/raintonic-formaui-services-theme.mjs +46 -196
  114. package/fesm2022/raintonic-formaui-services-theme.mjs.map +1 -1
  115. package/fesm2022/raintonic-formaui.mjs +1 -1
  116. package/fesm2022/raintonic-formaui.mjs.map +1 -1
  117. package/llms-full.txt +2329 -450
  118. package/llms.txt +36 -33
  119. package/package.json +42 -19
  120. package/styles/fonts/Geist-Bold.woff2 +0 -0
  121. package/styles/fonts/Geist-Italic.woff2 +0 -0
  122. package/styles/fonts/Geist-Light.woff2 +0 -0
  123. package/styles/fonts/Geist-Medium.woff2 +0 -0
  124. package/styles/fonts/Geist-Regular.woff2 +0 -0
  125. package/styles/fonts/Geist-SemiBold.woff2 +0 -0
  126. package/styles/fonts/GeistMono-Regular.woff2 +0 -0
  127. package/styles/generated/_tokens.scss +906 -0
  128. package/styles/index.scss +11 -10
  129. package/styles/partials/_brand.scss +46 -0
  130. package/styles/partials/_constants.scss +22 -20
  131. package/styles/partials/_fonts.scss +54 -10
  132. package/styles/partials/_grid.scss +29 -18
  133. package/styles/partials/_mixins.scss +69 -27
  134. package/styles/partials/_motion.scss +28 -33
  135. package/styles/partials/_theme.scss +28 -255
  136. package/styles/partials/_type.scss +117 -0
  137. package/styles/partials/_typography.scss +45 -45
  138. package/styles/partials/_utilities.scss +198 -98
  139. package/styles/partials/components/_button.scss +144 -75
  140. package/styles/partials/components/_dialog.scss +181 -180
  141. package/styles/partials/components/_overlay.scss +87 -87
  142. package/styles/partials/themes/_dark.scss +3 -268
  143. package/styles/partials/themes/_light.scss +4 -268
  144. package/styles/styles.css +7744 -0
  145. package/styles/styles.entry.scss +3 -0
  146. package/styles/utilities.css +4802 -0
  147. package/styles/utilities.entry.scss +3 -0
  148. package/types/raintonic-formaui-cdk-drag-drop.d.ts +0 -1
  149. package/types/raintonic-formaui-cdk-drag-drop.d.ts.map +1 -1
  150. package/types/raintonic-formaui-cdk-form-field.d.ts +118 -2
  151. package/types/raintonic-formaui-cdk-form-field.d.ts.map +1 -1
  152. package/types/raintonic-formaui-cdk-overlay.d.ts +2 -0
  153. package/types/raintonic-formaui-cdk-overlay.d.ts.map +1 -1
  154. package/types/raintonic-formaui-cdk-virtual-scroll.d.ts +0 -1
  155. package/types/raintonic-formaui-cdk-virtual-scroll.d.ts.map +1 -1
  156. package/types/raintonic-formaui-components-accordion.d.ts +1 -1
  157. package/types/raintonic-formaui-components-accordion.d.ts.map +1 -1
  158. package/types/raintonic-formaui-components-alert.d.ts +6 -1
  159. package/types/raintonic-formaui-components-alert.d.ts.map +1 -1
  160. package/types/raintonic-formaui-components-autocomplete.d.ts +73 -116
  161. package/types/raintonic-formaui-components-autocomplete.d.ts.map +1 -1
  162. package/types/raintonic-formaui-components-avatar.d.ts +13 -31
  163. package/types/raintonic-formaui-components-avatar.d.ts.map +1 -1
  164. package/types/raintonic-formaui-components-button.d.ts +4 -10
  165. package/types/raintonic-formaui-components-button.d.ts.map +1 -1
  166. package/types/raintonic-formaui-components-chip.d.ts +43 -0
  167. package/types/raintonic-formaui-components-chip.d.ts.map +1 -0
  168. package/types/raintonic-formaui-components-data-table.d.ts +48 -11
  169. package/types/raintonic-formaui-components-data-table.d.ts.map +1 -1
  170. package/types/raintonic-formaui-components-date-picker.d.ts +59 -23
  171. package/types/raintonic-formaui-components-date-picker.d.ts.map +1 -1
  172. package/types/raintonic-formaui-components-dropdown-menu.d.ts +394 -0
  173. package/types/raintonic-formaui-components-dropdown-menu.d.ts.map +1 -0
  174. package/types/raintonic-formaui-components-dual-tier-navigation.d.ts +87 -0
  175. package/types/raintonic-formaui-components-dual-tier-navigation.d.ts.map +1 -0
  176. package/types/raintonic-formaui-components-form-field.d.ts +51 -21
  177. package/types/raintonic-formaui-components-form-field.d.ts.map +1 -1
  178. package/types/raintonic-formaui-components-input.d.ts +20 -11
  179. package/types/raintonic-formaui-components-input.d.ts.map +1 -1
  180. package/types/raintonic-formaui-components-number-input.d.ts +5 -3
  181. package/types/raintonic-formaui-components-number-input.d.ts.map +1 -1
  182. package/types/raintonic-formaui-components-password-input.d.ts +18 -32
  183. package/types/raintonic-formaui-components-password-input.d.ts.map +1 -1
  184. package/types/raintonic-formaui-components-popover.d.ts.map +1 -1
  185. package/types/raintonic-formaui-components-progressbar.d.ts +1 -1
  186. package/types/raintonic-formaui-components-progressbar.d.ts.map +1 -1
  187. package/types/raintonic-formaui-components-radio.d.ts +1 -2
  188. package/types/raintonic-formaui-components-radio.d.ts.map +1 -1
  189. package/types/raintonic-formaui-components-select.d.ts +107 -76
  190. package/types/raintonic-formaui-components-select.d.ts.map +1 -1
  191. package/types/raintonic-formaui-components-sidebar-nav-menu.d.ts +223 -0
  192. package/types/raintonic-formaui-components-sidebar-nav-menu.d.ts.map +1 -0
  193. package/types/raintonic-formaui-components-stepper.d.ts +4 -2
  194. package/types/raintonic-formaui-components-stepper.d.ts.map +1 -1
  195. package/types/raintonic-formaui-components-strength-meter.d.ts +78 -0
  196. package/types/raintonic-formaui-components-strength-meter.d.ts.map +1 -0
  197. package/types/raintonic-formaui-components-time-picker.d.ts +44 -24
  198. package/types/raintonic-formaui-components-time-picker.d.ts.map +1 -1
  199. package/types/raintonic-formaui-components-toggle-group.d.ts +100 -0
  200. package/types/raintonic-formaui-components-toggle-group.d.ts.map +1 -0
  201. package/types/raintonic-formaui-components-tooltip.d.ts +2 -1
  202. package/types/raintonic-formaui-components-tooltip.d.ts.map +1 -1
  203. package/types/raintonic-formaui-components-topbar.d.ts +48 -0
  204. package/types/raintonic-formaui-components-topbar.d.ts.map +1 -0
  205. package/types/raintonic-formaui-components-tree-select.d.ts +25 -9
  206. package/types/raintonic-formaui-components-tree-select.d.ts.map +1 -1
  207. package/types/raintonic-formaui-components-tree.d.ts +12 -1
  208. package/types/raintonic-formaui-components-tree.d.ts.map +1 -1
  209. package/types/raintonic-formaui-core.d.ts +243 -5
  210. package/types/raintonic-formaui-core.d.ts.map +1 -1
  211. package/types/raintonic-formaui-services-breakpoint.d.ts +44 -0
  212. package/types/raintonic-formaui-services-breakpoint.d.ts.map +1 -0
  213. package/types/raintonic-formaui-services-dialog.d.ts +141 -2
  214. package/types/raintonic-formaui-services-dialog.d.ts.map +1 -1
  215. package/types/raintonic-formaui-services-notification.d.ts +24 -2
  216. package/types/raintonic-formaui-services-notification.d.ts.map +1 -1
  217. package/types/raintonic-formaui-services-theme.d.ts +13 -103
  218. package/types/raintonic-formaui-services-theme.d.ts.map +1 -1
  219. package/types/raintonic-formaui.d.ts +1 -1
  220. package/fesm2022/raintonic-formaui-components-big-menu.mjs +0 -86
  221. package/fesm2022/raintonic-formaui-components-big-menu.mjs.map +0 -1
  222. package/fesm2022/raintonic-formaui-components-menu.mjs +0 -896
  223. package/fesm2022/raintonic-formaui-components-menu.mjs.map +0 -1
  224. package/fesm2022/raintonic-formaui-components-sidebar.mjs +0 -275
  225. package/fesm2022/raintonic-formaui-components-sidebar.mjs.map +0 -1
  226. package/fesm2022/raintonic-formaui-components-tag.mjs +0 -95
  227. package/fesm2022/raintonic-formaui-components-tag.mjs.map +0 -1
  228. package/styles/_fonts-entry.scss +0 -3
  229. package/styles/fonts/inter-tight-latin-italic.woff2 +0 -0
  230. package/styles/fonts/inter-tight-latin.woff2 +0 -0
  231. package/types/raintonic-formaui-components-big-menu.d.ts +0 -73
  232. package/types/raintonic-formaui-components-big-menu.d.ts.map +0 -1
  233. package/types/raintonic-formaui-components-menu.d.ts +0 -403
  234. package/types/raintonic-formaui-components-menu.d.ts.map +0 -1
  235. package/types/raintonic-formaui-components-sidebar.d.ts +0 -185
  236. package/types/raintonic-formaui-components-sidebar.d.ts.map +0 -1
  237. package/types/raintonic-formaui-components-tag.d.ts +0 -43
  238. package/types/raintonic-formaui-components-tag.d.ts.map +0 -1
@@ -1,26 +1,25 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, ChangeDetectorRef, input, computed, output, signal, contentChildren, ElementRef, NgZone, effect, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
- import { NgForm, FormGroupDirective, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
5
- import { DOCUMENT } from '@angular/common';
6
- import { Subject, Subscription, fromEvent } from 'rxjs';
7
- import { filter } from 'rxjs/operators';
8
- import { injectNgControl, updateErrorState, syncRequiredState, syncNgControlDisabled } from '@raintonic/formaui/cdk/form-field';
2
+ import { Injectable, InjectionToken, inject, input, booleanAttribute, output, signal, computed, contentChildren, effect, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { Subject } from 'rxjs';
5
+ import * as i1 from '@raintonic/formaui/cdk/form-field';
6
+ import { FuiPopupOverlayDirective, FuiFormControlSyncDirective, injectNgControl } from '@raintonic/formaui/cdk/form-field';
9
7
  import { FuiIconComponent } from '@raintonic/formaui/components/icon';
10
- import { FuiButtonDirective } from '@raintonic/formaui/components/button';
11
- import { FuiIntlBase, DefaultErrorStateMatcher, FUI_FORM_FIELD_CONTROL } from '@raintonic/formaui/core';
12
- import { FuiOptionComponent } from '@raintonic/formaui/components/select';
13
- import { FuiOverlayService } from '@raintonic/formaui/cdk/overlay';
8
+ import { FUI_FORM_FIELD, FuiFormFieldComponent, FuiPrefixDirective } from '@raintonic/formaui/components/form-field';
9
+ import { FuiInputDirective } from '@raintonic/formaui/components/input';
10
+ import { FuiIntlBase, isEmpty, noop, computeMultiDisplayValue, syncMultiOptions, applyOptionSelection, getEnabledOptions, computePagedActiveOptionIndex, computeNextActiveOptionIndex, scrollOptionIntoView, findInitialActiveOptionIndex, computeActiveDescendant, announceMessage, FUI_FORM_FIELD_CONTROL } from '@raintonic/formaui/core';
11
+ import { FUI_OPTION, FUI_SELECT } from '@raintonic/formaui/components/select';
14
12
 
15
13
  class FuiAutocompleteIntl extends FuiIntlBase {
16
- addButtonLabel = 'Add New';
17
- refreshButtonLabel = 'Refresh';
18
- noOptionsText = 'No options found';
14
+ /** Placeholder text for the search input inside the panel. */
19
15
  searchPlaceholder = 'Search...';
16
+ /** Aria label for the clear selection button. */
20
17
  clearSelectionAriaLabel = 'Clear selection';
21
- noResultsAvailableText = 'No results available.';
18
+ /** Text shown when the search filter has no matching options. */
19
+ noResultsText = 'No results found';
20
+ /** Announced via live region when panel opens, indicating how many results are available. */
22
21
  resultsAvailableText(count) {
23
- return `${count} results available.`;
22
+ return count === 1 ? '1 result available.' : `${count} results available.`;
24
23
  }
25
24
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiAutocompleteIntl, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
26
25
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiAutocompleteIntl, providedIn: 'root' });
@@ -30,131 +29,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
30
29
  args: [{ providedIn: 'root' }]
31
30
  }] });
32
31
 
33
- /**
34
- * Injection token used to provide the parent autocomplete to options
35
- */
36
32
  const FUI_AUTOCOMPLETE = new InjectionToken('FUI_AUTOCOMPLETE');
37
33
  /**
38
34
  * # fui-autocomplete Component
39
35
  *
40
- * An autocomplete component designed to work seamlessly with fui-form-field.
41
- * Similar to Angular Material's mat-autocomplete integration with mat-form-field.
42
- * Provides full Reactive Forms support with validation, filtering, and optional
43
- * add/refresh actions.
36
+ * An autocomplete component that works inside fui-form-field.
37
+ * Options are content-projected as `<fui-option>` children same API as `<fui-select>`.
38
+ * Search filtering is always enabled, CSS-hiding non-matching options.
44
39
  *
45
40
  * ## Features
46
- * - Works inside fui-form-field like mat-autocomplete
47
- * - Full Reactive Forms integration (ControlValueAccessor)
48
- * - Options via projected content (fui-option)
49
- * - Built-in search/filter input in the panel
50
- * - Optional "Add New" and "Refresh" action buttons
51
- * - Disabled and readonly states
52
- * - Full accessibility support
53
- * - Full keyboard navigation (Arrow keys, Enter, Escape, Home, End, PageUp, PageDown)
54
- * - Uses overlay service for proper positioning
41
+ * - Content-projected `<fui-option>` children matching the `<fui-select>` API
42
+ * - Built-in search filtering CSS-hides non-matching options
43
+ * - Single and multiple selection
44
+ * - Full keyboard navigation
45
+ * - Full Reactive Forms integration
55
46
  *
56
47
  * ## Usage
57
48
  *
58
- * ### Basic Autocomplete with Form Field
49
+ * ### Basic Autocomplete
59
50
  * ```html
60
51
  * <fui-form-field>
61
- * <label>Country</label>
62
- * <fui-autocomplete placeholder="Search countries...">
63
- * <fui-option value="us">United States</fui-option>
64
- * <fui-option value="ca">Canada</fui-option>
65
- * <fui-option value="mx">Mexico</fui-option>
52
+ * <label>Framework</label>
53
+ * <fui-autocomplete placeholder="Select a framework" formControlName="framework">
54
+ * @for (fw of frameworks; track fw) {
55
+ * <fui-option [value]="fw">{{ fw.label }}</fui-option>
56
+ * }
66
57
  * </fui-autocomplete>
67
58
  * </fui-form-field>
68
59
  * ```
69
60
  *
70
- * ### With Reactive Forms and Validation
71
- * ```html
72
- * <form [formGroup]="form">
73
- * <fui-form-field>
74
- * <label>Country</label>
75
- * <fui-autocomplete formControlName="country" placeholder="Select a country">
76
- * <fui-option value="us">United States</fui-option>
77
- * <fui-option value="ca">Canada</fui-option>
78
- * </fui-autocomplete>
79
- * <fui-error *ngIf="form.get('country')?.hasError('required')">
80
- * Country is required
81
- * </fui-error>
82
- * </fui-form-field>
83
- * </form>
84
- * ```
85
- *
86
- * ### With Add/Refresh Buttons
61
+ * ### Multiple Selection
87
62
  * ```html
88
- * <fui-form-field>
89
- * <label>Category</label>
90
- * <fui-autocomplete
91
- * placeholder="Search categories..."
92
- * [showAddButton]="true"
93
- * [showRefreshButton]="true"
94
- * (addNew)="openAddCategoryDialog()"
95
- * (refresh)="refreshCategories()">
96
- * @for (category of categories; track category.id) {
97
- * <fui-option [value]="category.id">{{ category.name }}</fui-option>
98
- * }
99
- * </fui-autocomplete>
100
- * </fui-form-field>
63
+ * <fui-autocomplete [multiple]="true" placeholder="Select tags" formControlName="tags">
64
+ * @for (tag of tags; track tag) {
65
+ * <fui-option [value]="tag">{{ tag }}</fui-option>
66
+ * }
67
+ * </fui-autocomplete>
101
68
  * ```
102
69
  */
103
70
  class FuiAutocompleteComponent {
104
- // Static properties
105
71
  static nextId = 0;
106
72
  controlType = 'fui-autocomplete';
107
- // Injected dependencies required by input resolvers (must be declared before computed fields below)
73
+ // Injected dependencies
74
+ _popup = inject(FuiPopupOverlayDirective);
75
+ _formSync = inject(FuiFormControlSyncDirective);
76
+ _parentFormField = inject(FUI_FORM_FIELD, { optional: true });
108
77
  intl = inject(FuiAutocompleteIntl);
109
- _cdr = inject(ChangeDetectorRef);
110
- // Inputs using signal-based API
78
+ // --- Inputs ---
111
79
  placeholderInput = input('', { ...(ngDevMode ? { debugName: "placeholderInput" } : /* istanbul ignore next */ {}), alias: 'placeholder' });
112
- disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
113
- readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
114
- showAddButton = input(false, ...(ngDevMode ? [{ debugName: "showAddButton" }] : /* istanbul ignore next */ []));
115
- showRefreshButton = input(false, ...(ngDevMode ? [{ debugName: "showRefreshButton" }] : /* istanbul ignore next */ []));
116
- addButtonLabel = input(undefined, ...(ngDevMode ? [{ debugName: "addButtonLabel" }] : /* istanbul ignore next */ []));
117
- refreshButtonLabel = input(undefined, ...(ngDevMode ? [{ debugName: "refreshButtonLabel" }] : /* istanbul ignore next */ []));
118
- noOptionsText = input(undefined, ...(ngDevMode ? [{ debugName: "noOptionsText" }] : /* istanbul ignore next */ []));
119
- searchPlaceholder = input(undefined, ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
120
- errorStateMatcher = input(null, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
121
- // Resolved i18n labels: per-instance input wins, Intl service is fallback
122
- resolvedAddButtonLabel = computed(() => this.addButtonLabel() ?? this.intl.addButtonLabel, ...(ngDevMode ? [{ debugName: "resolvedAddButtonLabel" }] : /* istanbul ignore next */ []));
123
- resolvedRefreshButtonLabel = computed(() => this.refreshButtonLabel() ?? this.intl.refreshButtonLabel, ...(ngDevMode ? [{ debugName: "resolvedRefreshButtonLabel" }] : /* istanbul ignore next */ []));
124
- resolvedNoOptionsText = computed(() => this.noOptionsText() ?? this.intl.noOptionsText, ...(ngDevMode ? [{ debugName: "resolvedNoOptionsText" }] : /* istanbul ignore next */ []));
125
- resolvedSearchPlaceholder = computed(() => this.searchPlaceholder() ?? this.intl.searchPlaceholder, ...(ngDevMode ? [{ debugName: "resolvedSearchPlaceholder" }] : /* istanbul ignore next */ []));
80
+ disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled', transform: booleanAttribute });
81
+ readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly', transform: booleanAttribute });
82
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
126
83
  /**
127
- * Whether to compare option values using object identity or deep equality
84
+ * Comparison function for option values.
85
+ * Defaults to strict equality (`===`). Override when options use object
86
+ * values and you need to match by a property (e.g. comparing by `id`).
128
87
  */
129
88
  compareWith = input((o1, o2) => o1 === o2, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
130
- // Outputs
89
+ errorStateMatcher = input(null, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
90
+ // --- Outputs ---
131
91
  valueChange = output();
132
92
  selectionChange = output();
133
93
  openedChange = output();
134
- addNew = output();
135
- refresh = output();
136
94
  searchChange = output();
137
- // Internal state signals
95
+ // --- Internal state ---
138
96
  _value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
139
97
  _focused = signal(false, ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
140
98
  _disabled = signal(false, ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
141
99
  _readOnly = signal(false, ...(ngDevMode ? [{ debugName: "_readOnly" }] : /* istanbul ignore next */ []));
142
- // FuiFormFieldControl implementation
143
100
  stateChanges = new Subject();
144
101
  _uid = `fui-autocomplete-${FuiAutocompleteComponent.nextId++}`;
145
102
  _ariaDescribedby = null;
146
103
  // Error state
147
104
  _errorState = signal(false, ...(ngDevMode ? [{ debugName: "_errorState" }] : /* istanbul ignore next */ []));
148
105
  errorState = this._errorState;
149
- // Form control references
150
- _parentForm = inject(NgForm, { optional: true });
151
- _parentFormGroup = inject(FormGroupDirective, { optional: true });
152
- _defaultErrorStateMatcher = inject(DefaultErrorStateMatcher);
106
+ // Form control
153
107
  _ngControlRef = injectNgControl();
154
108
  get ngControl() {
155
109
  return this._ngControlRef.ngControl;
156
110
  }
157
- // Interface implementation
111
+ // Computed interface properties
158
112
  placeholder = computed(() => this.placeholderInput(), ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
159
113
  _required = signal(false, ...(ngDevMode ? [{ debugName: "_required" }] : /* istanbul ignore next */ []));
160
114
  required = this._required;
@@ -162,69 +116,39 @@ class FuiAutocompleteComponent {
162
116
  focused = this._focused;
163
117
  _ngControlDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_ngControlDisabled" }] : /* istanbul ignore next */ []));
164
118
  disabled = computed(() => this._disabled() || this.disabledInput() || this._ngControlDisabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
165
- empty = computed(() => {
166
- const val = this._value();
167
- return val === null || val === undefined;
168
- }, ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
119
+ readonly = computed(() => this.readonlyInput() || this._readOnly(), ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
120
+ empty = computed(() => isEmpty(this._value()), ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
169
121
  id = this._uid;
170
- // ViewChild for trigger and panel
122
+ // --- View refs ---
171
123
  trigger;
172
124
  panel;
173
125
  searchInput;
174
- // ContentChildren for options
175
- options = contentChildren(FuiOptionComponent, { ...(ngDevMode ? { debugName: "options" } : /* istanbul ignore next */ {}), descendants: true });
176
- // Panel open/close state
177
- panelOpen = signal(false, ...(ngDevMode ? [{ debugName: "panelOpen" }] : /* istanbul ignore next */ []));
178
- // Search term for filtering
126
+ // --- Content-projected options ---
127
+ options = contentChildren(FUI_OPTION, { ...(ngDevMode ? { debugName: "options" } : /* istanbul ignore next */ {}), descendants: true });
128
+ // --- Panel state ---
129
+ panelOpen = computed(() => this._popup.panelOpen(), ...(ngDevMode ? [{ debugName: "panelOpen" }] : /* istanbul ignore next */ []));
130
+ // --- Search ---
179
131
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
180
- // Active option index for keyboard navigation
181
- activeOptionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeOptionIndex" }] : /* istanbul ignore next */ []));
182
- // Live announcement for screen readers
183
- _liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "_liveAnnouncement" }] : /* istanbul ignore next */ []));
184
- _announce(message) {
185
- this._liveAnnouncement.set('');
186
- setTimeout(() => {
187
- this._liveAnnouncement.set(message);
188
- }, 50);
189
- }
190
- // Overlay reference
191
- _overlayRef = null;
192
- _overlaySubscriptions = new Subscription();
193
- // Services
194
- _overlayService = inject(FuiOverlayService);
195
- _elementRef = inject(ElementRef);
196
- _document = inject(DOCUMENT);
197
- _ngZone = inject(NgZone);
198
- _outsideClickSub;
199
- // ControlValueAccessor callbacks
200
- _onChange = () => {
201
- // Intentionally empty: will be replaced by Angular forms
202
- };
203
- _onTouched = () => {
204
- // Intentionally empty: will be replaced by Angular forms
205
- };
206
- // Computed properties
207
- displayValue = computed(() => {
208
- const currentValue = this.value();
209
- const opts = this.options();
210
- if (currentValue === null || currentValue === undefined) {
211
- return '';
212
- }
213
- const selectedOption = opts.find((opt) => this.compareWith()(opt.value(), currentValue));
214
- return selectedOption ? selectedOption.getLabel() : String(currentValue);
215
- }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
216
- // Filtered options based on search term
217
- filteredOptions = computed(() => {
132
+ // --- Active option index ---
133
+ activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
134
+ // --- Visible (not filtered-out) options — single source of truth for search filtering ---
135
+ _visibleOptions = computed(() => {
136
+ const term = this.searchTerm();
218
137
  const opts = this.options();
219
- const term = this.searchTerm().toLowerCase().trim();
220
- if (!term) {
221
- return opts.filter((opt) => !opt.disabled());
138
+ if (!term.trim()) {
139
+ return opts;
222
140
  }
223
- return opts.filter((opt) => !opt.disabled() && opt.getLabel().toLowerCase().includes(term));
224
- }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
225
- hasFilteredOptions = computed(() => {
226
- return this.filteredOptions().length > 0;
227
- }, ...(ngDevMode ? [{ debugName: "hasFilteredOptions" }] : /* istanbul ignore next */ []));
141
+ const lower = term.toLowerCase().trim();
142
+ return opts.filter((o) => o.getLabel().toLowerCase().includes(lower));
143
+ }, ...(ngDevMode ? [{ debugName: "_visibleOptions" }] : /* istanbul ignore next */ []));
144
+ _visibleOptionCount = computed(() => this._visibleOptions().length, ...(ngDevMode ? [{ debugName: "_visibleOptionCount" }] : /* istanbul ignore next */ []));
145
+ // --- Live announcement ---
146
+ _liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "_liveAnnouncement" }] : /* istanbul ignore next */ []));
147
+ // --- ControlValueAccessor callbacks ---
148
+ _onChange = noop;
149
+ _onTouched = noop;
150
+ // --- Computed ---
151
+ displayValue = computed(() => computeMultiDisplayValue(this), ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
228
152
  constructor() {
229
153
  // Set valueAccessor after NgControl is resolved
230
154
  void Promise.resolve().then(() => {
@@ -232,14 +156,12 @@ class FuiAutocompleteComponent {
232
156
  this._ngControlRef.ngControl.valueAccessor = this;
233
157
  }
234
158
  });
235
- // Subscribe to Intl changes to trigger re-render when labels mutate at runtime
236
- this.intl.changes.pipe(takeUntilDestroyed()).subscribe(() => { this._cdr.markForCheck(); });
237
- // Effect to emit state changes
159
+ // State changes effect
238
160
  effect(() => {
239
- // Track all reactive inputs and internal signals
240
161
  this.placeholderInput();
241
162
  this.readonly();
242
163
  this.disabledInput();
164
+ this.multiple();
243
165
  this.errorStateMatcher();
244
166
  this._focused();
245
167
  this._disabled();
@@ -247,41 +169,76 @@ class FuiAutocompleteComponent {
247
169
  this._ngControlDisabled();
248
170
  this._required();
249
171
  this._errorState();
250
- // Emit state change
251
172
  this.stateChanges.next();
252
173
  });
253
- // Effect to update options selected state when value changes
174
+ // Sync option states when value or options change
254
175
  effect(() => {
255
- const currentValue = this._value();
256
- const opts = this.options();
257
- const compareFn = this.compareWith();
258
- opts.forEach((option) => {
259
- const optValue = option.value();
260
- const isSelected = compareFn(currentValue, optValue);
261
- if (isSelected) {
262
- option.select();
263
- }
264
- else {
265
- option.deselect();
266
- }
267
- });
268
- // Notify form-field that state may have changed
176
+ syncMultiOptions(this);
269
177
  this.stateChanges.next();
270
178
  });
179
+ // Sync DOM visibility to match _visibleOptions computed signal
180
+ // One-way sync: computed signal is the source of truth, DOM is updated to match
181
+ effect(() => {
182
+ const term = this.searchTerm();
183
+ const opts = this.options();
184
+ const visible = this._visibleOptions();
185
+ const visibleSet = new Set(visible);
186
+ for (const opt of opts) {
187
+ opt._getHostElement().style.display = visibleSet.has(opt) ? '' : 'none';
188
+ }
189
+ });
190
+ // Announce visible results count when search term or panel state changes
191
+ effect(() => {
192
+ const term = this.searchTerm();
193
+ const visible = this._visibleOptions();
194
+ const isOpen = this._popup.panelOpen();
195
+ // Only announce when the panel is open and search filtering is active
196
+ if (isOpen && term.trim()) {
197
+ this._announce(this.intl.resultsAvailableText(visible.length));
198
+ }
199
+ });
200
+ // Focus management: move focus to search input when panel opens
201
+ let _panelInitialized = false;
202
+ effect(() => {
203
+ const isOpen = this._popup.panelOpen();
204
+ if (!_panelInitialized) {
205
+ _panelInitialized = true;
206
+ return;
207
+ }
208
+ if (isOpen) {
209
+ queueMicrotask(() => {
210
+ this.searchInput?.nativeElement.focus();
211
+ });
212
+ }
213
+ });
271
214
  }
272
- ngDoCheck() {
273
- if (this.ngControl) {
274
- updateErrorState(this.ngControl, this._errorState, this.errorStateMatcher(), this._defaultErrorStateMatcher, this._parentForm, this._parentFormGroup, this.stateChanges);
275
- syncRequiredState(this.ngControl, this._required, this.stateChanges);
276
- syncNgControlDisabled(this.ngControl, this._ngControlDisabled, this.stateChanges);
215
+ ngAfterViewInit() {
216
+ this._popup.setTrigger(this.trigger ?? null);
217
+ this._popup.setPanel(this.panel ?? null);
218
+ this._popup.panelClass.set(['fui-autocomplete-overlay-panel']);
219
+ this._popup.backdropClass.set('fui-autocomplete-backdrop');
220
+ this._popup.positions.set([
221
+ {
222
+ originX: 'start',
223
+ originY: 'bottom',
224
+ overlayX: 'start',
225
+ overlayY: 'top',
226
+ offsetY: 16,
227
+ },
228
+ ]);
229
+ if (this._parentFormField) {
230
+ this._popup.widthElement.set(this._parentFormField.getConnectedOverlayOrigin());
277
231
  }
232
+ this._formSync.errorState.set(this._errorState);
233
+ this._formSync.errorStateMatcher.set(this.errorStateMatcher());
234
+ this._formSync.required.set(this._required);
235
+ this._formSync.ngControlDisabled.set(this._ngControlDisabled);
236
+ this._formSync.stateChanges.set(this.stateChanges);
278
237
  }
279
238
  ngOnDestroy() {
280
239
  this.stateChanges.complete();
281
- this._outsideClickSub?.unsubscribe();
282
- this._disposeOverlay();
283
240
  }
284
- // ControlValueAccessor implementation
241
+ // --- ControlValueAccessor ---
285
242
  writeValue(value) {
286
243
  this._value.set(value ?? null);
287
244
  this.stateChanges.next();
@@ -296,11 +253,13 @@ class FuiAutocompleteComponent {
296
253
  this._disabled.set(isDisabled);
297
254
  this.stateChanges.next();
298
255
  }
299
- // FuiFormFieldControl implementation
256
+ // --- FuiFormFieldControl ---
300
257
  onContainerClick(_event) {
301
- if (!this.disabled()) {
302
- this.toggle();
303
- }
258
+ if (this.disabled())
259
+ return;
260
+ if (this.multiple() && this.panelOpen())
261
+ return;
262
+ this.toggle();
304
263
  }
305
264
  setDescribedByIds(ids) {
306
265
  this._ariaDescribedby = ids.length ? ids.join(' ') : null;
@@ -308,7 +267,8 @@ class FuiAutocompleteComponent {
308
267
  setReadOnly(readOnly) {
309
268
  this._readOnly.set(readOnly);
310
269
  }
311
- // Public methods
270
+ // --- Public API ---
271
+ /** Clears the current value. */
312
272
  clear(event) {
313
273
  event.stopPropagation();
314
274
  this._value.set(null);
@@ -323,122 +283,88 @@ class FuiAutocompleteComponent {
323
283
  blur() {
324
284
  this.trigger?.nativeElement.blur();
325
285
  }
326
- // Toggle panel open/close
327
- toggle() {
328
- if (this.disabled())
329
- return;
330
- if (this.panelOpen()) {
331
- this.close();
332
- }
333
- else {
334
- this.open();
335
- }
336
- }
337
- // Open the dropdown panel
286
+ /** Opens the panel. */
338
287
  open() {
339
- if (this.disabled() || this.readonly() || this.panelOpen())
288
+ if (this.disabled() || this.readonly() || this._popup.panelOpen())
340
289
  return;
341
- this.panelOpen.set(true);
342
290
  this._focused.set(true);
343
291
  this.searchTerm.set('');
344
- this.openedChange.emit(true);
345
- // Set active option to currently selected or first option
346
292
  this._setInitialActiveOption();
347
- // Create overlay after the view updates
348
- setTimeout(() => {
349
- this._createOverlay();
293
+ this._popup.open();
294
+ if (this._popup.panelOpen()) {
350
295
  this._scrollToActiveOption();
351
- this._listenForOutsideClicks();
352
- // Focus the search input
353
- this.searchInput?.nativeElement.focus();
354
- }, 0);
296
+ this.openedChange.emit(true);
297
+ // Announce how many results are available
298
+ this._announce(this.intl.resultsAvailableText(this.options().length));
299
+ }
355
300
  }
356
- // Close the dropdown panel
357
- close() {
358
- if (!this.panelOpen())
301
+ /** Closes the panel. */
302
+ close(restoreFocus = true) {
303
+ if (!this._popup.panelOpen())
359
304
  return;
360
- this._outsideClickSub?.unsubscribe();
361
- this.panelOpen.set(false);
362
305
  this._focused.set(false);
363
- this.activeOptionIndex.set(-1);
306
+ this.activeIndex.set(-1);
364
307
  this.searchTerm.set('');
365
- this._disposeOverlay();
366
- this.openedChange.emit(false);
367
308
  this._onTouched();
368
- // Clear active state on all options
369
- this.options().forEach((opt) => {
370
- opt.setInactive();
371
- });
372
- // Return focus to trigger
373
- setTimeout(() => {
309
+ this._popup.close();
310
+ this.openedChange.emit(false);
311
+ if (restoreFocus) {
374
312
  this.trigger?.nativeElement.focus();
375
- }, 0);
313
+ }
314
+ }
315
+ /** Toggles the panel. */
316
+ toggle() {
317
+ if (this.disabled() || this.readonly())
318
+ return;
319
+ this._popup.toggle();
376
320
  }
377
- // Handle option selection (called by FuiOptionComponent or internally)
321
+ // --- Selection ---
322
+ /**
323
+ * Called by fui-option via FUI_SELECT._onOptionSelected.
324
+ * Implements FuiSelectParent.
325
+ */
378
326
  _onOptionSelected(option) {
379
- const newValue = option.value();
380
- // Deselect previous option
381
- const opts = this.options();
382
- opts.forEach((opt) => {
383
- if (opt !== option) {
384
- opt.deselect();
385
- }
386
- });
387
- option.select();
327
+ const result = applyOptionSelection(this, option);
328
+ const newValue = result.newValue;
388
329
  this._value.set(newValue);
389
330
  this._onChange(newValue);
390
331
  this.valueChange.emit(newValue);
391
332
  this.selectionChange.emit({ source: this, value: newValue });
392
- this._announce(`${option.getLabel()} selected`);
393
- this.close();
394
333
  this.stateChanges.next();
334
+ if (result.wasSelected) {
335
+ this._announce(`${option.getLabel()} deselected`);
336
+ }
337
+ else {
338
+ this._announce(`${option.getLabel()} selected`);
339
+ }
340
+ if (!this.multiple()) {
341
+ this.close();
342
+ }
395
343
  }
396
- // Handle search input
344
+ // --- Search ---
397
345
  onSearchInput(event) {
398
346
  const target = event.target;
399
347
  const term = target.value;
400
348
  this.searchTerm.set(term);
401
349
  this.searchChange.emit(term);
402
- // Reset active index when search changes
403
- const filtered = this.filteredOptions();
404
- if (filtered.length > 0) {
405
- this._setActiveOptionIndex(0);
406
- }
407
- else {
408
- this.activeOptionIndex.set(-1);
409
- }
410
- }
411
- // Handle add new action
412
- onAddNew() {
413
- this.close();
414
- this.addNew.emit();
415
- }
416
- // Handle refresh action
417
- onRefresh() {
418
- this.refresh.emit();
419
- // Keep panel open and clear search
420
- this.searchTerm.set('');
421
- }
422
- // Get display value for trigger
423
- _getDisplayValue() {
424
- return this.displayValue();
350
+ // Reset active index to first visible enabled option
351
+ const enabled = getEnabledOptions(this._visibleOptions());
352
+ this.activeIndex.set(enabled.length > 0 ? 0 : -1);
425
353
  }
426
- // Handle keyboard navigation
354
+ // --- Keyboard Navigation ---
355
+ /** Entry point for trigger keydown */
427
356
  _handleKeydown(event) {
428
357
  if (this.disabled())
429
358
  return;
430
- const isOpen = this.panelOpen();
431
- if (!isOpen) {
359
+ if (!this.panelOpen()) {
432
360
  this._handleClosedKeydown(event);
433
361
  }
434
362
  else {
435
- this._handleOpenKeydown(event);
363
+ this._handlePanelKeydown(event);
436
364
  }
437
365
  }
438
- // Handle keydown when panel is closed
439
366
  _handleClosedKeydown(event) {
440
- const key = event.key;
441
- switch (key) {
367
+ switch (event.key) {
442
368
  case 'Enter':
443
369
  case ' ':
444
370
  case 'ArrowDown':
@@ -448,254 +374,104 @@ class FuiAutocompleteComponent {
448
374
  break;
449
375
  }
450
376
  }
451
- // Handle keydown when panel is open (for search input)
377
+ /** Handle keydown inside the panel (search input or item list) */
452
378
  _handlePanelKeydown(event) {
453
379
  if (this.disabled())
454
380
  return;
455
- const key = event.key;
456
- const opts = this.filteredOptions();
457
- const activeIndex = this.activeOptionIndex();
458
- switch (key) {
381
+ const opts = this._getVisibleEnabledOptions();
382
+ const activeIdx = this.activeIndex();
383
+ switch (event.key) {
459
384
  case 'ArrowDown':
460
385
  event.preventDefault();
461
- this._setNextActiveOption(1);
386
+ this._setActiveOptionIndex(computeNextActiveOptionIndex(opts.length, activeIdx, 1));
462
387
  break;
463
388
  case 'ArrowUp':
464
389
  event.preventDefault();
465
- this._setNextActiveOption(-1);
390
+ this._setActiveOptionIndex(computeNextActiveOptionIndex(opts.length, activeIdx, -1));
466
391
  break;
467
392
  case 'Home':
468
393
  event.preventDefault();
469
- if (opts.length > 0) {
470
- this._setActiveOptionIndex(0);
471
- }
394
+ this._setActiveOptionIndex(0);
472
395
  break;
473
396
  case 'End':
474
397
  event.preventDefault();
475
- if (opts.length > 0) {
476
- this._setActiveOptionIndex(opts.length - 1);
477
- }
398
+ this._setActiveOptionIndex(opts.length - 1);
478
399
  break;
479
400
  case 'PageDown':
480
401
  event.preventDefault();
481
402
  if (opts.length > 0) {
482
- const pageSize = Math.min(10, opts.length);
483
- const newIndex = Math.min(activeIndex + pageSize, opts.length - 1);
484
- this._setActiveOptionIndex(newIndex);
403
+ this._setActiveOptionIndex(computePagedActiveOptionIndex(opts.length, activeIdx, 1));
485
404
  }
486
405
  break;
487
406
  case 'PageUp':
488
407
  event.preventDefault();
489
408
  if (opts.length > 0) {
490
- const pageSize = Math.min(10, opts.length);
491
- const newIndex = Math.max(activeIndex - pageSize, 0);
492
- this._setActiveOptionIndex(newIndex);
409
+ this._setActiveOptionIndex(computePagedActiveOptionIndex(opts.length, activeIdx, -1));
493
410
  }
494
411
  break;
495
412
  case 'Enter':
496
413
  event.preventDefault();
497
- if (activeIndex >= 0 && activeIndex < opts.length) {
498
- this._onOptionSelected(opts[activeIndex]);
414
+ if (activeIdx >= 0 && activeIdx < opts.length) {
415
+ this._onOptionSelected(opts[activeIdx]);
499
416
  }
500
417
  break;
501
418
  case 'Escape':
502
419
  event.preventDefault();
420
+ event.stopPropagation();
503
421
  this.close();
504
422
  break;
505
423
  case 'Tab':
506
- // Allow tab to close and move to next element
507
424
  this.close();
508
425
  break;
509
426
  }
510
427
  }
511
- // Handle keydown when panel is open (for trigger)
512
- _handleOpenKeydown(event) {
513
- // Delegate to panel keydown handler
514
- this._handlePanelKeydown(event);
515
- }
516
- // Set the next active option based on delta
517
- _setNextActiveOption(delta) {
518
- const opts = this.filteredOptions();
519
- if (opts.length === 0)
520
- return;
521
- let currentIndex = this.activeOptionIndex();
522
- if (currentIndex < 0) {
523
- currentIndex = delta > 0 ? -1 : opts.length;
524
- }
525
- let newIndex = currentIndex + delta;
526
- // Wrap around
527
- if (newIndex < 0) {
528
- newIndex = opts.length - 1;
529
- }
530
- else if (newIndex >= opts.length) {
531
- newIndex = 0;
532
- }
533
- this._setActiveOptionIndex(newIndex);
534
- }
535
- // Set the active option index
536
428
  _setActiveOptionIndex(index) {
537
- const opts = this.filteredOptions();
429
+ const opts = this._getVisibleEnabledOptions();
538
430
  if (index < 0 || index >= opts.length)
539
431
  return;
540
- // Update active state on options
541
- const allOpts = this.options();
542
- allOpts.forEach((opt) => {
432
+ // Deactivate all visible options
433
+ this.options().forEach((opt) => {
543
434
  opt.setInactive();
544
435
  });
545
436
  const activeOption = opts[index];
546
437
  activeOption.setActive();
547
- this.activeOptionIndex.set(index);
548
- this._scrollToActiveOption();
549
- this._announce(`${activeOption.getLabel()}, ${index + 1} of ${opts.length}`);
438
+ this.activeIndex.set(index);
439
+ scrollOptionIntoView(activeOption._getHostElement(), this.panel?.nativeElement, '.fui-autocomplete__options');
440
+ // Announce active item
441
+ const label = activeOption.getLabel();
442
+ this._announce(`${label}, ${index + 1} of ${opts.length}`);
550
443
  }
551
- // Set initial active option when opening
552
444
  _setInitialActiveOption() {
553
- const opts = this.filteredOptions();
445
+ const opts = this._getVisibleEnabledOptions();
554
446
  if (opts.length === 0) {
555
- this.activeOptionIndex.set(-1);
447
+ this.activeIndex.set(-1);
556
448
  return;
557
449
  }
558
- const currentValue = this._value();
559
- const compareFn = this.compareWith();
560
- // Find the selected option
561
- let selectedIndex = -1;
562
- if (currentValue !== null && currentValue !== undefined) {
563
- selectedIndex = opts.findIndex((opt) => compareFn(opt.value(), currentValue));
564
- }
565
- const initialIndex = selectedIndex >= 0 ? selectedIndex : 0;
450
+ const initialIndex = findInitialActiveOptionIndex(opts, this._value(), this.multiple(), this.compareWith());
566
451
  this._setActiveOptionIndex(initialIndex);
567
452
  }
568
- // Scroll to active option
569
453
  _scrollToActiveOption() {
570
- const opts = this.filteredOptions();
571
- const activeIndex = this.activeOptionIndex();
572
- if (activeIndex < 0 || activeIndex >= opts.length)
454
+ const opts = this._getVisibleEnabledOptions();
455
+ const activeIdx = this.activeIndex();
456
+ if (activeIdx < 0 || activeIdx >= opts.length)
573
457
  return;
574
- const activeOption = opts[activeIndex];
575
- const element = activeOption._getHostElement();
576
- const panelElement = this.panel?.nativeElement;
577
- if (element && panelElement) {
578
- const optionsContainer = panelElement.querySelector('.fui-autocomplete__options');
579
- if (optionsContainer) {
580
- const optionTop = element.offsetTop - optionsContainer.offsetTop;
581
- const optionBottom = optionTop + element.offsetHeight;
582
- const containerTop = optionsContainer.scrollTop;
583
- const containerBottom = containerTop + optionsContainer.clientHeight;
584
- if (optionTop < containerTop) {
585
- optionsContainer.scrollTop = optionTop;
586
- }
587
- else if (optionBottom > containerBottom) {
588
- optionsContainer.scrollTop = optionBottom - optionsContainer.clientHeight;
589
- }
590
- }
591
- }
458
+ scrollOptionIntoView(opts[activeIdx]._getHostElement(), this.panel?.nativeElement, '.fui-autocomplete__options');
592
459
  }
593
- // Start listening for outside clicks when panel opens
594
- _listenForOutsideClicks() {
595
- this._outsideClickSub?.unsubscribe();
596
- this._ngZone.runOutsideAngular(() => {
597
- setTimeout(() => {
598
- this._outsideClickSub = fromEvent(this._document, 'click')
599
- .pipe(filter(() => this.panelOpen()), filter((event) => {
600
- const target = event.target;
601
- const triggerElement = this.trigger?.nativeElement;
602
- const panelElement = this.panel?.nativeElement;
603
- const overlayElement = this._overlayRef?.overlayElement;
604
- return (!triggerElement?.contains(target) &&
605
- !panelElement?.contains(target) &&
606
- !overlayElement?.contains(target));
607
- }))
608
- .subscribe(() => {
609
- this._ngZone.run(() => {
610
- this.close();
611
- });
612
- });
613
- });
614
- });
460
+ /** Get visible (non-hidden) enabled options for keyboard navigation */
461
+ _getVisibleEnabledOptions() {
462
+ return getEnabledOptions(this._visibleOptions());
615
463
  }
616
- // Create overlay for panel
617
- _createOverlay() {
618
- if (this._overlayRef || !this.panel || !this.trigger)
619
- return;
620
- const triggerElement = this.trigger.nativeElement;
621
- const triggerWidth = triggerElement.getBoundingClientRect().width;
622
- const positions = [
623
- {
624
- originX: 'start',
625
- originY: 'bottom',
626
- overlayX: 'start',
627
- overlayY: 'top',
628
- offsetY: 4,
629
- },
630
- {
631
- originX: 'start',
632
- originY: 'top',
633
- overlayX: 'start',
634
- overlayY: 'bottom',
635
- offsetY: -4,
636
- },
637
- ];
638
- const positionStrategy = this._overlayService
639
- .position()
640
- .connectedTo(triggerElement, positions)
641
- .withPush(true)
642
- .withViewportMargin(8);
643
- this._overlayRef = this._overlayService.create({
644
- positionStrategy,
645
- scrollStrategy: this._overlayService.scrollStrategies.reposition(),
646
- hasBackdrop: true,
647
- backdropClass: 'fui-autocomplete-backdrop',
648
- backdropClickBehavior: 'close',
649
- panelClass: ['fui-autocomplete-overlay-panel'],
650
- minWidth: triggerWidth,
651
- });
652
- // Track overlay subscriptions for proper cleanup
653
- this._overlaySubscriptions.unsubscribe();
654
- this._overlaySubscriptions = new Subscription();
655
- this._overlaySubscriptions.add(this._overlayRef.backdropClick.subscribe(() => {
656
- this.close();
657
- }));
658
- this._overlaySubscriptions.add(this._overlayRef.keydownEvents.subscribe((event) => {
659
- if (event.key === 'Escape') {
660
- this.close();
661
- }
662
- }));
663
- // Attach panel to overlay
664
- const panelElement = this.panel.nativeElement;
665
- this._overlayRef.attach(panelElement);
666
- }
667
- // Dispose overlay
668
- _disposeOverlay() {
669
- this._overlaySubscriptions.unsubscribe();
670
- if (this._overlayRef) {
671
- this._overlayRef.dispose();
672
- this._overlayRef = null;
673
- }
674
- }
675
- // Get the active option's id for aria-activedescendant
464
+ /** aria-activedescendant */
676
465
  _getActiveDescendant() {
677
- const opts = this.filteredOptions();
678
- const activeIndex = this.activeOptionIndex();
679
- if (activeIndex >= 0 && activeIndex < opts.length) {
680
- return opts[activeIndex].id;
681
- }
682
- return null;
683
- }
684
- // Set active option by index (for template use)
685
- setActiveOption(index, option) {
686
- const allOpts = this.options();
687
- allOpts.forEach((opt) => {
688
- opt.setInactive();
689
- });
690
- option.setActive();
691
- this.activeOptionIndex.set(index);
466
+ const opts = this._getVisibleEnabledOptions();
467
+ return computeActiveDescendant(opts, this.activeIndex());
692
468
  }
693
- // Mat-autocomplete compatibility methods
694
- get selected() {
695
- return this.value;
469
+ // --- Announcement ---
470
+ _announce(message) {
471
+ announceMessage(this._liveAnnouncement, message);
696
472
  }
697
473
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
698
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: FuiAutocompleteComponent, isStandalone: true, selector: "fui-autocomplete", inputs: { placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, showAddButton: { classPropertyName: "showAddButton", publicName: "showAddButton", isSignal: true, isRequired: false, transformFunction: null }, showRefreshButton: { classPropertyName: "showRefreshButton", publicName: "showRefreshButton", isSignal: true, isRequired: false, transformFunction: null }, addButtonLabel: { classPropertyName: "addButtonLabel", publicName: "addButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, refreshButtonLabel: { classPropertyName: "refreshButtonLabel", publicName: "refreshButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsText: { classPropertyName: "noOptionsText", publicName: "noOptionsText", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", selectionChange: "selectionChange", openedChange: "openedChange", addNew: "addNew", refresh: "refresh", searchChange: "searchChange" }, host: { properties: { "attr.id": "id", "class.fui-autocomplete--open": "panelOpen()", "class.fui-autocomplete--disabled": "disabled()", "class.fui-autocomplete--focused": "focused()", "class.fui-autocomplete--error": "errorState()", "class.fui-autocomplete--readonly": "_readOnly()" }, classAttribute: "fui-autocomplete" }, providers: [
474
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: FuiAutocompleteComponent, isStandalone: true, selector: "fui-autocomplete", inputs: { placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", selectionChange: "selectionChange", openedChange: "openedChange", searchChange: "searchChange" }, host: { properties: { "attr.id": "id", "class.fui-autocomplete--open": "panelOpen()", "class.fui-autocomplete--disabled": "disabled()", "class.fui-autocomplete--focused": "focused()", "class.fui-autocomplete--error": "errorState()", "class.fui-autocomplete--readonly": "readonly()", "class.fui-autocomplete--multiple": "multiple()", "attr.aria-readonly": "readonly() ? \"true\" : null" }, classAttribute: "fui-autocomplete" }, providers: [
699
475
  {
700
476
  provide: NG_VALUE_ACCESSOR,
701
477
  useExisting: FuiAutocompleteComponent,
@@ -709,18 +485,31 @@ class FuiAutocompleteComponent {
709
485
  provide: FUI_AUTOCOMPLETE,
710
486
  useExisting: FuiAutocompleteComponent,
711
487
  },
712
- ], queries: [{ propertyName: "options", predicate: FuiOptionComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true }], ngImport: i0, template: "<!-- Autocomplete Trigger -->\r\n<div\r\n #trigger\r\n [id]=\"id\"\r\n class=\"fui-autocomplete__trigger\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n [attr.role]=\"'combobox'\"\r\n [attr.aria-haspopup]=\"'listbox'\"\r\n [attr.aria-expanded]=\"panelOpen()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [attr.aria-invalid]=\"errorState()\"\r\n [attr.aria-describedby]=\"_ariaDescribedby\"\r\n [attr.aria-required]=\"required()\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-activedescendant]=\"panelOpen() ? _getActiveDescendant() : null\"\r\n [attr.aria-controls]=\"panelOpen() ? id + '-panel' : null\"\r\n (keydown)=\"_handleKeydown($event)\"\r\n>\r\n <span class=\"fui-autocomplete__value\">\r\n @if (empty()) {\r\n <span class=\"fui-autocomplete__placeholder\">{{ placeholder() }}</span>\r\n } @else {\r\n <span class=\"fui-autocomplete__value-text\">{{ _getDisplayValue() }}</span>\r\n }\r\n </span>\r\n\r\n @if (!_readOnly()) {\r\n <!-- Clear button \u2014 visible only when a value is selected and not disabled -->\r\n @if (!empty() && !disabled()) {\r\n <span class=\"fui-autocomplete__clear\" role=\"button\" [attr.aria-label]=\"intl.clearSelectionAriaLabel\" (click)=\"clear($event)\">\r\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\r\n <path\r\n d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z\"\r\n />\r\n </svg>\r\n </span>\r\n }\r\n\r\n <!-- Dropdown arrow icon -->\r\n <svg\r\n class=\"fui-autocomplete__arrow\"\r\n [class.fui-autocomplete__arrow--open]=\"panelOpen()\"\r\n viewBox=\"0 0 16 16\"\r\n fill=\"currentColor\"\r\n aria-hidden=\"true\"\r\n >\r\n <path d=\"M8 11L3 6l.7-.7L8 9.6l4.3-4.3L13 6z\" />\r\n </svg>\r\n }\r\n</div>\r\n\r\n<!-- Hidden container for projected options (used for data binding) -->\r\n<div class=\"fui-autocomplete__options-source\">\r\n <ng-content></ng-content>\r\n</div>\r\n\r\n<!-- Dropdown Panel -->\r\n@if (panelOpen()) {\r\n <div #panel [id]=\"id + '-panel'\" class=\"fui-autocomplete__panel\" [attr.aria-label]=\"placeholder()\">\r\n <!-- Search Input -->\r\n <div class=\"fui-autocomplete__search\">\r\n <fui-icon name=\"magnifying-glass\" size=\"sm\" class=\"fui-autocomplete__search-icon\"> </fui-icon>\r\n <input\r\n #searchInput\r\n type=\"text\"\r\n class=\"fui-autocomplete__search-input\"\r\n [placeholder]=\"resolvedSearchPlaceholder()\"\r\n [value]=\"searchTerm()\"\r\n autocomplete=\"off\"\r\n role=\"searchbox\"\r\n [attr.aria-label]=\"resolvedSearchPlaceholder()\"\r\n [attr.aria-controls]=\"id + '-panel-listbox'\"\r\n [attr.aria-activedescendant]=\"_getActiveDescendant()\"\r\n (input)=\"onSearchInput($event)\"\r\n (keydown)=\"_handlePanelKeydown($event)\"\r\n />\r\n </div>\r\n <!-- Live region for announcing result count to screen readers -->\r\n <div class=\"fui-sr-only\" aria-live=\"assertive\" aria-atomic=\"true\">\r\n @if (_liveAnnouncement()) {\r\n {{ _liveAnnouncement() }}\r\n } @else if (hasFilteredOptions()) {\r\n {{ intl.resultsAvailableText(filteredOptions().length) }}\r\n } @else {\r\n {{ intl.noResultsAvailableText }}\r\n }\r\n </div>\r\n\r\n <!-- Action Buttons -->\r\n @if (showAddButton() || showRefreshButton()) {\r\n <div class=\"fui-autocomplete__actions\">\r\n @if (showAddButton()) {\r\n <button\r\n type=\"button\"\r\n fuiButton\r\n variant=\"tertiary\"\r\n size=\"sm\"\r\n class=\"fui-autocomplete__action-button\"\r\n (click)=\"onAddNew()\"\r\n >\r\n <fui-icon name=\"plus\" size=\"sm\"></fui-icon>\r\n {{ resolvedAddButtonLabel() }}\r\n </button>\r\n }\r\n @if (showRefreshButton()) {\r\n <button\r\n type=\"button\"\r\n fuiButton\r\n variant=\"tertiary\"\r\n size=\"sm\"\r\n class=\"fui-autocomplete__action-button\"\r\n (click)=\"onRefresh()\"\r\n >\r\n <fui-icon name=\"refresh-cw\" size=\"sm\"></fui-icon>\r\n {{ resolvedRefreshButtonLabel() }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Options List -->\r\n <div\r\n class=\"fui-autocomplete__options\"\r\n [id]=\"id + '-panel-listbox'\"\r\n role=\"listbox\"\r\n [attr.aria-label]=\"placeholder()\"\r\n >\r\n @if (hasFilteredOptions()) {\r\n @for (option of filteredOptions(); track option.id; let i = $index) {\r\n <div\r\n class=\"fui-autocomplete__option\"\r\n [class.fui-autocomplete__option--active]=\"activeOptionIndex() === i\"\r\n [class.fui-autocomplete__option--selected]=\"option._selected()\"\r\n role=\"option\"\r\n [attr.id]=\"option.id\"\r\n [attr.aria-selected]=\"option._selected()\"\r\n (click)=\"_onOptionSelected(option)\"\r\n (mouseenter)=\"setActiveOption(i, option)\"\r\n (mouseleave)=\"option.setInactive()\"\r\n >\r\n <span class=\"fui-autocomplete__option-text\">{{ option.getLabel() }}</span>\r\n @if (option._selected()) {\r\n <fui-icon name=\"check\" size=\"sm\" class=\"fui-autocomplete__option-check\"></fui-icon>\r\n }\r\n </div>\r\n }\r\n } @else {\r\n <div class=\"fui-autocomplete__no-options\">\r\n {{ resolvedNoOptionsText() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: ["@keyframes fui-skeleton-pulse{0%{opacity:1}50%{opacity:.4}to{opacity:1}}@keyframes fui-spin{to{transform:rotate(360deg)}}@keyframes fui-shake{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-2px)}20%,40%,60%,80%{transform:translate(2px)}}.fui-motion-fade-in{transition:opacity var(--fui-duration-fast-02) var(--fui-ease-entrance) 0ms}.fui-motion-fade-out{transition:opacity var(--fui-duration-fast-01) var(--fui-ease-exit) 0ms}.fui-motion-slide-in-bottom{transition:transform var(--fui-duration-moderate-01) var(--fui-ease-entrance) 0ms;transform:translateY(0)}.fui-motion-slide-in-bottom.fui-motion-entering{transform:translateY(1rem)}.fui-motion-slide-in-top{transition:transform var(--fui-duration-moderate-01) var(--fui-ease-entrance) 0ms;transform:translateY(0)}.fui-motion-slide-in-top.fui-motion-entering{transform:translateY(-1rem)}.fui-motion-scale-in{transition:transform,opacity var(--fui-duration-moderate-01) var(--fui-ease-entrance) 0ms;transform:scale(1);opacity:1}.fui-motion-scale-in.fui-motion-entering{transform:scale(.95);opacity:0}.fui-no-motion{transition:none!important;animation:none!important}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@keyframes fui-pulse{0%{transform:scale(1);opacity:1}50%{transform:scale(1.05)}to{transform:scale(1);opacity:1}}@keyframes fui-slide-in{0%{transform:translate(120%)}to{transform:translate(0)}}.fui-slide-in{animation:fui-slide-in var(--fui-duration-moderate-01) var(--fui-ease-entrance)}@keyframes fui-popover-enter{0%{opacity:0;transform:translateY(-14px)}60%{opacity:1}to{opacity:1;transform:translateY(0)}}.fui-autocomplete{--fui-autocomplete-font-size: var(--fui-font-size-02);--fui-autocomplete-panel-max-height: 24rem;--fui-autocomplete-panel-border-radius: var(--fui-border-radius-sm);--fui-autocomplete-panel-shadow: var(--fui-shadow-04);--fui-autocomplete-panel-bg: var(--fui-surface-00);--fui-autocomplete-option-padding: .5rem .75rem;--fui-autocomplete-search-padding: .75rem;position:relative;display:inline-block;width:100%}.fui-autocomplete__options-source{display:none}.fui-autocomplete__trigger{display:flex;align-items:center;width:100%;border:none;color:var(--fui-text-primary);font-family:var(--fui-font-family-sans);font-size:var(--fui-font-size-02);line-height:1.29;letter-spacing:.16px;cursor:pointer;outline:none;transition:border-color var(--fui-duration-fast-01) var(--fui-ease-standard),background-color var(--fui-duration-fast-01) var(--fui-ease-standard);gap:.25rem}.fui-autocomplete__trigger:hover:not([aria-disabled=true]){background-color:var(--fui-field-background-hover)}.fui-autocomplete__trigger[aria-disabled=true]{background-color:var(--fui-field-background-disabled);color:var(--fui-text-primary-disabled);cursor:not-allowed}.fui-autocomplete__value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.fui-autocomplete__placeholder{color:var(--fui-text-secondary)}.fui-autocomplete__value-text{color:var(--fui-text-primary)}.fui-autocomplete__clear{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;cursor:pointer;color:var(--fui-icon-secondary);flex-shrink:0;border-radius:50%;transition:color var(--fui-duration-fast-01) var(--fui-ease-standard),background-color var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__clear svg{width:.75rem;height:.75rem}.fui-autocomplete__clear:hover{color:var(--fui-text-primary);background-color:var(--fui-background-hover)}.fui-autocomplete__arrow{width:1rem;height:1rem;flex-shrink:0;color:var(--fui-icon-primary);pointer-events:none;transition:transform var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__arrow--open{transform:rotate(180deg)}.fui-autocomplete__panel{position:absolute;width:100%;background-color:var(--fui-autocomplete-panel-bg);border:1px solid var(--fui-border-color);border-radius:var(--fui-autocomplete-panel-border-radius);box-shadow:var(--fui-autocomplete-panel-shadow);max-height:var(--fui-autocomplete-panel-max-height);overflow:hidden;display:flex;flex-direction:column}.fui-autocomplete__search{position:relative;padding:.75rem;border-bottom:1px solid var(--fui-border-color);background-color:var(--fui-field-background)}.fui-autocomplete__search-icon{position:absolute;left:1.5rem;top:50%;transform:translateY(-50%);color:var(--fui-icon-secondary);pointer-events:none}.fui-autocomplete__search-input{width:100%;padding:.5rem .75rem .5rem 2.5rem;border:1px solid var(--fui-border-color);border-radius:var(--fui-border-radius-sm);background-color:var(--fui-background-primary);color:var(--fui-text-primary);font-family:var(--fui-font-family-sans);font-size:var(--fui-font-size-02);line-height:1.29;letter-spacing:.16px;transition:border-color var(--fui-duration-fast-01) var(--fui-ease-standard),outline var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__search-input:focus{outline:2px solid var(--fui-primary);outline-offset:-2px;border-color:var(--fui-primary)}.fui-autocomplete__search-input::placeholder{color:var(--fui-text-secondary)}.fui-autocomplete__actions{display:flex;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--fui-border-color);background-color:var(--fui-field-background)}.fui-autocomplete__action-button{display:flex;align-items:center;gap:.25rem;font-size:var(--fui-font-size-01)}.fui-autocomplete__action-button fui-icon{flex-shrink:0}.fui-autocomplete__options{max-height:16rem;overflow-y:auto;padding:.25rem 0;background-color:var(--fui-surface-01);scrollbar-width:thin;scrollbar-color:var(--fui-border-color) transparent}.fui-autocomplete__options::-webkit-scrollbar{width:6px}.fui-autocomplete__options::-webkit-scrollbar-track{background:transparent}.fui-autocomplete__options::-webkit-scrollbar-thumb{background-color:var(--fui-border-color);border-radius:3px}.fui-autocomplete__options::-webkit-scrollbar-thumb:hover{background-color:var(--fui-text-secondary)}.fui-autocomplete__option{display:flex;align-items:center;justify-content:space-between;padding:.5rem .75rem;cursor:pointer;color:var(--fui-text-primary);font-size:var(--fui-font-size-02);line-height:1.29;letter-spacing:.16px;transition:background-color var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__option:hover:not(.fui-autocomplete__option--disabled){background-color:var(--fui-background-hover)}.fui-autocomplete__option--active{background-color:var(--fui-surface-05)}.fui-autocomplete__option--selected{background-color:var(--fui-surface-05);font-weight:600}.fui-autocomplete__option--disabled{color:var(--fui-text-primary-disabled);cursor:not-allowed}.fui-autocomplete__option-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fui-autocomplete__option-check{color:var(--fui-icon-primary);flex-shrink:0;margin-left:.5rem}.fui-autocomplete__no-options{padding:1rem .75rem;text-align:center;color:var(--fui-text-secondary);font-size:var(--fui-font-size-02);font-style:italic}.fui-autocomplete--disabled .fui-autocomplete__trigger{background-color:var(--fui-field-background-disabled);color:var(--fui-text-primary-disabled);cursor:not-allowed}.fui-autocomplete--disabled .fui-autocomplete__arrow{color:var(--fui-text-primary-disabled)}.fui-autocomplete--readonly .fui-autocomplete__trigger{background-color:transparent;min-height:unset;cursor:default;color:var(--fui-text-primary)}.fui-autocomplete--readonly .fui-autocomplete__trigger[aria-disabled=true]{background-color:transparent;color:var(--fui-text-primary);cursor:default}.fui-autocomplete--readonly .fui-autocomplete__value-text{color:var(--fui-text-primary)}.fui-autocomplete--error .fui-autocomplete__trigger{border-bottom-color:var(--fui-error)}.fui-autocomplete--error .fui-autocomplete__trigger:focus{outline-color:var(--fui-error)}.fui-autocomplete-overlay-panel .fui-autocomplete__panel{border:1px solid var(--fui-border-color);border-radius:var(--fui-border-radius-sm);box-shadow:var(--fui-shadow-04);transform-origin:top center;animation:fui-popover-enter var(--fui-duration-moderate-01) var(--fui-ease-entrance) both;will-change:transform,opacity}.fui-autocomplete-backdrop{background:transparent}@media(prefers-contrast:high){.fui-autocomplete__trigger,.fui-autocomplete__panel{border-width:2px}.fui-autocomplete__trigger:focus,.fui-autocomplete__search-input:focus{outline-width:3px}.fui-autocomplete__option--active,.fui-autocomplete__option--selected{outline:2px solid var(--fui-primary);outline-offset:-2px}}@media(prefers-reduced-motion:reduce){.fui-autocomplete__trigger,.fui-autocomplete__arrow,.fui-autocomplete__option,.fui-autocomplete__search-input{transition:none}.fui-autocomplete-overlay-panel .fui-autocomplete__panel{animation:none}}\n"], dependencies: [{ kind: "component", type: FuiIconComponent, selector: "fui-icon", inputs: ["name", "size", "weight", "color", "ariaLabel", "spin", "pulse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: FuiButtonDirective, selector: "button[fuiButton], a[fuiButton]", inputs: ["variant", "size", "disabled", "fullWidth", "loading", "iconOnly", "aria-label", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
488
+ {
489
+ provide: FUI_SELECT,
490
+ useExisting: FuiAutocompleteComponent,
491
+ },
492
+ ], queries: [{ propertyName: "options", predicate: FUI_OPTION, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true, static: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true }], hostDirectives: [{ directive: i1.FuiPopupOverlayDirective, inputs: ["positions", "positions", "panelClass", "panelClass", "backdropClass", "backdropClass", "scrollStrategy", "scrollStrategy", "minWidthFromTrigger", "minWidthFromTrigger"], outputs: ["openedChange", "openedChange", "escapeKey", "escapeKey"] }, { directive: i1.FuiFormControlSyncDirective }], ngImport: i0, template: "<!-- Trigger -->\r\n<div\r\n #trigger\r\n [id]=\"id\"\r\n class=\"fui-autocomplete__trigger\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n role=\"combobox\"\r\n [attr.aria-haspopup]=\"'listbox'\"\r\n [attr.aria-expanded]=\"panelOpen()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [attr.aria-invalid]=\"errorState()\"\r\n [attr.aria-describedby]=\"_ariaDescribedby\"\r\n [attr.aria-required]=\"required()\"\r\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-activedescendant]=\"panelOpen() ? _getActiveDescendant() : null\"\r\n [attr.aria-controls]=\"panelOpen() ? id + '-panel' : null\"\r\n (keydown)=\"_handleKeydown($event)\"\r\n>\r\n <span class=\"fui-autocomplete__value\">\r\n @if (empty()) {\r\n <span class=\"fui-autocomplete__placeholder\">{{ placeholder() }}</span>\r\n } @else {\r\n <span class=\"fui-autocomplete__value-text\">{{ displayValue() }}</span>\r\n }\r\n </span>\r\n\r\n @if (!readonly() && !disabled()) {\r\n @if (!empty()) {\r\n <button\r\n type=\"button\"\r\n class=\"fui-autocomplete__clear\"\r\n role=\"button\"\r\n [attr.aria-label]=\"intl.clearSelectionAriaLabel\"\r\n (click)=\"clear($event)\"\r\n >\r\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\r\n <path\r\n d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z\"\r\n />\r\n </svg>\r\n </button>\r\n }\r\n\r\n <span class=\"fui-autocomplete__arrow\" [class.fui-autocomplete__arrow--open]=\"panelOpen()\" aria-hidden=\"true\">\r\n <fui-icon name=\"caret-down\" size=\"sm\"></fui-icon>\r\n </span>\r\n }\r\n</div>\r\n\r\n<!-- Live region for screen reader announcements -->\r\n<span class=\"fui-sr-only\" aria-live=\"assertive\" aria-atomic=\"true\">{{ _liveAnnouncement() }}</span>\r\n\r\n<!-- Dropdown Panel (always rendered so #panel ViewChild is always available) -->\r\n<div #panel [hidden]=\"false\" [id]=\"id + '-panel'\" class=\"fui-autocomplete__panel\">\r\n <!-- Search Input -->\r\n <div class=\"fui-autocomplete__search-input\">\r\n <fui-form-field hideSubscript>\r\n <fui-icon fuiPrefix name=\"magnifying-glass\" size=\"sm\" />\r\n <input\r\n #searchInput\r\n fuiInput\r\n type=\"text\"\r\n [placeholder]=\"intl.searchPlaceholder\"\r\n [value]=\"searchTerm()\"\r\n autocomplete=\"off\"\r\n role=\"searchbox\"\r\n aria-label=\"Search\"\r\n (input)=\"onSearchInput($event)\"\r\n (keydown)=\"_handlePanelKeydown($event)\"\r\n />\r\n </fui-form-field>\r\n </div>\r\n\r\n <!-- Options List -->\r\n <div\r\n class=\"fui-autocomplete__options\"\r\n role=\"listbox\"\r\n [attr.aria-multiselectable]=\"multiple()\"\r\n [attr.aria-label]=\"placeholder()\"\r\n >\r\n <ng-content></ng-content>\r\n\r\n <!-- No results empty state when search has no matching options -->\r\n @if (searchTerm() && _visibleOptions().length === 0) {\r\n <div class=\"fui-autocomplete__no-results\" role=\"status\">\r\n {{ intl.noResultsText }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Panel Actions (e.g., Add new option, Refresh) -->\r\n <div class=\"fui-autocomplete__panel-actions\">\r\n <ng-content select=\"[fuiAutocompletePanelActions]\"></ng-content>\r\n </div>\r\n</div>\r\n", styles: ["@keyframes fui-skeleton-pulse{0%{opacity:1}50%{opacity:.4}to{opacity:1}}@keyframes fui-spin{to{transform:rotate(360deg)}}@keyframes fui-shake{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-2px)}20%,40%,60%,80%{transform:translate(2px)}}.fui-motion-fade-in{transition-property:opacity;transition-duration:var(--fui-duration-fast);transition-timing-function:var(--fui-ease-out);transition-delay:0ms}.fui-motion-fade-out{transition-property:opacity;transition-duration:var(--fui-duration-fast);transition-timing-function:var(--fui-ease-in);transition-delay:0ms}.fui-motion-slide-in-bottom{transition-property:transform;transition-duration:var(--fui-duration-base);transition-timing-function:var(--fui-ease-out);transition-delay:0ms;transform:translateY(0)}.fui-motion-slide-in-bottom.fui-motion-entering{transform:translateY(1rem)}.fui-motion-slide-in-top{transition-property:transform;transition-duration:var(--fui-duration-base);transition-timing-function:var(--fui-ease-out);transition-delay:0ms;transform:translateY(0)}.fui-motion-slide-in-top.fui-motion-entering{transform:translateY(-1rem)}.fui-motion-scale-in{transition-property:transform,opacity;transition-duration:var(--fui-duration-base);transition-timing-function:var(--fui-ease-out);transition-delay:0ms;transform:scale(1);opacity:1}.fui-motion-scale-in.fui-motion-entering{transform:scale(.95);opacity:0}.fui-no-motion{transition:none!important;animation:none!important}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@keyframes fui-pulse{0%{transform:scale(1);opacity:1}50%{transform:scale(1.05)}to{transform:scale(1);opacity:1}}@keyframes fui-slide-in{0%{transform:translate(120%)}to{transform:translate(0)}}.fui-slide-in{animation:fui-slide-in var(--fui-duration-base) var(--fui-ease-out)}@keyframes fui-popover-enter{0%{opacity:0;transform:translateY(-14px)}60%{opacity:1}to{opacity:1;transform:translateY(0)}}.fui-autocomplete{position:relative;display:inline-block;width:100%;min-width:8rem}.fui-autocomplete__trigger{display:flex;align-items:center;width:100%;border:none;color:var(--fui-text-primary);font-family:var(--fui-font-sans);font-size:var(--fui-text-base);line-height:1.29;letter-spacing:.16px;cursor:pointer;outline:none;gap:var(--fui-spacing-2)}.fui-autocomplete__trigger[aria-disabled=true]{color:var(--fui-text-disabled);cursor:not-allowed}.fui-autocomplete__value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.fui-autocomplete__placeholder{color:var(--fui-text-secondary)}.fui-autocomplete__value-text{color:var(--fui-text-primary)}.fui-autocomplete__clear{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;cursor:pointer;color:var(--fui-text-secondary);flex-shrink:0;border-radius:50%}.fui-autocomplete__clear svg{width:.75rem;height:.75rem}.fui-autocomplete__clear:hover{color:var(--fui-text-primary);background-color:var(--fui-bg-subtle)}.fui-autocomplete__arrow{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--fui-icon-xs);height:var(--fui-icon-xs);margin-left:var(--fui-spacing-2);color:var(--fui-text-secondary);pointer-events:none;transition:color var(--fui-duration-fast) var(--fui-ease-in-out),transform var(--fui-duration-fast) var(--fui-ease-in-out)}.fui-autocomplete__arrow--open{color:var(--fui-field-border-focus);transform:rotate(180deg)}.fui-autocomplete--disabled .fui-autocomplete__trigger{color:var(--fui-text-disabled);cursor:not-allowed}.fui-autocomplete--disabled .fui-autocomplete__arrow{color:var(--fui-text-disabled)}.fui-autocomplete--readonly .fui-autocomplete__trigger{background-color:transparent;min-height:unset;cursor:default;color:var(--fui-text-primary)}.fui-autocomplete .fui-autocomplete__panel{display:none}.fui-autocomplete__panel{--fui-autocomplete-panel-max-height: 24rem;--fui-autocomplete-panel-border-radius: var(--fui-radius-sm);--fui-autocomplete-panel-shadow: var(--fui-shadow-lg);--fui-autocomplete-panel-bg: var(--fui-bg-default);width:100%;background-color:var(--fui-autocomplete-panel-bg);border:var(--fui-border-width-sm) solid var(--fui-border-default);border-radius:var(--fui-autocomplete-panel-border-radius);box-shadow:var(--fui-autocomplete-panel-shadow);max-height:var(--fui-autocomplete-panel-max-height);overflow:hidden;display:flex;flex-direction:column;transform-origin:top center;padding:0 0 var(--fui-spacing-2) 0;animation:fui-popover-enter var(--fui-duration-base) var(--fui-ease-out) both;will-change:transform,opacity}.fui-autocomplete__panel .fui-autocomplete__search-input{padding:var(--fui-spacing-1) var(--fui-spacing-3)}.fui-autocomplete__panel .fui-autocomplete__no-results{padding:var(--fui-spacing-1) var(--fui-spacing-3);font-size:var(--fui-text-md);color:var(--fui-text-disabled)}.fui-autocomplete__options{padding-top:var(--fui-spacing-2);max-height:16rem;overflow-y:auto;background-color:var(--fui-bg-default);scrollbar-width:thin;scrollbar-color:var(--fui-border-default) transparent}.fui-autocomplete__options::-webkit-scrollbar{width:6px}.fui-autocomplete__options::-webkit-scrollbar-track{background:transparent}.fui-autocomplete__options::-webkit-scrollbar-thumb{background-color:var(--fui-border-default);border-radius:3px}.fui-autocomplete__options::-webkit-scrollbar-thumb:hover{background-color:var(--fui-text-secondary)}.fui-autocomplete__panel-actions{padding:var(--fui-spacing-4) var(--fui-spacing-4) 0 var(--fui-spacing-4)}.fui-autocomplete__panel-actions:empty{display:none}.fui-autocomplete-backdrop{background:transparent}@media(prefers-contrast:high){.fui-autocomplete__trigger{border-width:2px}.fui-autocomplete__trigger:focus{outline-width:3px}.fui-autocomplete__panel{border-width:2px}}@media(prefers-reduced-motion:reduce){.fui-autocomplete__trigger,.fui-autocomplete__arrow{transition:none}.fui-autocomplete__panel{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FuiIconComponent, selector: "fui-icon", inputs: ["name", "size", "weight", "color", "ariaLabel", "spin", "pulse"] }, { kind: "component", type: FuiFormFieldComponent, selector: "fui-form-field", inputs: ["appearance", "hideRequiredMarker", "hideSubscript"] }, { kind: "directive", type: FuiInputDirective, selector: "input[fuiInput], textarea[fuiInput], select[fuiInput]", inputs: ["type", "placeholder", "readonly", "maxlength", "minlength", "pattern", "errorStateMatcher", "disabled"], outputs: ["valueChange"] }, { kind: "directive", type: FuiPrefixDirective, selector: "[fuiPrefix]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
713
493
  }
714
494
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiAutocompleteComponent, decorators: [{
715
495
  type: Component,
716
- args: [{ selector: 'fui-autocomplete', standalone: true, imports: [FuiIconComponent, ReactiveFormsModule, FuiButtonDirective], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
496
+ args: [{ selector: 'fui-autocomplete', standalone: true, imports: [ReactiveFormsModule, FuiIconComponent, FuiFormFieldComponent, FuiInputDirective, FuiPrefixDirective], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, hostDirectives: [
497
+ {
498
+ directive: FuiPopupOverlayDirective,
499
+ inputs: ['positions', 'panelClass', 'backdropClass', 'scrollStrategy', 'minWidthFromTrigger'],
500
+ outputs: ['openedChange', 'escapeKey'],
501
+ },
502
+ FuiFormControlSyncDirective,
503
+ ], host: {
717
504
  class: 'fui-autocomplete',
718
505
  '[attr.id]': 'id',
719
506
  '[class.fui-autocomplete--open]': 'panelOpen()',
720
507
  '[class.fui-autocomplete--disabled]': 'disabled()',
721
508
  '[class.fui-autocomplete--focused]': 'focused()',
722
509
  '[class.fui-autocomplete--error]': 'errorState()',
723
- '[class.fui-autocomplete--readonly]': '_readOnly()',
510
+ '[class.fui-autocomplete--readonly]': 'readonly()',
511
+ '[class.fui-autocomplete--multiple]': 'multiple()',
512
+ '[attr.aria-readonly]': 'readonly() ? "true" : null',
724
513
  }, providers: [
725
514
  {
726
515
  provide: NG_VALUE_ACCESSOR,
@@ -735,17 +524,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
735
524
  provide: FUI_AUTOCOMPLETE,
736
525
  useExisting: FuiAutocompleteComponent,
737
526
  },
738
- ], template: "<!-- Autocomplete Trigger -->\r\n<div\r\n #trigger\r\n [id]=\"id\"\r\n class=\"fui-autocomplete__trigger\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n [attr.role]=\"'combobox'\"\r\n [attr.aria-haspopup]=\"'listbox'\"\r\n [attr.aria-expanded]=\"panelOpen()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [attr.aria-invalid]=\"errorState()\"\r\n [attr.aria-describedby]=\"_ariaDescribedby\"\r\n [attr.aria-required]=\"required()\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-activedescendant]=\"panelOpen() ? _getActiveDescendant() : null\"\r\n [attr.aria-controls]=\"panelOpen() ? id + '-panel' : null\"\r\n (keydown)=\"_handleKeydown($event)\"\r\n>\r\n <span class=\"fui-autocomplete__value\">\r\n @if (empty()) {\r\n <span class=\"fui-autocomplete__placeholder\">{{ placeholder() }}</span>\r\n } @else {\r\n <span class=\"fui-autocomplete__value-text\">{{ _getDisplayValue() }}</span>\r\n }\r\n </span>\r\n\r\n @if (!_readOnly()) {\r\n <!-- Clear button \u2014 visible only when a value is selected and not disabled -->\r\n @if (!empty() && !disabled()) {\r\n <span class=\"fui-autocomplete__clear\" role=\"button\" [attr.aria-label]=\"intl.clearSelectionAriaLabel\" (click)=\"clear($event)\">\r\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\r\n <path\r\n d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z\"\r\n />\r\n </svg>\r\n </span>\r\n }\r\n\r\n <!-- Dropdown arrow icon -->\r\n <svg\r\n class=\"fui-autocomplete__arrow\"\r\n [class.fui-autocomplete__arrow--open]=\"panelOpen()\"\r\n viewBox=\"0 0 16 16\"\r\n fill=\"currentColor\"\r\n aria-hidden=\"true\"\r\n >\r\n <path d=\"M8 11L3 6l.7-.7L8 9.6l4.3-4.3L13 6z\" />\r\n </svg>\r\n }\r\n</div>\r\n\r\n<!-- Hidden container for projected options (used for data binding) -->\r\n<div class=\"fui-autocomplete__options-source\">\r\n <ng-content></ng-content>\r\n</div>\r\n\r\n<!-- Dropdown Panel -->\r\n@if (panelOpen()) {\r\n <div #panel [id]=\"id + '-panel'\" class=\"fui-autocomplete__panel\" [attr.aria-label]=\"placeholder()\">\r\n <!-- Search Input -->\r\n <div class=\"fui-autocomplete__search\">\r\n <fui-icon name=\"magnifying-glass\" size=\"sm\" class=\"fui-autocomplete__search-icon\"> </fui-icon>\r\n <input\r\n #searchInput\r\n type=\"text\"\r\n class=\"fui-autocomplete__search-input\"\r\n [placeholder]=\"resolvedSearchPlaceholder()\"\r\n [value]=\"searchTerm()\"\r\n autocomplete=\"off\"\r\n role=\"searchbox\"\r\n [attr.aria-label]=\"resolvedSearchPlaceholder()\"\r\n [attr.aria-controls]=\"id + '-panel-listbox'\"\r\n [attr.aria-activedescendant]=\"_getActiveDescendant()\"\r\n (input)=\"onSearchInput($event)\"\r\n (keydown)=\"_handlePanelKeydown($event)\"\r\n />\r\n </div>\r\n <!-- Live region for announcing result count to screen readers -->\r\n <div class=\"fui-sr-only\" aria-live=\"assertive\" aria-atomic=\"true\">\r\n @if (_liveAnnouncement()) {\r\n {{ _liveAnnouncement() }}\r\n } @else if (hasFilteredOptions()) {\r\n {{ intl.resultsAvailableText(filteredOptions().length) }}\r\n } @else {\r\n {{ intl.noResultsAvailableText }}\r\n }\r\n </div>\r\n\r\n <!-- Action Buttons -->\r\n @if (showAddButton() || showRefreshButton()) {\r\n <div class=\"fui-autocomplete__actions\">\r\n @if (showAddButton()) {\r\n <button\r\n type=\"button\"\r\n fuiButton\r\n variant=\"tertiary\"\r\n size=\"sm\"\r\n class=\"fui-autocomplete__action-button\"\r\n (click)=\"onAddNew()\"\r\n >\r\n <fui-icon name=\"plus\" size=\"sm\"></fui-icon>\r\n {{ resolvedAddButtonLabel() }}\r\n </button>\r\n }\r\n @if (showRefreshButton()) {\r\n <button\r\n type=\"button\"\r\n fuiButton\r\n variant=\"tertiary\"\r\n size=\"sm\"\r\n class=\"fui-autocomplete__action-button\"\r\n (click)=\"onRefresh()\"\r\n >\r\n <fui-icon name=\"refresh-cw\" size=\"sm\"></fui-icon>\r\n {{ resolvedRefreshButtonLabel() }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Options List -->\r\n <div\r\n class=\"fui-autocomplete__options\"\r\n [id]=\"id + '-panel-listbox'\"\r\n role=\"listbox\"\r\n [attr.aria-label]=\"placeholder()\"\r\n >\r\n @if (hasFilteredOptions()) {\r\n @for (option of filteredOptions(); track option.id; let i = $index) {\r\n <div\r\n class=\"fui-autocomplete__option\"\r\n [class.fui-autocomplete__option--active]=\"activeOptionIndex() === i\"\r\n [class.fui-autocomplete__option--selected]=\"option._selected()\"\r\n role=\"option\"\r\n [attr.id]=\"option.id\"\r\n [attr.aria-selected]=\"option._selected()\"\r\n (click)=\"_onOptionSelected(option)\"\r\n (mouseenter)=\"setActiveOption(i, option)\"\r\n (mouseleave)=\"option.setInactive()\"\r\n >\r\n <span class=\"fui-autocomplete__option-text\">{{ option.getLabel() }}</span>\r\n @if (option._selected()) {\r\n <fui-icon name=\"check\" size=\"sm\" class=\"fui-autocomplete__option-check\"></fui-icon>\r\n }\r\n </div>\r\n }\r\n } @else {\r\n <div class=\"fui-autocomplete__no-options\">\r\n {{ resolvedNoOptionsText() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: ["@keyframes fui-skeleton-pulse{0%{opacity:1}50%{opacity:.4}to{opacity:1}}@keyframes fui-spin{to{transform:rotate(360deg)}}@keyframes fui-shake{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-2px)}20%,40%,60%,80%{transform:translate(2px)}}.fui-motion-fade-in{transition:opacity var(--fui-duration-fast-02) var(--fui-ease-entrance) 0ms}.fui-motion-fade-out{transition:opacity var(--fui-duration-fast-01) var(--fui-ease-exit) 0ms}.fui-motion-slide-in-bottom{transition:transform var(--fui-duration-moderate-01) var(--fui-ease-entrance) 0ms;transform:translateY(0)}.fui-motion-slide-in-bottom.fui-motion-entering{transform:translateY(1rem)}.fui-motion-slide-in-top{transition:transform var(--fui-duration-moderate-01) var(--fui-ease-entrance) 0ms;transform:translateY(0)}.fui-motion-slide-in-top.fui-motion-entering{transform:translateY(-1rem)}.fui-motion-scale-in{transition:transform,opacity var(--fui-duration-moderate-01) var(--fui-ease-entrance) 0ms;transform:scale(1);opacity:1}.fui-motion-scale-in.fui-motion-entering{transform:scale(.95);opacity:0}.fui-no-motion{transition:none!important;animation:none!important}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@keyframes fui-pulse{0%{transform:scale(1);opacity:1}50%{transform:scale(1.05)}to{transform:scale(1);opacity:1}}@keyframes fui-slide-in{0%{transform:translate(120%)}to{transform:translate(0)}}.fui-slide-in{animation:fui-slide-in var(--fui-duration-moderate-01) var(--fui-ease-entrance)}@keyframes fui-popover-enter{0%{opacity:0;transform:translateY(-14px)}60%{opacity:1}to{opacity:1;transform:translateY(0)}}.fui-autocomplete{--fui-autocomplete-font-size: var(--fui-font-size-02);--fui-autocomplete-panel-max-height: 24rem;--fui-autocomplete-panel-border-radius: var(--fui-border-radius-sm);--fui-autocomplete-panel-shadow: var(--fui-shadow-04);--fui-autocomplete-panel-bg: var(--fui-surface-00);--fui-autocomplete-option-padding: .5rem .75rem;--fui-autocomplete-search-padding: .75rem;position:relative;display:inline-block;width:100%}.fui-autocomplete__options-source{display:none}.fui-autocomplete__trigger{display:flex;align-items:center;width:100%;border:none;color:var(--fui-text-primary);font-family:var(--fui-font-family-sans);font-size:var(--fui-font-size-02);line-height:1.29;letter-spacing:.16px;cursor:pointer;outline:none;transition:border-color var(--fui-duration-fast-01) var(--fui-ease-standard),background-color var(--fui-duration-fast-01) var(--fui-ease-standard);gap:.25rem}.fui-autocomplete__trigger:hover:not([aria-disabled=true]){background-color:var(--fui-field-background-hover)}.fui-autocomplete__trigger[aria-disabled=true]{background-color:var(--fui-field-background-disabled);color:var(--fui-text-primary-disabled);cursor:not-allowed}.fui-autocomplete__value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.fui-autocomplete__placeholder{color:var(--fui-text-secondary)}.fui-autocomplete__value-text{color:var(--fui-text-primary)}.fui-autocomplete__clear{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;cursor:pointer;color:var(--fui-icon-secondary);flex-shrink:0;border-radius:50%;transition:color var(--fui-duration-fast-01) var(--fui-ease-standard),background-color var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__clear svg{width:.75rem;height:.75rem}.fui-autocomplete__clear:hover{color:var(--fui-text-primary);background-color:var(--fui-background-hover)}.fui-autocomplete__arrow{width:1rem;height:1rem;flex-shrink:0;color:var(--fui-icon-primary);pointer-events:none;transition:transform var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__arrow--open{transform:rotate(180deg)}.fui-autocomplete__panel{position:absolute;width:100%;background-color:var(--fui-autocomplete-panel-bg);border:1px solid var(--fui-border-color);border-radius:var(--fui-autocomplete-panel-border-radius);box-shadow:var(--fui-autocomplete-panel-shadow);max-height:var(--fui-autocomplete-panel-max-height);overflow:hidden;display:flex;flex-direction:column}.fui-autocomplete__search{position:relative;padding:.75rem;border-bottom:1px solid var(--fui-border-color);background-color:var(--fui-field-background)}.fui-autocomplete__search-icon{position:absolute;left:1.5rem;top:50%;transform:translateY(-50%);color:var(--fui-icon-secondary);pointer-events:none}.fui-autocomplete__search-input{width:100%;padding:.5rem .75rem .5rem 2.5rem;border:1px solid var(--fui-border-color);border-radius:var(--fui-border-radius-sm);background-color:var(--fui-background-primary);color:var(--fui-text-primary);font-family:var(--fui-font-family-sans);font-size:var(--fui-font-size-02);line-height:1.29;letter-spacing:.16px;transition:border-color var(--fui-duration-fast-01) var(--fui-ease-standard),outline var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__search-input:focus{outline:2px solid var(--fui-primary);outline-offset:-2px;border-color:var(--fui-primary)}.fui-autocomplete__search-input::placeholder{color:var(--fui-text-secondary)}.fui-autocomplete__actions{display:flex;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--fui-border-color);background-color:var(--fui-field-background)}.fui-autocomplete__action-button{display:flex;align-items:center;gap:.25rem;font-size:var(--fui-font-size-01)}.fui-autocomplete__action-button fui-icon{flex-shrink:0}.fui-autocomplete__options{max-height:16rem;overflow-y:auto;padding:.25rem 0;background-color:var(--fui-surface-01);scrollbar-width:thin;scrollbar-color:var(--fui-border-color) transparent}.fui-autocomplete__options::-webkit-scrollbar{width:6px}.fui-autocomplete__options::-webkit-scrollbar-track{background:transparent}.fui-autocomplete__options::-webkit-scrollbar-thumb{background-color:var(--fui-border-color);border-radius:3px}.fui-autocomplete__options::-webkit-scrollbar-thumb:hover{background-color:var(--fui-text-secondary)}.fui-autocomplete__option{display:flex;align-items:center;justify-content:space-between;padding:.5rem .75rem;cursor:pointer;color:var(--fui-text-primary);font-size:var(--fui-font-size-02);line-height:1.29;letter-spacing:.16px;transition:background-color var(--fui-duration-fast-01) var(--fui-ease-standard)}.fui-autocomplete__option:hover:not(.fui-autocomplete__option--disabled){background-color:var(--fui-background-hover)}.fui-autocomplete__option--active{background-color:var(--fui-surface-05)}.fui-autocomplete__option--selected{background-color:var(--fui-surface-05);font-weight:600}.fui-autocomplete__option--disabled{color:var(--fui-text-primary-disabled);cursor:not-allowed}.fui-autocomplete__option-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fui-autocomplete__option-check{color:var(--fui-icon-primary);flex-shrink:0;margin-left:.5rem}.fui-autocomplete__no-options{padding:1rem .75rem;text-align:center;color:var(--fui-text-secondary);font-size:var(--fui-font-size-02);font-style:italic}.fui-autocomplete--disabled .fui-autocomplete__trigger{background-color:var(--fui-field-background-disabled);color:var(--fui-text-primary-disabled);cursor:not-allowed}.fui-autocomplete--disabled .fui-autocomplete__arrow{color:var(--fui-text-primary-disabled)}.fui-autocomplete--readonly .fui-autocomplete__trigger{background-color:transparent;min-height:unset;cursor:default;color:var(--fui-text-primary)}.fui-autocomplete--readonly .fui-autocomplete__trigger[aria-disabled=true]{background-color:transparent;color:var(--fui-text-primary);cursor:default}.fui-autocomplete--readonly .fui-autocomplete__value-text{color:var(--fui-text-primary)}.fui-autocomplete--error .fui-autocomplete__trigger{border-bottom-color:var(--fui-error)}.fui-autocomplete--error .fui-autocomplete__trigger:focus{outline-color:var(--fui-error)}.fui-autocomplete-overlay-panel .fui-autocomplete__panel{border:1px solid var(--fui-border-color);border-radius:var(--fui-border-radius-sm);box-shadow:var(--fui-shadow-04);transform-origin:top center;animation:fui-popover-enter var(--fui-duration-moderate-01) var(--fui-ease-entrance) both;will-change:transform,opacity}.fui-autocomplete-backdrop{background:transparent}@media(prefers-contrast:high){.fui-autocomplete__trigger,.fui-autocomplete__panel{border-width:2px}.fui-autocomplete__trigger:focus,.fui-autocomplete__search-input:focus{outline-width:3px}.fui-autocomplete__option--active,.fui-autocomplete__option--selected{outline:2px solid var(--fui-primary);outline-offset:-2px}}@media(prefers-reduced-motion:reduce){.fui-autocomplete__trigger,.fui-autocomplete__arrow,.fui-autocomplete__option,.fui-autocomplete__search-input{transition:none}.fui-autocomplete-overlay-panel .fui-autocomplete__panel{animation:none}}\n"] }]
739
- }], ctorParameters: () => [], propDecorators: { placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], showAddButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddButton", required: false }] }], showRefreshButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRefreshButton", required: false }] }], addButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonLabel", required: false }] }], refreshButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "refreshButtonLabel", required: false }] }], noOptionsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsText", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], addNew: [{ type: i0.Output, args: ["addNew"] }], refresh: [{ type: i0.Output, args: ["refresh"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], trigger: [{
527
+ {
528
+ provide: FUI_SELECT,
529
+ useExisting: FuiAutocompleteComponent,
530
+ },
531
+ ], template: "<!-- Trigger -->\r\n<div\r\n #trigger\r\n [id]=\"id\"\r\n class=\"fui-autocomplete__trigger\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n role=\"combobox\"\r\n [attr.aria-haspopup]=\"'listbox'\"\r\n [attr.aria-expanded]=\"panelOpen()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [attr.aria-invalid]=\"errorState()\"\r\n [attr.aria-describedby]=\"_ariaDescribedby\"\r\n [attr.aria-required]=\"required()\"\r\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-activedescendant]=\"panelOpen() ? _getActiveDescendant() : null\"\r\n [attr.aria-controls]=\"panelOpen() ? id + '-panel' : null\"\r\n (keydown)=\"_handleKeydown($event)\"\r\n>\r\n <span class=\"fui-autocomplete__value\">\r\n @if (empty()) {\r\n <span class=\"fui-autocomplete__placeholder\">{{ placeholder() }}</span>\r\n } @else {\r\n <span class=\"fui-autocomplete__value-text\">{{ displayValue() }}</span>\r\n }\r\n </span>\r\n\r\n @if (!readonly() && !disabled()) {\r\n @if (!empty()) {\r\n <button\r\n type=\"button\"\r\n class=\"fui-autocomplete__clear\"\r\n role=\"button\"\r\n [attr.aria-label]=\"intl.clearSelectionAriaLabel\"\r\n (click)=\"clear($event)\"\r\n >\r\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\r\n <path\r\n d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z\"\r\n />\r\n </svg>\r\n </button>\r\n }\r\n\r\n <span class=\"fui-autocomplete__arrow\" [class.fui-autocomplete__arrow--open]=\"panelOpen()\" aria-hidden=\"true\">\r\n <fui-icon name=\"caret-down\" size=\"sm\"></fui-icon>\r\n </span>\r\n }\r\n</div>\r\n\r\n<!-- Live region for screen reader announcements -->\r\n<span class=\"fui-sr-only\" aria-live=\"assertive\" aria-atomic=\"true\">{{ _liveAnnouncement() }}</span>\r\n\r\n<!-- Dropdown Panel (always rendered so #panel ViewChild is always available) -->\r\n<div #panel [hidden]=\"false\" [id]=\"id + '-panel'\" class=\"fui-autocomplete__panel\">\r\n <!-- Search Input -->\r\n <div class=\"fui-autocomplete__search-input\">\r\n <fui-form-field hideSubscript>\r\n <fui-icon fuiPrefix name=\"magnifying-glass\" size=\"sm\" />\r\n <input\r\n #searchInput\r\n fuiInput\r\n type=\"text\"\r\n [placeholder]=\"intl.searchPlaceholder\"\r\n [value]=\"searchTerm()\"\r\n autocomplete=\"off\"\r\n role=\"searchbox\"\r\n aria-label=\"Search\"\r\n (input)=\"onSearchInput($event)\"\r\n (keydown)=\"_handlePanelKeydown($event)\"\r\n />\r\n </fui-form-field>\r\n </div>\r\n\r\n <!-- Options List -->\r\n <div\r\n class=\"fui-autocomplete__options\"\r\n role=\"listbox\"\r\n [attr.aria-multiselectable]=\"multiple()\"\r\n [attr.aria-label]=\"placeholder()\"\r\n >\r\n <ng-content></ng-content>\r\n\r\n <!-- No results empty state when search has no matching options -->\r\n @if (searchTerm() && _visibleOptions().length === 0) {\r\n <div class=\"fui-autocomplete__no-results\" role=\"status\">\r\n {{ intl.noResultsText }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Panel Actions (e.g., Add new option, Refresh) -->\r\n <div class=\"fui-autocomplete__panel-actions\">\r\n <ng-content select=\"[fuiAutocompletePanelActions]\"></ng-content>\r\n </div>\r\n</div>\r\n", styles: ["@keyframes fui-skeleton-pulse{0%{opacity:1}50%{opacity:.4}to{opacity:1}}@keyframes fui-spin{to{transform:rotate(360deg)}}@keyframes fui-shake{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-2px)}20%,40%,60%,80%{transform:translate(2px)}}.fui-motion-fade-in{transition-property:opacity;transition-duration:var(--fui-duration-fast);transition-timing-function:var(--fui-ease-out);transition-delay:0ms}.fui-motion-fade-out{transition-property:opacity;transition-duration:var(--fui-duration-fast);transition-timing-function:var(--fui-ease-in);transition-delay:0ms}.fui-motion-slide-in-bottom{transition-property:transform;transition-duration:var(--fui-duration-base);transition-timing-function:var(--fui-ease-out);transition-delay:0ms;transform:translateY(0)}.fui-motion-slide-in-bottom.fui-motion-entering{transform:translateY(1rem)}.fui-motion-slide-in-top{transition-property:transform;transition-duration:var(--fui-duration-base);transition-timing-function:var(--fui-ease-out);transition-delay:0ms;transform:translateY(0)}.fui-motion-slide-in-top.fui-motion-entering{transform:translateY(-1rem)}.fui-motion-scale-in{transition-property:transform,opacity;transition-duration:var(--fui-duration-base);transition-timing-function:var(--fui-ease-out);transition-delay:0ms;transform:scale(1);opacity:1}.fui-motion-scale-in.fui-motion-entering{transform:scale(.95);opacity:0}.fui-no-motion{transition:none!important;animation:none!important}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@keyframes fui-pulse{0%{transform:scale(1);opacity:1}50%{transform:scale(1.05)}to{transform:scale(1);opacity:1}}@keyframes fui-slide-in{0%{transform:translate(120%)}to{transform:translate(0)}}.fui-slide-in{animation:fui-slide-in var(--fui-duration-base) var(--fui-ease-out)}@keyframes fui-popover-enter{0%{opacity:0;transform:translateY(-14px)}60%{opacity:1}to{opacity:1;transform:translateY(0)}}.fui-autocomplete{position:relative;display:inline-block;width:100%;min-width:8rem}.fui-autocomplete__trigger{display:flex;align-items:center;width:100%;border:none;color:var(--fui-text-primary);font-family:var(--fui-font-sans);font-size:var(--fui-text-base);line-height:1.29;letter-spacing:.16px;cursor:pointer;outline:none;gap:var(--fui-spacing-2)}.fui-autocomplete__trigger[aria-disabled=true]{color:var(--fui-text-disabled);cursor:not-allowed}.fui-autocomplete__value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.fui-autocomplete__placeholder{color:var(--fui-text-secondary)}.fui-autocomplete__value-text{color:var(--fui-text-primary)}.fui-autocomplete__clear{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;cursor:pointer;color:var(--fui-text-secondary);flex-shrink:0;border-radius:50%}.fui-autocomplete__clear svg{width:.75rem;height:.75rem}.fui-autocomplete__clear:hover{color:var(--fui-text-primary);background-color:var(--fui-bg-subtle)}.fui-autocomplete__arrow{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--fui-icon-xs);height:var(--fui-icon-xs);margin-left:var(--fui-spacing-2);color:var(--fui-text-secondary);pointer-events:none;transition:color var(--fui-duration-fast) var(--fui-ease-in-out),transform var(--fui-duration-fast) var(--fui-ease-in-out)}.fui-autocomplete__arrow--open{color:var(--fui-field-border-focus);transform:rotate(180deg)}.fui-autocomplete--disabled .fui-autocomplete__trigger{color:var(--fui-text-disabled);cursor:not-allowed}.fui-autocomplete--disabled .fui-autocomplete__arrow{color:var(--fui-text-disabled)}.fui-autocomplete--readonly .fui-autocomplete__trigger{background-color:transparent;min-height:unset;cursor:default;color:var(--fui-text-primary)}.fui-autocomplete .fui-autocomplete__panel{display:none}.fui-autocomplete__panel{--fui-autocomplete-panel-max-height: 24rem;--fui-autocomplete-panel-border-radius: var(--fui-radius-sm);--fui-autocomplete-panel-shadow: var(--fui-shadow-lg);--fui-autocomplete-panel-bg: var(--fui-bg-default);width:100%;background-color:var(--fui-autocomplete-panel-bg);border:var(--fui-border-width-sm) solid var(--fui-border-default);border-radius:var(--fui-autocomplete-panel-border-radius);box-shadow:var(--fui-autocomplete-panel-shadow);max-height:var(--fui-autocomplete-panel-max-height);overflow:hidden;display:flex;flex-direction:column;transform-origin:top center;padding:0 0 var(--fui-spacing-2) 0;animation:fui-popover-enter var(--fui-duration-base) var(--fui-ease-out) both;will-change:transform,opacity}.fui-autocomplete__panel .fui-autocomplete__search-input{padding:var(--fui-spacing-1) var(--fui-spacing-3)}.fui-autocomplete__panel .fui-autocomplete__no-results{padding:var(--fui-spacing-1) var(--fui-spacing-3);font-size:var(--fui-text-md);color:var(--fui-text-disabled)}.fui-autocomplete__options{padding-top:var(--fui-spacing-2);max-height:16rem;overflow-y:auto;background-color:var(--fui-bg-default);scrollbar-width:thin;scrollbar-color:var(--fui-border-default) transparent}.fui-autocomplete__options::-webkit-scrollbar{width:6px}.fui-autocomplete__options::-webkit-scrollbar-track{background:transparent}.fui-autocomplete__options::-webkit-scrollbar-thumb{background-color:var(--fui-border-default);border-radius:3px}.fui-autocomplete__options::-webkit-scrollbar-thumb:hover{background-color:var(--fui-text-secondary)}.fui-autocomplete__panel-actions{padding:var(--fui-spacing-4) var(--fui-spacing-4) 0 var(--fui-spacing-4)}.fui-autocomplete__panel-actions:empty{display:none}.fui-autocomplete-backdrop{background:transparent}@media(prefers-contrast:high){.fui-autocomplete__trigger{border-width:2px}.fui-autocomplete__trigger:focus{outline-width:3px}.fui-autocomplete__panel{border-width:2px}}@media(prefers-reduced-motion:reduce){.fui-autocomplete__trigger,.fui-autocomplete__arrow{transition:none}.fui-autocomplete__panel{animation:none}}\n"] }]
532
+ }], ctorParameters: () => [], propDecorators: { placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], trigger: [{
740
533
  type: ViewChild,
741
534
  args: ['trigger', { static: false }]
742
535
  }], panel: [{
743
536
  type: ViewChild,
744
- args: ['panel', { static: false }]
537
+ args: ['panel', { static: true }]
745
538
  }], searchInput: [{
746
539
  type: ViewChild,
747
540
  args: ['searchInput', { static: false }]
748
- }], options: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FuiOptionComponent), { ...{ descendants: true }, isSignal: true }] }] } });
541
+ }], options: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FUI_OPTION), { ...{ descendants: true }, isSignal: true }] }] } });
749
542
 
750
543
  /**
751
544
  * Generated bundle index. Do not edit.